6

Suppose I have a struct type implementing IDisposible, and if I use the codes below:

using (MyStruct ms = new MyStruct())
{
     InnerAction(ms);   //Notice "InnerAction" is "InnerAction(MyStruct ms)"
}

Of course I see after the block of using, the ms is disposed. However what about the struct in "InnerAction"? Is it still alive because of deep copy or it is also disposed?

If it's still alive (not disposed), Must I use "ref" for "InnerAction"?

Please give me your proof:)

Thx all.

xqMogvKW
  • 628
  • 7
  • 17
  • 1
    There is no "deep copy" and therefore nothing left to dispose. – Henrik Jul 24 '15 at 07:47
  • 11
    `IDisposable` struct types are [a bad idea](http://ericlippert.com/2011/03/14/to-box-or-not-to-box/)... – xanatos Jul 24 '15 at 07:49
  • 1
    Mutable structs are [a bad idea](http://stackoverflow.com/questions/441309/why-are-mutable-structs-evil) To implement disposable suggests must have state, thus must be mutable. – weston Jul 24 '15 at 07:54

3 Answers3

4

It's worse than you think: ms is not even disposed.

The reason is that the using statement makes an internal copy which it calls dispose on in a try/finally construct.

Consider this LinqPad example:

void Main()
{
    MyStruct ms;
    using (ms = new MyStruct())
    {
        InnerAction(ms);
    }

    ms.IsDisposed.Dump();
    _naughtyCachedStruct.IsDisposed.Dump();
}

MyStruct _naughtyCachedStruct;

void InnerAction(MyStruct s)
{
    _naughtyCachedStruct = s;
}

struct MyStruct : IDisposable
{
    public Boolean IsDisposed { get; set; }

    public void Dispose()
    {
        IsDisposed = true;
    }
}

Here's some of the decompiled IL:

IL_0000:  nop         
IL_0001:  ldloca.s    01 // CS$0$0000
IL_0003:  initobj     UserQuery.MyStruct
IL_0009:  ldloc.1     // CS$0$0000
IL_000A:  dup         
IL_000B:  stloc.0     // ms
IL_000C:  dup         
IL_000D:  stloc.0     // ms
IL_000E:  stloc.2     // CS$3$0001
IL_000F:  nop         
IL_0010:  ldarg.0     
IL_0011:  ldloc.0     // ms

Notice that in IL_000E a compiler generated local (CS$3$0001) is created and a copy of ms is stored there. Later...

IL_001B:  ldloca.s    02 // CS$3$0001
IL_001D:  constrained. UserQuery.MyStruct
IL_0023:  callvirt    System.IDisposable.Dispose
IL_0028:  nop         
IL_0029:  endfinally  

Dispose is called against this local, not ms (which is stored in location 0).

The result is that both ms and the copy that InnerAction holds onto are both not disposed.

Conclusion: don't use structs in using statements.

EDIT: as @Weston points out in the comments, you can manually box the struct and act on the boxed instance, since it then lives on the heap. This way you can get the instance to dispose, but if you had cast it back to the struct in the using statement, you'll only end up storing a copy before the instance was disposed. Further, boxing removes the benefit of staying off the heap, which you are presumably up to here.

MyStruct ms = new MyStruct();
var disposable = (IDisposable)ms;
using (disposable)
{
    InnerAction(disposable);
}

((MyStruct)disposable).IsDisposed.Dump();
codekaizen
  • 26,990
  • 7
  • 84
  • 140
  • This is what I came up with, but then I saw this: http://stackoverflow.com/a/1330596/360211 It says "value types are not boxed in using"... – weston Jul 24 '15 at 07:57
  • They are not boxed in a way that the `using` refers to an object on the heap. The IL above shows this. – codekaizen Jul 24 '15 at 07:58
  • Oh yeah, I guess boxing might save the situation as *would* refer to same instance. – weston Jul 24 '15 at 08:01
  • I just checked: it does "save" it insofar as the `using` acts on the right instance of the struct (the boxed struct on the heap): http://share.linqpad.net/tihvmj.linq. – codekaizen Jul 24 '15 at 08:08
  • In last example, did you look at `ms.IsDisposed.Dump();`? it's false, so the boxed version isn't the original – weston Jul 24 '15 at 08:44
  • 1
    True; it will be because it is not boxed on the heap; only the boxed instance is `true`. – codekaizen Jul 24 '15 at 09:06
2

The behavior of your code depends on the internal implementation of MyStruct.

Consider the following implementation:

struct MyStruct : IDisposable
{
    private A m_A = new A();
    private B m_B = new B();

    public void Dispose()
    {
        m_A.Dispose();
        m_B.Dispose();
    }
}

class A : IDisposable
{
    private bool m_IsDisposed;
    public void Dispose()
    {
        if (m_IsDisposed)
            throw new ObjectDisposedException();
        m_IsDisposed = true;
    }
}

class B : IDisposable
{
    private bool m_IsDisposed;
    public void Dispose()
    {
        if (m_IsDisposed)
            throw new ObjectDisposedException();
        m_IsDisposed = true;
    }
}

In the above code, the MyStruct implementation only delegates the Dispose call to other reference types. In that case, the instance in your example may be considered as "Disposed" after the using block is over. Similar behavior may be achieved by saving an internal reference to a boolean member indicates if the class is disposed.

However in the examples in @codekaizen's answer and in @xanatos's comment, the behavior is that only a copy is disposed, as indicated there.

The bottom line is that you have the ability to make your struct to behave correctly with the Disposed pattern, but I would avoid that because it is very bug-prone.

MaMazav
  • 1,773
  • 3
  • 19
  • 33
  • 1
    This is a good insight, and essentially the pattern used by `System.Threading.CancellationToken` where it is used to provide safe access to shared state. – codekaizen Jul 24 '15 at 09:05
0

I think it's unfortunate that the implementers of C# decided that employing using with a structure should cause all methods on that structure (including Dispose) to receive copies of it, since such behavior leads to slower code than would operating on the original, precludes what would otherwise be some useful semantics, and does not in any circumstance I can identify lead to what would otherwise be broken code working correctly. Nonetheless, the behavior is what it is.

Consequently, I would suggest that no structure should implement IDisposable in any fashion which is expected to modify the structure itself. The only structure types which implement IDisposable should fit one or both of the following patterns:

  1. The structure serves to encapsulate an immutable reference to an object, and the structure behaves as though that object's state as its own. I can't think of where I've seen this pattern used to encapsulate objects requiring disposal, but it would seem possible.

  2. The structure's type implements an interface which inherits IDisposable, and some of whose implementations require cleanup. If the structure does not itself require cleanup and its disposal method does nothing, the fact that the disposal method is invoked on a copy would have no consequence beyond the fact that the system will waste time making a useless copy of the structure before invoking a do-nothing method on it.

Note that the behavior of C#'s using statement causes trouble not only when it comes to Dispose, but also when it comes to the invocation of other methods. Consider:

void showListContents1(List<string> l)
{
  var en = l.GetEnumerator();
  try
  {
    while(en.MoveNext())
      Console.WriteLine("{0}", en.Current);
  }
  finally
  {
    en.Dispose();
  }
}

void showListContents(List<string> l)
{
  using(var en = l.GetEnumerator())
  {
    while(en.MoveNext())
      Console.WriteLine("{0}", en.Current);
  }
}

While the two methods lay look equivalent, the first will work and the second won't. In the first method, each call to MoveNext will act upon variable en, and will thus advance the enumerator. In the second, each call to MoveNext will act upon a different copy of en; none of them will ever advance the enumerator en. The fact that the Dispose call in the second case is invoked upon a copy of en wouldn't be a problem, since that copy does nothing. Unfortunately, the way C# handles struct-type using arguments breaks the code within the using statement as well.

supercat
  • 77,689
  • 9
  • 166
  • 211