using (MyStruct ms = new MyStruct())
{
InnerAction(ms); //Notice "InnerAction" is "InnerAction(MyStruct ms)"
}
当然,在使用块之后,ms被处理掉了。但是,“InnerAction”中的结构体怎么样呢?它是否因为深度复制而仍然存在,还是也被处理掉了?
如果它仍然存在(没有被处理掉),我必须对“InnerAction”使用“ref”吗?
请给我证明 :)
谢谢大家。
using (MyStruct ms = new MyStruct())
{
InnerAction(ms); //Notice "InnerAction" is "InnerAction(MyStruct ms)"
}
情况比你想象的还要糟糕: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
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
会针对这个本地变量调用,而不是针对存储在位置0的ms
进行调用。
结果是ms
和InnerAction
持有的副本都没有被释放。
结论:不要在using
语句中使用结构体。
编辑:如@Weston在评论中指出的那样,您可以手动将结构体装箱并对装箱实例进行操作,因为它随后将位于堆上。通过这种方式,您可以使实例被释放,但如果您将其在using
语句中转换回结构体,则最终只会存储一个在实例被释放之前的副本。此外,装箱会消除保持离开堆的好处,这可能是您在这里所追求的。
MyStruct ms = new MyStruct();
var disposable = (IDisposable)ms;
using (disposable)
{
InnerAction(disposable);
}
((MyStruct)disposable).IsDisposed.Dump();
using
引用堆上的对象的方式中。上面的IL显示了这一点。 - codekaizenusing
操作右侧的结构实例上(堆上的装箱结构体),它确实“保存”了它:http://share.linqpad.net/tihvmj.linq。 - codekaizenms.IsDisposed.Dump();
了吗?它是false,所以装箱版本不是原始的。 - weston您的代码行为取决于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;
}
}
using
块结束后,您示例中的实例可能被视为“Disposed”。可以通过保存对表示类是否已处理的布尔成员的内部引用来实现类似的行为。System.Threading.CancellationToken
使用的模式,它用于提供对共享状态的安全访问。 - codekaizen我认为C#的实现者决定使用using
与结构体一起使用时,会导致该结构体上的所有方法(包括Dispose
)都接收到它的副本,这种行为会导致代码变慢,而不是操作原始数据,排除了本来可能有用的语义,并且在任何情况下都不能使本来错误的代码正常工作。尽管如此,这种行为就是它的样子。
因此,我建议不要以任何预期修改结构体本身的方式实现IDisposable
。只有符合以下一种或两种模式的结构类型才应该实现IDisposable
:
该结构用于封装对对象的不可变引用,并且该结构的行为就像该对象的状态是其自身的一样。我想不出在哪里看到过使用此模式来封装需要处理的对象,但这似乎是可能的。
该结构的类型实现了继承自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
的不同副本;它们都无法推进枚举器 en
。尽管第二种情况中对 en
的 Dispose
调用不会有问题,因为该副本什么也不做。不幸的是,C# 处理结构类型 using
参数的方式也会破坏 using
语句内的代码。
IDisposable
结构类型是一个不好的想法。 - xanatos