代码之家  ›  专栏  ›  技术社区  ›  Winston Smith

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

  •  33
  • Winston Smith  · 技术社区  · 15 年前

    在昨天回答问题时,我注意到如果使用对象初始值设定项初始化对象,编译器会创建一个额外的局部变量。

    考虑以下C 3.0代码,在VS2008中以发布模式编译:

    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,我们可以检查主要方法的代码:

    .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不是很熟悉,但它看起来像是在实例化 <>在本地0 ,设置前 class2 = <>g__initLocal0 .

    为什么会这样?

    那么,在使用对象初始值设定项时是否会有性能开销(即使很小)?

    3 回复  |  直到 15 年前
        1
  •  60
  •   LukeH    15 年前

    线程安全性和原子性。

    首先,考虑这一行代码:

    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公司 对象在最后一行(从中分配时)之前不存在 temp . 因为引用赋值是一个原子操作,所以它给出的语义与原始的单行赋值语句完全相同。IE 对象从未处于半初始化状态。

        2
  •  32
  •   Eric Lippert    15 年前

    卢克的回答既正确又优秀,对你很好。然而,它并不完整。我们这样做还有更多的理由。

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

    Foo foo = new Foo() { Bar = bar };
    

    方法

    Foo foo;
    Foo temp = new Foo();
    temp.Bar = bar;
    foo = temp;
    

    更直接的是

    Foo foo = new Foo();
    foo.Bar = bar;
    

    好吧,作为一个纯粹的实际问题,根据表达式的内容而不是上下文来指定表达式的行为总是比较容易的。但是,对于这个特定的情况,假设我们指定这是分配给本地或字段的所需代码生成器。在这种情况下,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被访问两次,所以我们希望每个属性都被访问一次。

    现在,在您的特定情况下,我们可以编写一个优化过程,检测额外的局部是不必要的,并将其优化掉。但我们还有其他的优先事项,而这种抖动可能会很好地优化本地环境。

    问得好。我想写这篇博客已经有一段时间了。

        3
  •  2
  •   andyp    15 年前

    为什么:可以这样做是为了确保(从语言的角度)不存在对未初始化对象的“已知”引用吗?类似于对象初始值设定项的(伪)构造函数语义?但这只是个主意……除了在多线程环境中,我无法想象使用引用和访问未初始化对象的方法。

    编辑:太慢..