Inheritance with structs - with full safety (i.e. no value slicing)

Topics: C# Language Design
Sep 26, 2014 at 4:55 PM
Edited Sep 29, 2014 at 2:58 PM
C# has never permitted struct inheritance, for very good reasons - true polymorphism would cause value types to be sliced.

However, structs turn out to be highly useful in avoiding the mandatory heap allocation that class instantiation causes, particularly on mobile devices. The problem is that every struct has to be written from scratch, including all its methods and containers. None of the benefits of inheritance, even the simplest, are therefore available to structs.

So perhaps it is time to revisit struct inheritance, keeping in view that generics have substantially improved the expressiveness of the type system.

Problem statement: Is there a way to allow for structs inheritance that:
  • Does not involve contravening type safety guarantees or cause type slicing? In other words, can C# give developers the benefit of allowing structs to derive from other structs without any of the pitfalls of polymorphism?
  • Requires no changes to the .net runtime?
As an example, consider a tree whose nodes are either all of type Node (which only has an Id property), or all of type Person, which has an Id and a Name property.
struct Node
{
    int Id { get; set; }
}

struct Person
{
    int Id { get; set; }
    string Name {get; set;}
}

For speed we implement the tree as a flat list, where each node is an element in the list, and so has Index property. Since there is no polymorphism with structs, we have entirely separate types. Here's what the tree code now looks like

A Node tree:
struct Node
{
    int Id { get; set; }
    int Index { get; set; } // the index within NodeTree.Items of this Node.
    int ParentIndex { get; set; }
    int LeftChildIndex { get; set; }
    int RightChildIndex { get; set; }
    void SetLeft(ref Node child) { ... } // sets the left child
    void SetRight(ref Node child) { ... } // sets the left child
}

class NodeTree
{
    List<Node> Items = new List<Node>();
    // implement tree operations
}
PersonTree looks exactly the same, but with the extra Name field in Person:
struct Person
{
    int Id { get; set; }
    int Index { get; set; }
    int ParentIndex { get; set; }
    int LeftChildIndex { get; set; }
    int RightChildIndex { get; set; }
    void SetLeft(ref Person child) { ... } // sets the left child
    void SetRight(ref Person child) { ... } // sets the left child
    string Name { get; set; }
}

class PersonTree
{
    List<Person> Items = new List<Person>();
    // implement tree operations
}
This is the crux of the issue: implementing any kind of inheritance in structs is basically about cutting and pasting code into new types.

Derived Structs

I propose allowing structs to derive from other user-defined (i.e. non-builtin) structs like so:
struct Person: Node
{
    string Name { get; set; }
}
What does this mean? Well, we're telling the C# compiler that we want it to generate a new type, Person, that has all the fields, properties and methods as Node, but with all references to anything that's a Node to be replaced by Person. So here's the code that it generates. Notice how Person.SetLeft() and Person.SetRight() take Person parameters. (The code below is one possible implementation; there are other possibilities also.)
interface __INode<T> where T: __INode<T> // automatically generated by the C# compiler
{
    int Id { get; set; }
    int Index { get; set; }
    int ParentIndex { get; set; }
    int LeftChildIndex { get; set; }
    int RightChildIndex { get; set; }
    void SetLeft(ref T child) { ... } // sets the left child
    void SetRight(ref T child) { ... } // sets the left child
}

struct Node : __INode<Node> // the C# compiler adds : __INode<Node>; the rest of the type is unchanged
{
    int Id { get; set; }
    int Index { get; set; }
    int ParentIndex { get; set; }
    int LeftChildIndex { get; set; }
    int RightChildIndex { get; set; }
    void SetLeft(ref Node child) { ... } // sets the left child
    void SetRight(ref Node child) { ... } // sets the left child
}

struct Person : __INode<Person>
{
    // Copy all methods and fields from Node, replacing Node with Person
    int Id { get; set; }
    int Index { get; set; }
    int ParentIndex { get; set; }
    int LeftChildIndex { get; set; }
    int RightChildIndex { get; set; }
    void SetLeft(ref Person child) { ... } // sets the left child
    void SetRight(ref Person child) { ... } // sets the left child

    // Explicitly declared on Person
    string Name { get; set; }
}
So, the code for Person is generated from Node. Also, Person and Node both inherit from the generated interface __INode<T>, whose purpose is described soon. But there is no other relationship between Person and Node. There is no polymorphism, and so no type-slicing. A method that accepts a Node will not accept a Person, and vice-versa. However, a generic method that takes an __INode<T> will happily accept either a Node or Person. The only thing is, __INode<T> is generated, so how do we specify it?

Enter generic value type inheritance constraints. A new kind of constraint, very similar to how we specified that Person inherits from Node, allows us to specify the hidden interface implicitly:
class Tree<T> where T: Node
{
    List<T> Items = new List<T>();
    // implement tree operations
}
The compiler simply translates where T: Node constraint to where T: struct, __INode<T>.

Now the purpose of __INode<T> becomes clearer. All the generated code you see above compiles fine in today's C#. Automatically generating __INode<T> and adding it to Node and derived structs just reuses mechanisms that C# already has. This allows the where T:Node constraint to be easily converted to something the compiler already understands, i.e. where T: struct, __INode<T>.

Thus, the incremental change to the compiler is minimal. You cannot use __INode<T> directly from your code; as a matter of fact the name produced by the compiler will actually be impossible to guess. Generating hidden types is a pretty standard technique in C# - it is used in events, async/await and other places.

Now NodeTree in the first example can be replaced with Tree<Node> and PersonTree with Tree<Person>. Because generics are instantiated per value type, this works with full safety, no boxing/unboxing, and again no type-slicing.

(For those unfamiliar with how generics work with value types, this bears repeating: when a generic is instantiated with a value type, the runtime (even today) generates a new set of code specifically for that value type. This avoids casting to and from Object, and so avoids boxing/unboxing. Thus a List<double> under the covers has an array of size (in bytes) sizeof(double) * Count, while a List<byte> has an underlying array of size Count, in bytes.)

Summary

I propose:
  • A syntax for deriving a struct from another user-defined (i.e. non-builtin) struct.
  • A value type inheritance constraint on generics.
  • An implementation that requires changes to the C# compiler, but no changes to the .net runtime. This is one possible implementation; there may be other ways to implement the same or similar syntax under the covers. What is important are the benefits, not so much the mechanism of implementation.
The result is:
  • Massive simplification of structs that need to share code in an inheritance-like pattern
  • This makes using structs in complex data structures truly viable because class inheritance hierarchies can be converted to struct inheritance hierarchies. The speed improvements can be dramatic in large data structures that currently do millions of object allocations simply because they use classes where they could use structs.

Limitation

An intentional limitation in this proposal is that you can't derive a struct from a builtin, such as int or double. While there may be benefits in allowing such inheritance, I have tried to limit the problem space to the scenarios described.

The result is a very natural syntax that gives structs much of the inheritance power of classes while retaining type safety.
Sep 26, 2014 at 5:22 PM
I think that you'd be talking about a fairly radical change to the entire .NET runtime in order to support this. The limitation is not imposed by the C# language.

This post on StackOverflow explains one of the reasons for this limitation:

http://stackoverflow.com/questions/1222935/why-dont-structs-support-inheritance
Sep 26, 2014 at 5:24 PM
Edited Sep 26, 2014 at 5:30 PM
I don't think this requires anything from the runtime - just automatically generated code by the C# compiler. Am I missing something?

The StackOverflow post, and others like it, talk about polymorphism in structs being unviable. I'm not suggesting that. As mentioned in the original post, the value types Node and Person are not polymorphic. They are entirely separate.
Sep 26, 2014 at 7:55 PM
Had a few parts of the runtime been designed a tiny bit differently, it would be possible to do much more with the type system. Unfortunately, I think there's a pretty hard rule which requires that a storage location of type System.Enum or any type which does not derive directly from either System.ValueType or System.Enum will be treated as a reference type.

I think the simplest change that would make achievable the kinds of things for which "struct inheritance" would be useful would be to add a couple of virtual methods to System.ValueType and change the existing boxing and unboxing opcodes to use them. ValueType's implementation of of virtual Object Box() would use some "runtime magic" to behave as the current boxing instruction does, and its virtual void UnboxValueFrom(Object) would do likewise (unboxing into "this"). Such an approach would, in conjunction with implicit casts to and from other value types, make it possible to have one value type behave in a fashion that's substitutable for another. Although there are actually two forms of "unbox" instruction, one of which yields a value and one of which yields a "byref", I don't think the one that yields a "byref" would ever be called upon to access a boxed object as anything other than its own type, so I don't think a virtual method would be needed for that.
Sep 27, 2014 at 2:36 AM
aspiring wrote:
I don't think this requires anything from the runtime - just automatically generated code by the C# compiler. Am I missing something?

The StackOverflow post, and others like it, talk about polymorphism in structs being unviable. I'm not suggesting that. As mentioned in the original post, the value types Node and Person are not polymorphic. They are entirely separate.
Well if all you're looking for is a way to easily define common members to be shared by multiple structs they I can see how that could be pretty easily implemented. However the use cases seem quite limited since you wouldn't gain any of the actual benefits of inheritance. If a developer were to treat them as related types through the use of their parent interface they would pretty much lose all of the benefits of being structs due to boxing.

Honestly can't say I've ever run into a situation where this would be useful. It feels like it would encourage developers to define their types as structs instead of classes contrary to Microsoft's own recommendations:

Avoid defining a struct unless it has all of the following characteristics:
  1. It logically represents a single value, similar to primitive types (int, double, etc.).
  2. It has an instance size under 16 bytes.
  3. It is immutable.
  4. It will not have to be boxed frequently.
Sep 27, 2014 at 3:09 AM
Edited Sep 27, 2014 at 3:55 AM
Halo_Four wrote:
Well if all you're looking for is a way to easily define common members to be shared by multiple structs they I can see how that could be pretty easily implemented.
Could you explain how? If you mean rolling a code generator and then including those files in the project, imho that is not what I'd consider easy or robust.
However the use cases seem quite limited since you wouldn't gain any of the actual benefits of inheritance.
You do gain many the benefits of inheritance, as explained:
  • common properties and methods,
  • generic containers.
You don't get polymorphism. But today you don't get any of these. So this does move the language forward.
Note, however, the primary concern is performance and reducing GC costs while keeping code maintainable.
Honestly can't say I've ever run into a situation where this would be useful.
This reduces O(n) heap allocations to O(1) heap allocations - how is that not useful?

About Microsoft's recommendations - I don't see a contradiction. Would you say that having structs in the type system "encourages" people to use them blindly? Similarly, having inheritable structs is not an excuse to use them blindly. But when they are useful, they are extremely useful IMHO, because of the allocations they eliminate. This is also critical on mobile devices. At the very least, the syntax makes it possible to go back and forth between classes and structs with a relatively small effort, so that performance can be measured.

I plan to provide a test case simulating this approach, and to compare its performance with classes.
Sep 27, 2014 at 7:53 AM
  1. It is immutable.
Look at all these immutable structs in the framework. Especially in System.Drawing.
Sep 27, 2014 at 12:10 PM
aspiring wrote:
However, structs turn out to be highly useful in avoiding the mandatory heap allocation that class instantiation causes, particularly on mobile devices.
Though your approach doesn't avoid heap allocations. Still, interesting idea, even though I personally haven't yet required something like this.
Sep 27, 2014 at 12:20 PM
Expandable wrote:
Though your approach doesn't avoid heap allocations.
In the example I've given, the only heap allocation is the List, i.e. order (1) allocations, as mentioned. Are you referring to something else?

Another interesting observation is that if the size of the tree is known before construction, internal List resizing can be eliminated by setting the List's Capacity. Without that the list doubles its capacity whenever it reaches its limit. That's a secondary benefit (and needs to be measured) but still something that a class-based implementation cannot optimize for.
Sep 27, 2014 at 4:23 PM
aspiring wrote:
Expandable wrote:
Though your approach doesn't avoid heap allocations.
In the example I've given, the only heap allocation is the List, i.e. order (1) allocations, as mentioned. Are you referring to something else?
Are you sure? T could be Node so that you can store both Node and Person instances in the tree. In that case, I don't think that the compiler (or rather the runtime) has a choice other than to box the values.
Sep 27, 2014 at 4:47 PM
Edited Sep 27, 2014 at 5:08 PM
Expandable wrote:
Are you sure? T could be Node so that you can store both Node and Person instances in the tree. In that case, I don't think that the compiler (or rather the runtime) has a choice other than to box the values.
No, that's an easy error to make when thinking about C# generics. As I mention in the original post, the current runtime (with no changes) generates code for every new value type that the generic is instantiated for. Hence a List<double> causes all the methods of the generic to be regenerated - I'm talking about code here, not data. See this link for more details. The purpose of this code generation is to avoid boxing and unboxing. Only reference types share a single implementation of a given generic.

This is also what makes this proposal safe - you cannot insert a Node into a Tree<Person> or a Person into a Tree<Node>. Value types continue to be non-polymorphic, even under this proposal. Inheritance is not the same as polymorphism. :)
Sep 27, 2014 at 7:26 PM
aspiring wrote:
Halo_Four wrote:
Well if all you're looking for is a way to easily define common members to be shared by multiple structs they I can see how that could be pretty easily implemented.
Could you explain how? If you mean rolling a code generator and then including those files in the project, imho that is not what I'd consider easy or robust.
I meant that if that was all you expected this feature to accomplish was for the compiler to spit out a base interface and then automatically implement that interface with a default definition in the "inheriting" structs, I imagine that would be relatively easy for the compiler to handle. That is opposed to making inheritance of ValueTypes a proper concept within the runtime itself.
However the use cases seem quite limited since you wouldn't gain any of the actual benefits of inheritance.
You do gain many the benefits of inheritance, as explained:
  • common properties and methods,
  • generic containers.
Both of which you'd have today without pseudo-inheritance. But people using the base struct for a type parameter for a generic container expecting to still avoid the boxing might be in for a nasty surprise. That alone is reason enough to not treat it as struct "inheritance"; that ain't a struct.
You don't get polymorphism. But today you don't get any of these. So this does move the language forward.
Note, however, the primary concern is performance and reducing GC costs while keeping code maintainable.
Honestly can't say I've ever run into a situation where this would be useful.
This reduces O(n) heap allocations to O(1) heap allocations - how is that not useful?

About Microsoft's recommendations - I don't see a contradiction. Would you say that having structs in the type system "encourages" people to use them blindly? Similarly, having inheritable structs is not an excuse to use them blindly. But when they are useful, they are extremely useful IMHO, because of the allocations they eliminate. This is also critical on mobile devices. At the very least, the syntax makes it possible to go back and forth between classes and structs with a relatively small effort, so that performance can be measured.

I plan to provide a test case simulating this approach, and to compare its performance with classes.
You trade one performance implication for another. It is more difficult to use structs today which makes you think about why you are using them. You eliminate heap allocations (if you can avoid boxing) but gain a lot of block copying since those large structs can no longer be copied with simple opcodes.
Sep 27, 2014 at 9:47 PM
@Halo_Four,
I agree with the first point (which seems to agree with the basic proposal.)
You trade one performance implication for another. It is more difficult to use structs today which makes you think about why you are using them. You eliminate heap allocations (if you can avoid boxing) but gain a lot of block copying since those large structs can no longer be copied with simple opcodes.
Do I surmise correctly that you don't think this is a net performance win? I will post some numbers soon; that should provide concrete data.
Sep 28, 2014 at 12:43 AM
Edited Sep 28, 2014 at 12:45 AM
How does the compare with pseudo-inhertitance?
Sep 28, 2014 at 1:22 AM
aspiring wrote:
You trade one performance implication for another. It is more difficult to use structs today which makes you think about why you are using them. You eliminate heap allocations (if you can avoid boxing) but gain a lot of block copying since those large structs can no longer be copied with simple opcodes.
Do I surmise correctly that you don't think this is a net performance win? I will post some numbers soon; that should provide concrete data.
I think that if developers have an easy mechanism through which to extend structs that they'll tend to define wide structs (or inherit an aggregate of fields from parent structs) and if they also pass them around a few times that the cost of copying them will surpass the gains of not allocating them on the heap. I think that when structs are short and sweet that they definitely pose a performance boost.
Sep 29, 2014 at 11:23 AM
aspiring wrote:
Expandable wrote:
Are you sure? T could be Node so that you can store both Node and Person instances in the tree. In that case, I don't think that the compiler (or rather the runtime) has a choice other than to box the values.
No, that's an easy error to make when thinking about C# generics. As I mention in the original post, the current runtime (with no changes) generates code for every new value type that the generic is instantiated for. Hence a List<double> causes all the methods of the generic to be regenerated - I'm talking about code here, not data. See this link for more details. The purpose of this code generation is to avoid boxing and unboxing. Only reference types share a single implementation of a given generic.
I'm well aware of that. However, I was specifically referring to the case that the list is in fact instantiated with Node, in which case you could potentially also pass a Person. However, that's obviously not how you expect this feature to work. In fact, since Node is a struct and you're not proposing any runtime support for struct polymorphism, indeed no Person can be added to a list of Nodes, so no boxing should take place at runtime.
This is also what makes this proposal safe - you cannot insert a Node into a Tree<Person> or a Person into a Tree<Node>. Value types continue to be non-polymorphic, even under this proposal. Inheritance is not the same as polymorphism. :)
OK, I think I'm beginning to understand what you want to achieve. But then why bother with the generated interface at all? Interfaces do allow polymorphism, even for structs (albeit with boxing). I guess that's what confused me at first. I understand now that the interface is only required to specify the generic constraint at the CLR level, but has no use other than that. Specifically, you cannot use it in your C# code at all, because it has an unutterable name.

Interesting, seems to be quite an elegant solution to me. I'm still not sure it's really needed (at least I never did), on the other hand with the recent effort to make structs behave more like classes, this might be a reasonable addition to the language. People might be confused at first that inheritance doesn't give them polymorphism automatically, but that difference should be OK considering that structs are not heap allocated anyway. Though this might be more obvious to C++ programmers than to programmers only familiar with high-level languages such as C# and Java.

Which makes me wonder, how about being able to pass a Person to a ref Node parameter? Shouldn't that be allowed, especially if you follow through with the C++ analogy? I don't see how that would be possible though without changing the CLR or boxing.
Sep 29, 2014 at 12:02 PM
Edited Sep 29, 2014 at 12:25 PM
AdamSpeight2008 wrote:
How does the compare with pseudo-inhertitance?
Thanks, I hadn't seen that. (Oddly, clicking the link takes the browser back to this thread. I had to search "pseudo-inheritance" to find the page you're referring to.)

After reading that discussion, it seems to me to be orthogonal to this one, although there may be ways of unifying the two proposals. I have added a Limitation section to this proposal that specifically excludes it from deriving a struct from a built-in type such as double.
Sep 29, 2014 at 5:53 PM
Halo_Four wrote:
Avoid defining a struct unless it has all of the following characteristics:
  1. It logically represents a single value, similar to primitive types (int, double, etc.).
  2. It has an instance size under 16 bytes.
  3. It is immutable.
  4. It will not have to be boxed frequently.
A structure is a bunch of variables stuck together with duct tape. Sometimes it is useful to make a structure which tries to pretend it's an object, and one should avoid making a structure that pretends it's an object unless it meets the above criteria. On the other hand, sometimes it is useful to have a type which can be used as a bunch of related-but independent variables stuck together with duct tape (e.g. the coordinates of a point). Although class objects can be used to aggregate variables or values, structures are often more convenient and efficient than immutable class objects when manipulating data, and they're more convenient, efficient, and safer than mutable class objects when passing data around. Those who think everything should behave like an "object" may have a natural aversion to anything that doesn't, but if what one needs is a bunch of variables stuck together with duct tape, I would posit that it's better use a bunch of variable stuck together with duct tape (i.e. a structure) as a bunch of variables stuck together with duct tape, than to try to wrap it in such a way as to behave like an object which binds a bunch of variables together with Super Glue® brand adhesive.

There are some unfortunate limitations in the way C# and .NET handle structures. For example, they offer no means to distinguish struct methods and properties which should only be invokable on structures held in writable storage locations (e.g. the Offset method of System.Drawing.Point) from those which should be invokable on any structure-type value. Fixing such problems shouldn't be difficult (e.g. have the compiler squawk at any attempt to invoke on a read-only value a member tagged with a MemberModifiesThis(true) attribute) but some people would rather complain that "mutable structs are evil" than work to improve them.
Sep 29, 2014 at 9:25 PM
supercat wrote:
Halo_Four wrote:
Avoid defining a struct unless it has all of the following characteristics:
  1. It logically represents a single value, similar to primitive types (int, double, etc.).
  2. It has an instance size under 16 bytes.
  3. It is immutable.
  4. It will not have to be boxed frequently.
A structure is a bunch of variables stuck together with duct tape. Sometimes it is useful to make a structure which tries to pretend it's an object, and one should avoid making a structure that pretends it's an object unless it meets the above criteria. On the other hand, sometimes it is useful to have a type which can be used as a bunch of related-but independent variables stuck together with duct tape (e.g. the coordinates of a point). Although class objects can be used to aggregate variables or values, structures are often more convenient and efficient than immutable class objects when manipulating data, and they're more convenient, efficient, and safer than mutable class objects when passing data around. Those who think everything should behave like an "object" may have a natural aversion to anything that doesn't, but if what one needs is a bunch of variables stuck together with duct tape, I would posit that it's better use a bunch of variable stuck together with duct tape (i.e. a structure) as a bunch of variables stuck together with duct tape, than to try to wrap it in such a way as to behave like an object which binds a bunch of variables together with Super Glue® brand adhesive.
I don't deny any of the above. I love structs and make wide but judicious use of them.

The rules outlined by Microsoft are certainly to be considered guidelines and not absolutes. Microsoft breaks them themselves in a myriad of places, as kekekeks pointed out with System.Drawing. Most of those breaks are likely due to the fact that those structs are used directly with GDI+ just under the hood.

The other rules, at least #2 and #4, definitely fall into the category of affecting efficiency. A struct wider than 16-bytes requires multiple native op codes in order to be copied. Larger than 128 bytes and it switches to copying the struct by calling to an internal version of memcpy. These operations are much slower and inherently non-atomic. That performance benefit will quickly evaporate.
Sep 30, 2014 at 4:54 PM
Copying a reference to an immutable object is slightly faster than copying a structure of any significant size. Unless the average reference to an immutable class object would be copied multiple times in its lifetime, however, or a significant fraction of instances will be coerced to types that would require boxing if they were structures, the time spent creating immutable object instances (including amortized GC costs) will outweigh any savings achieved by copying references. If the frequency with which an entity with a foo needs a slightly-modified version is even remotely comparable to the frequency with which instances would need to be copied, then not only will structs significantly outperform classes, but the difference will often be greater with larger structures than with smaller ones [e.g. modifying 4 bytes out of a 256-byte struct requires a single 32-bit write; creating a new immutable class object with 256 bytes of data of which four have been changed requires at minimum allocating a new 264-byte GC object, copying 256 bytes of data from the old one, and then modifying the new one; if not two full orders of magnitude slower, very close to it].

If a structure is being used as an object, the Microsoft rules make sense. In cases where they are being used as aggregations of related but independent variables, I would consider rule #1 to be absolutely wrong (the greater the independence of the components, less the structure will resemble an object and the stronger the argument that it shouldn't be one). Likewise the advantage of a large structure over a comparably-sized immutable class object exceeds that of a smaller structure over a smaller immutable object.

Rather than viewing things like Point to be exceptions to the guideline, I would suggest that a better guideline would be to recognize that structure types should generally cleanly fit one of two distinct usage cases: as lightweight immutable objects, or as aggregations of variables. The MS guidelines are appropriate for the former usage case, but not the latter.
Oct 14, 2014 at 5:04 PM
I think the Node/Person example could have been solved with generics. Something like:
interface INodeData
{
    int Id { get; set; }
}

// could use a better name
struct NoData : INodeData
{
    int Id { get; set; }
}

struct Person : INodeData
{
    int Id { get; set; }
    string Name { get; set; }
}

struct Node<T> where T : INodeData
{
    T Data { get; set; }
    int Index { get; set; } // the index within NodeTree.Items of this Node.
    int ParentIndex { get; set; }
    int LeftChildIndex { get; set; }
    int RightChildIndex { get; set; }
    void SetLeft(ref Node<T> child) { ... } // sets the left child
    void SetRight(ref Node<T> child) { ... } // sets the left child
}
This way, it compiles with the current compiler and it's using only structs, so no unnecessary heap allocations.
Oct 14, 2014 at 7:55 PM
Interface-constrained generic structures are very powerful, but there is as yet no way to indicate that certain struct variables should not be implicitly copied, or that mutating members should not be invoked upon readonly copies of structures. Further, properties don't really work like variables. One could define:
delegate ActByRef<T1,T2>(ref T1 p1, ref T2 p2);
interface INodeData
{
    int Id { get; set; }
    int actOnId<TP>(ActByRef<int,TP> actor, ref TP extraParam);
    int compareExchangeIdToFrom(int newId, int oldId);
}
struct Person : INodeData
{
    int _id;
    int Id { get {return _id} set {_id = value;} }
    string Name { get; set; }

    int actOnId<TP>(ActByRef<int,TP> actor, ref TP extraParam);
    { actor.Act(ref _id, ref extraParam); }

    int compareExchangeIdToFrom(int newId, int oldId)
    { return System.Threading.Interlocked.CompareExchange(ref _id, newId, oldId); }
}
but that's a bit icky for both caller and implementation.