结构类型的深层副本是否也在“使用......”块中处理?

时间:2015-07-24 07:42:56

标签: c# struct using idisposable

假设我有一个实现IDisposible的结构类型,如果我使用下面的代码:

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

当然我看到在使用块之后,ms被处理掉了。然而," InnerAction&#34 ;?中的结构呢?它是否仍然存在,因为深层复制或它也被处置?

如果它还活着(未处理),我必须使用"参考" for" InnerAction"?

请告诉我你的证明:)

所有。

3 个答案:

答案 0 :(得分:4)

比你想象的还要糟糕:ms甚至没有被处理掉。

原因是using语句创建了一个内部副本,它在try / finally构造中调用dispose。

考虑这个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;
    }
}

这里有一些反编译的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

请注意,在IL_000E中,创建了一个编译器生成的本地(CS$3$0001),并在那里存储了ms的副本。后来...

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,而不是ms(存储在位置0中)。

结果是msInnerAction所持有的副本都没有被处理掉。

结论:不要在using语句中使用结构。

编辑:正如@Weston在评论中指出的那样,you can manually box the struct and act on the boxed instance,因为它然后存在于堆上。通过这种方式,您可以获取要处置的实例,但如果您已将其强制转换回using语句中的结构,那么您只会在实例处置之前存储副本。此外,拳击移除了远离堆的好处,你可能已经到了这里。

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

((MyStruct)disposable).IsDisposed.Dump();

答案 1 :(得分:2)

代码的行为取决于MyStruct的内部实现。

考虑以下实施:

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;
    }
}

在上面的代码中,MyStruct实现仅将Dispose调用委托给其他引用类型。在这种情况下,在using块结束后,示例中的实例可能会被视为“已处置”。通过保存对布尔成员的内部引用可以实现类似的行为,指示该类是否已被释放。

然而,在@ codekaizen的答案和@ xanatos的评论中的例子中,行为是只有副本被处理,如那里所示。

最重要的是,您可以使用Disposed模式使结构正常运行,但我会避免这种情况,因为它非常容易出错。

答案 2 :(得分:0)

我认为不幸的是,C#的实施者决定将using与结构一起使用会导致该结构上的所有方法(包括Dispose)接收它的副本,因为行为导致代码比在原始代码上操作更慢,排除了本来可能是一些有用的语义,并且在任何情况下我都无法识别导致破解代码正常工作的原因。尽管如此,这种行为才是真实的。

因此,我建议任何结构都不应以任何预期修改结构本身的方式实现IDisposable。实现IDisposable的唯一结构类型应该符合以下一种或两种模式:

  1. 该结构用于封装对象的不可变引用,并且该结构表现为该对象的状态为其自身。我无法想到我曾经看到这种模式用于封装需要处理的物体,但似乎有可能。

  2. 结构的类型实现了一个继承IDisposable的接口,其中一些实现需要清理。如果结构本身不需要清理并且其处理方法什么都不做,那么在复制上调用处理方法的事实除了在调用do之前系统将浪费时间制作无用的结构副本这一事实之外没有任何后果。 - 没有任何方法。

  3. 请注意,C#的using语句的行为不仅会导致Dispose出现问题,而且会导致调用其他方法时出现问题。考虑:

    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);
      }
    }
    

    虽然这两种方法看起来相同,但第一种方法可行,第二种方法则不然。在第一种方法中,对MoveNext的每次调用都将对变量en起作用,从而推进枚举器。在第二个中,对MoveNext的每次调用都将根据en的不同副本进行操作;他们都不会推进调查员en。在Dispose的副本上调用第二种情况下的en调用的事实不会成为问题,因为该副本什么都不做。不幸的是,C#处理struct-type using参数的方式也会破坏using语句中的代码。