2009-11-05 40 views
31

昨天在回答SO問題時,我注意到如果使用Object Initializer初始化一個對象,編譯器會創建一個額外的局部變量。使用對象初始值設定項時,爲什麼編譯器會生成額外的局部變量?

考慮下面的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); 
    } 
} 

使用反射器,我們可以檢查爲主要方法的代碼:

.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)的實例的兩個引用,但是隻有一個對Class1class1)的實例的引用。現在

,我不是很熟悉,IL,但它看起來像它的實例<>g__initLocal0,設置class2 = <>g__initLocal0之前。

爲什麼會發生這種情況?

它是否遵循那麼使用對象初始化器(即使它非常輕微)時會有性能開銷?

回答

58

線程安全性和原子性。

首先,考慮這行代碼:

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對象從不存在於半初始化狀態。

+7

+1,很好的例子 – Tenner 2009-11-05 17:36:32

2

對於爲什麼:可能是爲了確保沒有(已知的)對未完全初始化的對象(從語言的角度來看)的引用存在?對象初始值設定項就像(僞)構造函數語義?但這只是一個想法..我無法想象一種方式來使用引用,並訪問多線程環境中未初始化的對象。

編輯:太慢了..

31

盧克的回答是正確和優秀,你那麼好。然而,這並不完整。我們爲什麼要這樣做有更多的理由。

該規範非常明確,這是正確的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訪問兩次,我們希望他們每個訪問一次。

現在,在您的具體情況下,我們可以編寫一個優化過程,檢測額外的本地是不必要的並優化它。但我們還有其他優先事項,抖動可能在優化當地人方面做得很好。

好問題。我一直想寫這篇文章一段時間。

相關問題