views:

91

answers:

2

The following code prints (when invoking MyMethod):

0
0
0
1

I would expect it to print:

0
0
1
1

Why is this?

Code:

private struct MyStruct
{
    public MyInnerStruct innerStruct;
}

private struct MyInnerStruct
{
    public int counter;

    public void AddOne()
    {
        ++counter;
    }
}

public static void MyMethod()
{
    MyStruct[] myStructs = new MyStruct[] { new MyStruct() };

    foreach (var myStruct in myStructs)
    {
        MyStruct myStructCopy = myStruct;

        Console.WriteLine(myStruct.innerStruct.counter);
        Console.WriteLine(myStructCopy.innerStruct.counter);

        myStruct.innerStruct.AddOne();
        myStructCopy.innerStruct.AddOne();

        Console.WriteLine(myStruct.innerStruct.counter);
        Console.WriteLine(myStructCopy.innerStruct.counter);
    }
}
A: 

I think you'll find that the MyStruct.innerStruct actually returns a copy of the struct, and not the struct itself. Therefore you're increasing the value of the copy...

Sure I read a blog about this recently somewhere.

If you change the following

myStruct.innerStruct.AddOne();
Console.WriteLine(myStruct.innerStruct.counter);

to

MyInnerStruct inner = myStruct.innerStruct;
inner.AddOne();
Console.WriteLine(inner.counter);

Then you should see it start working.

Ian
But why does the myStructCopy get its counter increased?My best guess is that is has something to do with myStruct being readonly.
TOS
+3  A: 

The reason you are seeing this behavior has to due with using an iteration variable. Iteration variables are read-only in the sense that in C# you cannot modify them (C# lang spec section 8.8.4 details this

The iteration variable corresponds to a read-only local variable with a scope that extends over the embedded statement

Playing with read-only mutable structs is a path to unexpected behavior. Instead of using the variable directly you are actually using a copy of the variable. Hence it's the copy that is getting incremented in the case of myStruct and not the actual value. This is why the original value remains unchanged.

Eric did a rather in depth article on this topic that you can access here

Yet another reason why you should always have immutable structs.

JaredPar
Nice, that was the blog I was looking for :)
Ian
Doesn't this explanation raise another question? If myStruct is readonly (which I'd expect it to be) then surely myStruct.InnerCopy.AddOne() should fail by throwing an exception. If you can call a non-const method (as we'd call it in the C++ world) on a const / read-only object and have it silently do nothing rather than complain, there's surely something very wrong...
AAT
AAT, what happens here is that the mutating method is called on a mutable *copy*. Value types are COPIED BY VALUE, that's why they're called "value" types. If you don't want stuff to be copied by value, then don't use value types.
Eric Lippert
@AAT C#'s read-only and the C++ concept of const are **very** different constructs. In C++ it's an attempt to ensure a single value is not unexpectedly mutated. In C# read-only when applied to value types causes a copy of the value to be created whenever it is used (or at least when a member is accessed) thus preventing mutation of the original value. I'm hesitant to say much further because I don't deeply understand all of the circumstances under which C# ensures of value of the struct is returned. Hopefully Eric will be along in a bit to give a more thorough answer.
JaredPar
Indeed, Jared, your explanation is correct. Unfortunately, we do not *strictly* ensure that so-called "read only variables" like the "foreach" and "using" variables eare treated *exactly* the same way that we treat readonly fields; it is *possible* to abuse certain compiler behaviours (involving closure rewriting) to induce mutations in such variables of mutable value types. I consider these behaviours to be compiler bugs, but the scenarios are obscure and the spec is imprecise. Hopefully we'll fix those up in a hypothetical future release.
Eric Lippert
@JaredPar - thanks for the explanation, that's just another one to put on my list of C# gotchas...
AAT
@AAT: Do you actually have such a list? If so, I would love to see it. We are always looking for ways to eliminate or mitigate "gotcha" scenarios.
Eric Lippert