C#: Why do mutations on readonly structs not break

2019-04-19 18:21发布

In C#, if you have a struct like so:

struct Counter
{
    private int _count;

    public int Value
    {
        get { return _count; }
    }

    public int Increment()
    {
        return ++_count;
    }
}

And you have a program like so:

static readonly Counter counter = new Counter();

static void Main()
{
    // print the new value from the increment function
    Console.WriteLine(counter.Increment());
    // print off the value stored in the item
    Console.WriteLine(counter.Value);
}

The output of the program will be:

1
0

This seems completely wrong. I would either expect the output to be two 1s (as it is if Counter is a class or if struct Counter : ICounter and counter is an ICounter) or be a compilation error. I realize that detecting this at compilation time is a rather difficult matter, but this behavior seems to violate logic.

Is there a reason for this behavior beyond implementation difficulty?

标签: c# readonly
2条回答
小情绪 Triste *
2楼-- · 2019-04-19 18:39

structs are value types and therefore have a value type sematics. This means each time you access the struct you basically work with a copy of the struct's value.

In your sample you don't change the original struct but only a temporary copy of it.

See here for further explanations:

Why are mutable structs evil

查看更多
We Are One
3楼-- · 2019-04-19 18:43

In .net, a struct instance method is semantically equivalent to a static struct method with a an extra ref parameter of the struct type. Thus, given the declarations:

struct Blah { 
   public int value;
   public void Add(int Amount) { value += Amount; }
   public static void Add(ref Blah it; int Amount; it.value += Amount;}
}

The method calls:

someBlah.Add(5);
Blah.Add(ref someBlah, 5);

are semantically equivalent, except for one difference: the latter call will only be permitted if someBlah is a mutable storage location (variable, field, etc.) and not if it is a read-only storage location, or a temporary value (result of reading a property, etc.).

This faced the designers of .net languages with a problem: disallowing the use of any member functions on read-only structs would be annoying, but they didn't want to allow member functions to write to read-only variables. They decided to "punt", and make it so that calling an instance method on a read-only structure will make a copy of the structure, invoke the function on that, and then discard it. This has the effect of slowing down calls to instance methods which do not write the underlying struct, and making it so that an attempt to use a method which updates the underlying struct on a read-only struct will yield different broken semantics from what would be achieved if it were passed the struct directly. Note that the extra time taken by the copy will almost never yield correct semantics in cases which would not have been correct without the copy.

One of my major peeves in .net is that there is still (as of at least 4.0, and probably 4.5) still no attribute via which a struct member function can indicate whether it modifies this. People rail about how structs should be immutable, rather than providing the tools to allow structs to safely offer mutating methods. This, despite the fact that so-called "immutable" structs are a lie. All non-trivial value types in mutable storage locations are mutable, as are all boxed value types. Making a struct "immutable" may compel one to rewrite a whole struct when one only wants to change one field, but since struct1 = struct2 mutates struct1 by copying all the public and private fields from struct2, and there's nothing the type definition for the struct can do to prevent that (except not have any fields) it does nothing to prevent unexpected mutation of struct members. Further, because of threading issues, structs are very limited in their ability to enforce any sort of invariant relationship among their fields. IMHO, it would generally be better for a struct with to allow arbitrary field access, making clear that any code receiving a struct must check whether its fields meet all required conditions, than try to prevent the formation of structs which don't meet conditions.

查看更多
登录 后发表回答