A struct is, at its heart, nothing more nor less than an aggregation of fields. In .NET it's possible for a structure to "pretend" to be an object, and for each structure type .NET implicitly defines a heap object type with the same fields and methods which--being a heap object--will behave like an object. A variable which holds a reference to such a heap object ("boxed" structure) will exhibit reference semantics, but one which holds a struct directly is simply an aggregation of variables.
I think much of the struct-versus-class confusion stems from the fact that structures have two very different usage cases, which should have very different design guidelines, but the MS guidelines don't distinguish between them. Sometimes there is a need for something which behaves like an object; in that case, the MS guidelines are pretty reasonable, though the "16 byte limit" should probably be more like 24-32. Sometimes, however, what's needed is an aggregation of variables. A struct used for that purpose should simply consist of a bunch of public fields, and possibly an Equals
override, ToString
override, and IEquatable(itsType).Equals
implementation. Structures which are used as aggregations of fields are not objects, and shouldn't pretend to be. From the structure's point of view, the meaning of field should be nothing more or less than "the last thing written to this field". Any additional meaning should be determined by the client code.
For example, if a variable-aggregating struct has members Minimum
and Maximum
, the struct itself should make no promise that Minimum <= Maximum
. Code which receives such a structure as a parameter should behave as though it were passed separate Minimum
and Maximum
values. A requirement that Minimum
be no greater than Maximum
should be regarded like a requirement that a Minimum
parameter be no greater than a separately-passed Maximum
one.
A useful pattern to consider sometimes is to have an ExposedHolder<T>
class defined something like:
class ExposedHolder<T>
{
public T Value;
ExposedHolder() { }
ExposedHolder(T val) { Value = T; }
}
If one has a List<ExposedHolder<someStruct>>
, where someStruct
is a variable-aggregating struct, one may do things like myList[3].Value.someField += 7;
, but giving myList[3].Value
to other code will give it the contents of Value
rather than giving it a means of altering it. By contrast, if one used a List<someStruct>
, it would be necessary to use var temp=myList[3]; temp.someField += 7; myList[3] = temp;
. If one used a mutable class type, exposing the contents of myList[3]
to outside code would require copying all the fields to some other object. If one used an immutable class type, or an "object-style" struct, it would be necessary to construct a new instance which was like myList[3]
except for someField
which was different, and then store that new instance into the list.
One additional note: If you are storing a large number of similar things, it may be good to store them in possibly-nested arrays of structures, preferably trying to keep the size of each array between 1K and 64K or so. Arrays of structures are special, in that indexing one will yield a direct reference to a structure within, so one can say "a[12].x = 5;". Although one can define array-like objects, C# does not allow for them to share such syntax with arrays.