struct类型的深拷贝在“Using……”块中是否也被dispose了?

Is the deep copy of struct type also disposed when in the block of “Using……”?

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

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

当然看到using块后,ms就被处理掉了。但是 "InnerAction" 中的结构呢?是因为深拷贝还活着还是也被disposed了?

如果它仍然存在(未处理),我必须将 "ref" 用于 "InnerAction" 吗?

请给我你的证明:)

谢谢大家。

比你想象的还要糟糕: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[=11=][=11=]00
IL_0003:  initobj     UserQuery.MyStruct
IL_0009:  ldloc.1     // CS[=11=][=11=]00
IL_000A:  dup         
IL_000B:  stloc.0     // ms
IL_000C:  dup         
IL_000D:  stloc.0     // ms
IL_000E:  stloc.2     // CS[=11=]01
IL_000F:  nop         
IL_0010:  ldarg.0     
IL_0011:  ldloc.0     // ms

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

IL_001B:  ldloca.s    02 // CS[=12=]01
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();

代码的行为取决于 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 块结束后,您的示例中的实例可能会被视为 "Disposed" 。可以通过保存对布尔成员的内部引用来实现类似的行为,指示 class 是否已处理。

然而,在@codekaizen 的回答和@xanatos 的评论中的示例中,行为是仅处理了一个副本,如此处所示。

最重要的是,您可以让您的结构在 Disposed 模式下正确运行,但我会避免这样做,因为它很容易出错。

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

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

  1. 该结构用于封装对对象的不可变引用,并且该结构的行为就像该对象的状态一样。我想不出我在哪里见过这种用于封装需要处理的对象的模式,但它似乎是可能的。

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

请注意,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 的不同副本; none 将永远推进调查员 en。事实上,第二种情况下的 Dispose 调用是在 en 的副本上调用的,这不是问题,因为该副本什么都不做。不幸的是,C# 处理结构类型 using 参数的方式也破坏了 using 语句中的代码。