使用对象初始值设定项时,为什么编译器会生成额外的局部变量?

Win*_*ith 33 .net c# c#-3.0

在昨天回答关于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);
    }
}
Run Code Online (Sandbox Code Playgroud)

使用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 
}
Run Code Online (Sandbox Code Playgroud)

在这里,我们可以看到编译器已经生成了两个对Class2(class2<>g__initLocal0)实例的引用,但只有一个对Class1(class1)实例的引用.

现在,我对IL并不是很熟悉,但<>g__initLocal0在设置之前它看起来像是在实例化class2 = <>g__initLocal0.

为什么会这样?

是否遵循这一点,使用对象初始化器时会产生性能开销(即使它非常轻微)?

Luk*_*keH 61

线程安全性和原子性.

首先,考虑以下代码行:

MyObject foo = new MyObject { Name = "foo", Value = 42 };
Run Code Online (Sandbox Code Playgroud)

阅读该陈述的任何人都可以合理地假设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;
Run Code Online (Sandbox Code Playgroud)

在第一种情况下,foo对象在第一行上实例化,但在最后一行完成执行之前它不会处于预期状态.如果另一个线程在最后一行执行之前尝试访问该对象会发生什么?该对象将处于半初始化状态.

在第二种情况下,foo对象在分配给它的最后一行之前不存在temp.由于引用赋值是原子操作,因此它提供与原始单行赋值语句完全相同的语义.即,foo对象从不存在于半初始化状态.


Eri*_*ert 33

卢克的答案既正确又优秀,对你很好.但是,它并不完整.我们这样做的原因还有很多.

规范非常清楚,这是正确的codegen; 规范说对象初始值设定项创建一个临时的,不可见的本地,它存储表达式的结果.但为什么我们这样说呢?那就是为什么呢

Foo foo = new Foo() { Bar = bar };
Run Code Online (Sandbox Code Playgroud)

手段

Foo foo;
Foo temp = new Foo();
temp.Bar = bar;
foo = temp;
Run Code Online (Sandbox Code Playgroud)

而不是更直截了当

Foo foo = new Foo();
foo.Bar = bar;
Run Code Online (Sandbox Code Playgroud)

好吧,作为一个纯粹的实际问题,基于其内容而不是其上下文来指定表达式的行为总是更容易.但是,对于这种特定情况,假设我们指定这是分配给本地或字段所需的codegen.在这种情况下,foo将在()之后明确赋值,因此可以在初始化程序中使用.你真的想要吗?

Foo foo = new Foo() { Bar = M(foo) };
Run Code Online (Sandbox Code Playgroud)

合法吗?我希望不是.foo在初始化完成之前没有明确分配.

或者,考虑属性.

Frob().MyFoo = new Foo() { Bar = bar };
Run Code Online (Sandbox Code Playgroud)

这必须是

Foo temp = new Foo();
temp.Bar = bar;
Frob().MyFoo = temp;
Run Code Online (Sandbox Code Playgroud)

并不是

Frob().MyFoo = new Foo();
Frob().MyFoo.Bar = bar;
Run Code Online (Sandbox Code Playgroud)

因为我们不希望Frob()调用两次而且我们不希望属性MyFoo被访问两次,我们希望它们每次访问一次.

现在,在您的特定情况下,我们可以编写一个优化传递,检测额外的本地是不必要的并优化它.但我们还有其他优先事项,抖动可能很好地优化了当地人.

好问题.我一直想写这篇文章一段时间.