The two best choices for a data transfer object should generally be either a deeply-immutable class object, or a struct with exposed fields of types suitable for use as data transfer objects. Other types of structs may sometimes be usable as well, but exposed-field structs are by far the simplest, and that simplicity is a great virtue.
The notion that mutable structs are evil dates back to some early C# compilers which in which
SomeReadonlyStruct.SomeProperty = 5;
would be silently transformed by the compiler into:
var temp = SomeReadonlyStruct;
temp.SomeProperty = 5;
Hiding struct fields behind readonly properties was an attempt to ensure the former statement would refuse to compile instead of yielding broken code. Since newer compilers will refuse to mutate writable fields of readonly structs, there's no longer any need to wrap fields in read-only properties.
Structs with exposed fields have a huge advantage compared with other types of data transfer objects: every struct which has exposed fields of types suitable for data-transfer and no other members else except perhaps a constructor behaves the same way, with no surprises. Someone who has never used structs might be a little surprised by the fact that they don't act like classes, but someone who understands how any such struct works will understand how they all work.
Consider the following code:
customerPhone = someDataSource.GetPhoneNumber(customerID);
customerPhone.Extention = "5309"
Some people dislike the fact that if customerPhone
is an exposed-field struct, setting the Extension
property won't affect the information in someDataSource
. While it's certainly true that writing a struct field won't update anything else, that's a much better situation than would exist if customerPhone
were a mutable class type. Anyone who understands that customerPhone
is an exposed-field struct type will know that changes to its members won't affect anything else. By contrast, if customerPhone
is a mutable class type, code like the above might update someDataSource
by changing the phone number associated with that customerID
. Or it might not. Or, if one phone number was associated with two customerID
values, the above code might change both of them. The amount of code one may have to study to determine exactly what effects and side effects the above code might have would be quite large. Worse, it may be difficult to be sure that one hasn't missed anything.
There are definitely some places where passing around class object references can be more efficient than passing around structs. There are also a few cases where mutable class objects can be useful data holders within a class. Some generic wrappers, however, can faclitate wrapping a struct in a mutable or immutable class, and exchanging the information between those types of classes.
interface IReadableHolder<T> { T Value {get;} }
class MutableHolder<T> : IReadableHolder<T>
{
public T Value;
IReadableHolder.Value {get {return Value;} }
public MutableHolder(T newValue) { Value = newValue; }
public MutableHolder(IReadableHolder<T> it) { Value = it.Value; }
}
class ImmutableHolder<T> : IReadableHolder<T>
{
T _Value;
public Value {get {return _Value;} }
public ImmutableHolder(T newValue) { _Value = newValue; }
}
Such a construct would be much more awkward without mutable structs.