11

This one's really an offshoot of this question, but I think it deserves its own answer.

According to section 15.13 of the ECMA-334 (on the using statement, below referred to as resource-acquisition):

Local variables declared in a resource-acquisition are read-only, and shall include an initializer. A compile-time error occurs if the embedded statement attempts to modify these local variables (via assignment or the ++ and -- operators) or pass them as ref or out parameters.

This seems to explain why the code below is illegal.

struct Mutable : IDisposable
{
    public int Field;
    public void SetField(int value) { Field = value; }
    public void Dispose() { }
}

using (var m = new Mutable())
{
    // This results in a compiler error.
    m.Field = 10;
}

But what about this?

using (var e = new Mutable())
{
    // This is doing exactly the same thing, but it compiles and runs just fine.
    e.SetField(10);
}

Is the above snippet undefined and/or illegal in C#? If it's legal, what is the relationship between this code and the excerpt from the spec above? If it's illegal, why does it work? Is there some subtle loophole that permits it, or is the fact that it works attributable only to mere luck (so that one shouldn't ever rely on the functionality of such seemingly harmless-looking code)?

Community
  • 1
  • 1
Dan Tao
  • 125,917
  • 54
  • 300
  • 447
  • Is calling a method using assignment? Is it using the `++` or `--` operators? Is it passing it as a `ref` or `out` parameter? – Anon. Jan 11 '11 at 21:09
  • @Anon: That's kind of my question. Calling a method on a value type which modifies that value's state is essentially not really any different from assignment, right? Which is why modifying a field is stricly disallowed? – Dan Tao Jan 11 '11 at 21:12
  • Where do you come up with this stuff? Notable is that a property doesn't work either. Which is just a method call under the hood, preventing Remoting from being a reason. Far fetched anyway. This quacks like a bug. Well, flaw. Mentioning Eric Lippert's name usually gets him to pay a visit. Done. – Hans Passant Jan 11 '11 at 22:25
  • As mentioned in my answer below: using has got nothing to do with it. The only effect it introduces is that it makes the var readonly. And it is not allowed to change the value of a read-only value type via an assignment operator, which seems to include not only the var itself but also all members. Function calls are allowed on readonly values and a function can change the value of a member of a struct. Behavior is different with an object type. – Mario The Spoon Jan 11 '11 at 22:28
  • 1
    As is evident from some of the answers, only a ***copy*** of the read-only variable `e` is modified. This is well-specified and fully defined behavior. Whenever a struct variable is considered read-only, every time an instance method is called on this variable, a copy is first made and the method is then called on the copy. So if the method turns out to mutate the struct, only the copy is affected, and the copy is not kept. Source: [Changing a struct inside another struct in a foreach loop](http://stackoverflow.com/questions/1688205/changing-a-struct-inside-another-struct-in-a-foreach-loop) – Jeppe Stig Nielsen Oct 16 '13 at 21:52

4 Answers4

3

I would read the standard in such a way that

using( var m = new Mutable() )
{
   m = new Mutable();
}

is forbidden - with reason that seem obious. Why for the struct Mutable it is not allowed beats me. Because for a class the code is legal and compiles fine...(object type i know..)

Also I do not see a reason why changing the contents of the value type does endanger the RA. Someone care to explain?

Maybe someone doing the syntx checking just misread the standard ;-)

Mario

Mario The Spoon
  • 4,799
  • 1
  • 24
  • 36
  • 2
    +1 that is how I read it. It does not state they are "immutable" just the local variable itself is "read-only". However, I can't comment as to why the struct version fails. – user7116 Jan 11 '11 at 21:58
  • 1
    The reason is that it is a value type. Calling an assignment operator on a readonly value type is not allowed. The error message issued by the compiler is just misleading. This means that on a readonly struct your are not allowed to call the assigment operator of a member. – Mario The Spoon Jan 11 '11 at 22:18
  • I was referring to this style of assignment `myStruct.Field = ...` rather than `myStruct = ...`. I'm as lost as you are as to why the former is disallowed. – user7116 Jan 11 '11 at 22:32
  • It seems that c# also sees the members of a struct as to be the same value type as the struct instance itself. so for c# myStruct.field = and myStruct = are the same (being an assignment operator on the using var). I can't express it more clearly. But since the a value type is always 'seen' as a total (allocated in one bunch on the stack) any manipulation also on the members seem to obey the same rules as a manipulation on the struct instance itself (I could not track this down int the standard) – Mario The Spoon Jan 11 '11 at 22:43
  • The same appearas to be the case when you declare a readonly struct and try to assign to a member. private readonly Mutable xxx = new Mutable(); { xxx.Field = 10; } is also not allowed by the compiler. – Mario The Spoon Jan 11 '11 at 22:45
  • 1
    But to answer your original question: Your code calling the function is completely legal and should not have any bad side-effects! – Mario The Spoon Jan 11 '11 at 22:48
2

This behavior is undefined. In The C# Programming language at the end of the C# 4.0 spec section 7.6.4 (Member Access) Peter Sestoft states:

The two bulleted points stating "if the field is readonly...then the result is a value" have a slightly surprising effect when the field has a struct type, and that struct type has a mutable field (not a recommended combination--see other annotations on this point).

He provides an example. I created my own example which displays more detail below.

Then, he goes on to say:

Somewhat strangely, if instead s were a local variable of struct type declared in a using statement, which also has the effect of making s immutable, then s.SetX() updates s.x as expected.

Here we see one of the authors acknowledge that this behavior is inconsistent. Per section 7.6.4, readonly fields are treated as values and do not change (copies change). Because section 8.13 tells us using statements treat resources as read-only:

the resource variable is read-only in the embedded statement,

resources in using statements should behave like readonly fields. Per the rules of 7.6.4 we should be dealing with a value not a variable. But surprisingly, the original value of the resource does change as demonstrated in this example:

    //Sections relate to C# 4.0 spec
    class Test
    {
        readonly S readonlyS = new S();

        static void Main()
        {
            Test test = new Test();
            test.readonlyS.SetX();//valid we are incrementing the value of a copy of readonlyS.  This is per the rules defined in 7.6.4
            Console.WriteLine(test.readonlyS.x);//outputs 0 because readonlyS is a value not a variable
            //test.readonlyS.x = 0;//invalid

            using (S s = new S())
            {
                s.SetX();//valid, changes the original value.  
                Console.WriteLine(s.x);//Surprisingly...outputs 2.  Although S is supposed to be a readonly field...the behavior diverges.
                //s.x = 0;//invalid
            }
        }

    }

    struct S : IDisposable
    {
        public int x;

        public void SetX()
        {
            x = 2;
        }

        public void Dispose()
        {

        }
    }    

The situation is bizarre. Bottom line, avoid creating readonly mutable fields.

P.Brian.Mackey
  • 43,228
  • 68
  • 238
  • 348
  • Upvoted. When a struct variable is considered in read-only "context", instance methods on that variable are not called directly, they are called on a "safety copy", like I just said in a comment to the question. – Jeppe Stig Nielsen Oct 16 '13 at 21:55
  • I wonder if the "bug" where the variable in `using` is not read-only enough has to with the fact that `(IDisposable)s` boxes `s`. When we `foreach` through a `List<>`, then formally we have something like `using (var e = theList.GetEnumerator()) { ... }` where `e` is a struct that *must* be mutated to make the `foreach` progress. [Method `MoveNext()`](http://msdn.microsoft.com/en-us/library/a3207y01.aspx) mutates a struct? I will research this more when I have more time. – Jeppe Stig Nielsen Oct 16 '13 at 22:09
  • The original `s` - inside `using()` is **not** mutated. The `s.SetX()` line mutates an invisible copy. Also, there will be no boxing when compiled with Roslyn thanks to a compiler hack. Read Lippert's [blog](https://stackoverflow.com/questions/1330571/when-does-a-using-statement-box-its-argument-when-its-a-struct/1330724#1330724) for more details. – l33t Apr 10 '19 at 11:28
2

I suspect the reason it compiles and runs is that SetField(int) is a function call, not an assignment or ref or out parameter call. The compiler has no way of knowing (in general) whether SetField(int) is going to mutate the variable or not.

This appears completely legal according to the spec.

And consider the alternatives. Static analysis to determine whether a given function call is going to mutate a value is clearly cost prohibitive in the C# compiler. The spec is designed to avoid that situation in all cases.

The other alternative would be for C# to not allow any method calls on value type variables declared in a using statement. That might not be a bad idea, since implementing IDisposable on a struct is just asking for trouble anyway. But when the C# language was first developed, I think they had high hopes for using structs in lots of interesting ways (as the GetEnumerator() example that you originally used demonstrates).

Jeffrey L Whitledge
  • 58,241
  • 9
  • 71
  • 99
  • 2
    This sounds reasonable and I'm inclined to agree with you. But the question still remains whether this behavior is in fact defined or not. – Dan Tao Jan 11 '11 at 21:14
  • 2
    The value is not changed by means of a `++` or `--` or `ref` or `out`, so I'd say it's defined to be allowed. Bottom line: don't declare mutable structs. They are confusing. – dtb Jan 11 '11 at 21:18
  • 1
    @dtb: I can see your reasoning; what I'm unsure of is whether the mention of those specific operators and `ref` and `out` were included in the wording of the spec as merely *examples* of the fact that local variables should be read-only, or as *the only cases* that are disallowed. Either way it seems a bit nebulous to me. And while I agree with you in general about not using mutable structs, they *do* exist in the BCL (consider my now-edited-out example of wrapping a `List.Enumerator` in a `using`). – Dan Tao Jan 11 '11 at 21:31
  • Jeffrey, I'm not necessarily trying to suggest that one or the other behavior ought to be enforcible by the compiler. I realize that analyzing method calls to determine whether they modify value types would likely be overkill. But it's still unclear to me whether it is actually *defined* what will happen. I could certainly imagine the argument being made that it cannot be disallowed by the compiler but it should still not be done since it leads to undefined behavior. – Dan Tao Jan 11 '11 at 21:36
  • @Dan Tao - The behavior of the using variable appears to be identical to the behavior of other read-only fields (declared with the `readonly` modifier). The spec itself doesn't go into an enormous amount of detail about read-only fields, but I don't think it could be called "undefined". The cases that are not allowed are very clearly spelled out, and the cases that are not forbidden behave just like any other (non-read-only) variable. – Jeffrey L Whitledge Jan 11 '11 at 21:55
  • @Jeffrey: That is a helpful way of looking at it; thanks. The truth though is that I *want* for this behavior to be undefined, because it's the only way I can understand [the odd behavior I described in a previous question](http://stackoverflow.com/questions/4642665), which seems a lot like a compiler bug if this is supposed to be allowed (though maybe there's a perfectly reasonable explanation, of course). – Dan Tao Jan 11 '11 at 22:06
  • @Jeffrey: The behaviour of the the `using` variable is quite different from a `readonly` field. The `using` variable is still a variable, but the compiler enforces read-only semantics on that variable inside the using block; you can quite legally mutate by calling a `SetValue` method or whatever as Dan has shown... – LukeH Jan 12 '11 at 00:53
  • 2
    ...By contrast, a `readonly` field is only treated as a variable inside a type's constructor/initialiser; anywhere else it is treated as a plain value. If you called a `SetValue` method on a struct held in a `readonly` field (and you're not inside the outer type's constructor/initialiser) then you're mutating *a copy of the value of that field*. The field itself remains unchanged. – LukeH Jan 12 '11 at 01:00
  • @LukeH: the italicized bit is what I think we're all missing. Certainly ties it up nicely for me. – user7116 Jan 12 '11 at 03:16
  • @LukeH why is this so? Actually this seems pretty dangerous to me, since calling the SetValue function is allowed, but does not what it is supposed to do - without anybody noticing! Also, SetValue does not work as epxected for a readonly struct, but works for a struct used as using variable! – Mario The Spoon Jan 12 '11 at 05:22
  • @Mario: That's why using mutable structs is almost always a bad idea. They behave counter-intuitively, although that counter-intuitive behaviour is (usually? always?) consistent with what's described in the spec. – LukeH Jan 12 '11 at 10:22
  • I suspect that the read-only restriction on `using` variables is only there to stop you shooting yourself in the foot by re-assigning the variable inside the `using` block (doing so would mean that the newly assigned object/struct would be disposed at the end of the block, rather than the original). A knock-on effect of this is that you're also blocked from assigning to the fields of a mutable struct, since modifying the fields *is* modifying the variable. (Note that this is pure speculation on my part, but seems reasonably plausible.) – LukeH Jan 12 '11 at 10:45
  • @LukeH: I agree with that—kind of the same reasoning why you can't reassign the local variable in a `foreach` loop (to prevent you from doing something stupid). Now that I'm thinking about it, I *suspect* you've nailed down the explanation (in your previous comment) for [the weird behavior I keep talking about](http://stackoverflow.com/questions/4642665): probably what's happening is that when a closure's added inside a `using`, the compiler generates a class in which the local variable is declared `readonly`; thus modifications to it only affect a copy, just as you explained. – Dan Tao Jan 12 '11 at 18:19
  • @Dan: That sort-of-does/sort-of-doesn't describe what's actually happening: The field that represents the captured local in the compiler-generated class *is not* marked `readonly` but, interestingly, the IL that accesses the field within the `using` block is *exactly the same* as would be generated if the field was `readonly` (ie, copy the field into a local and mutate the local rather than mutating the field itself). – LukeH Jan 13 '11 at 01:45
  • I know this is an old question, but: *"Static analysis to determine whether a given function call is going to mutate a value is clearly cost prohibitive in the C# compiler."* It's not just cost-prohibitive, it's impossible. See the halting problem. – cdhowie Aug 28 '11 at 08:21
  • A solution which should have been (and still should be) easy would have been for MS to define an attribute which would specify whether a struct method or property would modify `this`, and for compilers to reject any attempt to invoke a on a read-only value a method which was tagged as modifying `this`, while permitting the invocation of those which were not so tagged. When implementing something like a `ThreadSafeBitVector`, it would be nicer to say `MyVector.ClearBits(3,10);` than `ThreadSafeBitVector.ClearBits(ref MyVector,3,10);`, but the compiler would allow the former when it shouldn't. – supercat Feb 21 '13 at 16:03
2

To sum it up

struct Mutable : IDisposable
{
    public int Field;
    public void SetField( int value ) { Field = value; }
    public void Dispose() { }
}


class Program

{
    protected static readonly Mutable xxx = new Mutable();

    static void Main( string[] args )
    {
        //not allowed by compiler
        //xxx.Field = 10;

        xxx.SetField( 10 );

        //prints out 0 !!!! <--- I do think that this is pretty bad
        System.Console.Out.WriteLine( xxx.Field );

        using ( var m = new Mutable() )
        {
            // This results in a compiler error.
            //m.Field = 10;
            m.SetField( 10 );

            //This prints out 10 !!!
            System.Console.Out.WriteLine( m.Field );
        }



        System.Console.In.ReadLine();
    }

So in contrast to what I wrote above, I would recommend to NOT use a function to modify a struct within a using block. This seems wo work, but may stop to work in the future.

Mario

Mario The Spoon
  • 4,799
  • 1
  • 24
  • 36