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

时间:2009-11-05 10:56:23

标签: c# .net 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);
    }
}

使用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 
}

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

现在,我对IL并不是很熟悉,但在设置<>g__initLocal0之前,它似乎正在实例化class2 = <>g__initLocal0

为什么会这样?

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

3 个答案:

答案 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)

对于为什么:可能是为了确保不存在对非(完全)初始化对象(从语言的角度来看)的“已知”引用?对象初始化器的(伪)构造函数语义之类的东西?但这只是一个想法......除了在多线程环境中,我无法想象使用引用和访问未初始化对象的方法。

编辑:太慢..