在"正在使用……"的块中时,结构类型的深层副本是否也被处理

本文关键字:quot 副本 处理 类型 是否 结构 | 更新日期: 2023-09-27 18:04:30

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

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

当然,我看到在使用块之后,ms被处理掉了。但是,"InnerAction"中的结构呢?它是因为深度复制而仍然存在,还是也被处理了?

如果它还活着(未处理(,我必须使用"ref"作为"InnerAction"吗?

请给我你的证明:(

Thx all。

在"正在使用……"的块中时,结构类型的深层副本是否也被处理

这比你想象的更糟糕:ms甚至没有被处理。

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

考虑以下LinqPad示例:

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中(。

结果是CCD_ 7和CCD_。

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

EDIT:正如@Weston在评论中指出的那样,您可以手动装箱结构并对装箱的实例执行操作,因为它会存在于堆中。通过这种方式,您可以获得要处置的实例,但如果您在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块结束后,示例中的实例可能被视为"已处置"。类似的行为可以通过保存对布尔成员的内部引用来实现,该布尔成员指示类是否已被释放。

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

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

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

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

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

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

请注意,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的不同副本;它们中的任何一个都不会推进枚举器CCD_ 23。第二种情况中的Dispose调用是在en的副本上调用的,这不会是一个问题,因为该副本什么都不做。不幸的是,C#处理结构类型using参数的方式也破坏了using语句中的代码。