我对以下代码有一些疑问:
using System;
namespace ConsoleApplication2
{
public struct Disposable : IDisposable
{
public void Dispose() { }
}
class Program
{
static void Main(string[] args)
{
using (Test()) { }
}
static Disposable Test()
{
return new Disposable();
}
}
}
我的问题是:
Disposable
结构进行操作的using语句是否会从结构中返回Test()
框?为了试图找出自己,我检查了上面代码生成的IL,这里是Main(...)
方法的IL:
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
.maxstack 1
.locals init (
[0] valuetype ConsoleApplication2.Disposable CS$3$0000)
L_0000: call valuetype ConsoleApplication2.Disposable ConsoleApplication2.Program::Test()
L_0005: stloc.0
L_0006: leave.s L_0016
L_0008: ldloca.s CS$3$0000
L_000a: constrained ConsoleApplication2.Disposable
L_0010: callvirt instance void [mscorlib]System.IDisposable::Dispose()
L_0015: endfinally
L_0016: ret
.try L_0006 to L_0008 finally handler L_0008 to L_0016
}
我怀疑在L_0010
上调用虚拟方法会引入装箱操作,但实际的box
指令不在此处。
我问的原因是,不久前,大概1 - 2年,我在网上看到了某人评论过的使用声明的“优化”。这种情况是使用using语句作为对象的短时间锁定的语法,其中在方法中获取了锁,并且返回了一个结构,当处理掉时,将释放锁,代码如下:
using (LockTheObject())
{
// use the object
}
并且注释是通过将LockTheObject
方法的返回类型从IDisposable
更改为使用的实际结构,避免了装箱。
但我想知道这是真的还是真的。
有人能指出我正确的方向吗?如果,为了看到框操作,我将不得不检查运行时汇编代码,请给我一个示例来查找,我非常精通汇编代码,所以这不是问题,但没有跳出来当我看着那个时,对我来说。
答案 0 :(得分:6)
看起来好像放在using
语句中的任何值类型都不会被装箱。这似乎是一个C#优化,因为当实现IDisposable
的值类型在using
语句中而不是在任何其他上下文中时,才会省略装箱。
有关详细信息,请参阅The Using Statement And Disposable Value Types:
不久前Ian Griffiths写了一篇文章 他的TimedLock课程的改进 他把它从一个班级变成了一个班级 结构。这一变化导致了一个 实现的值类型 IDisposable接口。我有一个唠叨的问题 在我脑海里当时 我很快忘记了。该 问题不是那种情况 调用Dispose时输入的类型是什么?
还有Oh No! Not the TimedLock Again!:
约翰·桑兹指出了一个缺陷
我在最近的博客中展示的代码
在没有锁的情况下使用超时
放弃了大部分的便利
C#的lock
关键字。
答案 1 :(得分:4)
这是If my struct implements IDisposable will it be boxed when used in a using statement?
的副本更新:这个问题是the subject of my blog in March of 2011。谢谢你提出的好问题!
Andrew Hare的回答是正确的;我只想添加一个有趣的额外注释。我们发出的优化 - 使用受约束的callvirt在可能的情况下跳过装箱 - 实际上严格来说违反了C#规范。规范声明我们为值类型资源生成的finally块是:
finally
{
((IDisposable)resource).Dispose();
}
显然是值类型的装箱转换。可以构建设计的场景,其中实现中缺少拳击是可见的。
(感谢Vladimir Reshetnikov指出这违反规范的行为。)
答案 2 :(得分:3)
值类型的实例方法将this
参数作为其第一个参数,类似于引用类型的实例方法。但是,在这种情况下,参数是指向对象数据的托管指针,而不是对盒装对象的引用。您可能会发现它在内存中的布局如下:
Unboxed object:
-----------------------------------------
| DATA |
-----------------------------------------
^ managed pointer to struct
Boxed object:
------------------------------------------------------------
| GC/Object header | [Boxed] DATA |
------------------------------------------------------------
^ The 'unbox' opcode gives a managed pointer to the boxed data
^ A *reference* to any instance of a reference type or boxed object, points here
DATA
在这两种情况下都相同¹。
值类型的实例方法期望托管指针指向数据具体,因此不需要装箱对象。如上所述,在调用之前使用constrained
操作码。它告诉运行时,以下callvirt
指令正在接收指向ConsoleApplication2.Disposable
结构的托管指针,而不是它通常接收的对象引用。在这样做时,JIT可以解析由结构实现的Dispose()
的密封重载,并直接调用它而不用装箱对象。如果没有constrained
前缀,则传递给callvirt
指令的对象必须是对象引用,因为标准虚拟调用动态解析过程基于GC / Object头是总是在预期的位置 - 是的,这会强制拳击值类型。
¹我们现在继续忽略Nullable<T>
。