昨天在SO上回答问题时,我注意到如果使用Object Initializer初始化对象,编译器会创建一个额外的局部变量。
考虑以下在VS2008中以发布模式编译的C#3.0代码:
public class Class1
{
public string Foo { get; set; }
}
public class Class2
{
public string Foo { get; set; }
}
public class TestHarness
{
static void Main(string[] args)
{
Class1 class1 = new Class1();
class1.Foo = "fooBar";
Class2 class2 =
new Class2
{
Foo = "fooBar2"
};
Console.WriteLine(class1.Foo);
Console.WriteLine(class2.Foo);
}
}
使用Reflector,我们可以检查Main方法的代码:
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
.maxstack 2
.locals init (
[0] class ClassLibrary1.Class1 class1,
[1] class ClassLibrary1.Class2 class2,
[2] class ClassLibrary1.Class2 <>g__initLocal0)
L_0000: newobj instance void ClassLibrary1.Class1::.ctor()
L_0005: stloc.0
L_0006: ldloc.0
L_0007: ldstr "fooBar"
L_000c: callvirt instance void ClassLibrary1.Class1::set_Foo(string)
L_0011: newobj instance void ClassLibrary1.Class2::.ctor()
L_0016: stloc.2
L_0017: ldloc.2
L_0018: ldstr "fooBar2"
L_001d: callvirt instance void ClassLibrary1.Class2::set_Foo(string)
L_0022: ldloc.2
L_0023: stloc.1
L_0024: ldloc.0
L_0025: callvirt instance string ClassLibrary1.Class1::get_Foo()
L_002a: call void [mscorlib]System.Console::WriteLine(string)
L_002f: ldloc.1
L_0030: callvirt instance string ClassLibrary1.Class2::get_Foo()
L_0035: call void [mscorlib]System.Console::WriteLine(string)
L_003a: ret
}
在这里,我们可以看到编译器已经生成了对Class2
(class2
和<>g__initLocal0
)实例的两个引用,但只有一个对Class1
实例的引用}(class1
)。
现在,我对IL并不是很熟悉,但在设置<>g__initLocal0
之前,它似乎正在实例化class2 = <>g__initLocal0
。
为什么会这样?
是否遵循,使用对象初始化器时会产生性能开销(即使它非常轻微)?
答案 0 :(得分:61)
线程安全性和原子性。
首先,考虑以下代码:
MyObject foo = new MyObject { Name = "foo", Value = 42 };
任何阅读该陈述的人都可以合理地假设foo
对象的构造是原子的。在赋值之前,对象根本不存在。分配完成后,对象就存在并处于预期状态。
现在考虑两种可能的方式来翻译该代码:
// #1
MyObject foo = new MyObject();
foo.Name = "foo";
foo.Value = 42;
// #2
MyObject temp = new MyObject(); // temp will be a compiler-generated name
temp.Name = "foo";
temp.Value = 42;
MyObject foo = temp;
在第一种情况下,foo
对象在第一行被实例化,但在最后一行完成执行之前它不会处于预期状态。如果另一个线程在最后一行执行之前尝试访问该对象会发生什么?该对象将处于半初始化状态。
在第二种情况下,foo
对象在从temp
分配的最后一行之前不存在。由于引用赋值是原子操作,因此它提供与原始单行赋值语句完全相同的语义。即,foo
对象永远不会存在于半初始化状态。
答案 1 :(得分:33)
卢克的答案既正确又优秀,对你很好。但是,它并不完整。我们这样做的原因还有很多。
规范非常明确,这是正确的代码;规范说对象初始值设定项创建一个临时的,不可见的本地,它存储表达式的结果。但为什么我们这样说呢?那就是为什么呢
Foo foo = new Foo() { Bar = bar };
装置
Foo foo;
Foo temp = new Foo();
temp.Bar = bar;
foo = temp;
而不是更直截了当的
Foo foo = new Foo();
foo.Bar = bar;
嗯,作为一个纯粹的实际问题,根据表达式的内容而不是上下文来指定表达式的行为总是更容易。但是,对于这种特定情况,假设我们指定这是分配给本地或字段所需的codegen。在这种情况下,foo将在()之后明确分配,因此可以在初始化程序中使用。你真的想要
吗?Foo foo = new Foo() { Bar = M(foo) };
合法吗?我希望不是。 foo在初始化完成之前没有明确分配。
或者,考虑属性。
Frob().MyFoo = new Foo() { Bar = bar };
这必须是
Foo temp = new Foo();
temp.Bar = bar;
Frob().MyFoo = temp;
而不是
Frob().MyFoo = new Foo();
Frob().MyFoo.Bar = bar;
因为我们不希望Frob()调用两次而且我们不希望属性MyFoo被访问两次,我们希望它们每次访问一次。
现在,在您的特定情况下,我们可以编写一个优化传递,检测额外的本地是不必要的并优化它。但我们还有其他优先事项,抖动可能很好地优化了当地人。
好问题。我一直想写这篇文章一段时间。
答案 2 :(得分:2)
对于为什么:可能是为了确保不存在对非(完全)初始化对象(从语言的角度来看)的“已知”引用?对象初始化器的(伪)构造函数语义之类的东西?但这只是一个想法......除了在多线程环境中,我无法想象使用引用和访问未初始化对象的方法。
编辑:太慢..