2009-11-05 8 views
31

Odpowiadając na pytanie na temat SO wczoraj zauważyłem, że jeśli obiekt jest inicjowany za pomocą inicjalizatora obiektów, kompilator tworzy dodatkową zmienną lokalną.Podczas korzystania z inicjalizatorów obiektów, dlaczego kompilator generuje dodatkową zmienną lokalną?

Rozważmy następujący kod C# 3.0, skompilowany w trybie zwolnienia w 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); 
    } 
} 

Korzystanie reflektor, możemy zbadać kod do metody 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 
} 

Tutaj możemy zobaczyć kompilator wygenerował dwa odwołania do instancji Class2 (class2 i <>g__initLocal0), ale tylko jedno odwołanie do instancji Class1 (class1).

Nie jestem zaznajomiony z IL, ale wygląda na to, że tworzy się <>g__initLocal0, przed ustawieniem class2 = <>g__initLocal0.

Dlaczego tak się dzieje?

Czy ma to związek z tym, że podczas korzystania z Inserizerów obiektów występuje obciążenie wydajnościowe (nawet jeśli jest bardzo niewielkie)?

Odpowiedz

58

Bezpieczeństwo gwintu i atomowość.

pierwsze, rozważyć ten wiersz kodu:

MyObject foo = new MyObject { Name = "foo", Value = 42 }; 

ktoś czytając to stwierdzenie można zasadnie przypuszczać, że budowa obiektu foo będzie atomowej. Przed zadaniem obiekt w ogóle nie istnieje. Po zakończeniu przypisania obiekt istnieje i znajduje się w oczekiwanym stanie.

Rozważmy teraz dwa możliwe sposoby tłumaczenia tego kodu:

// #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; 

w pierwszym przypadku celem foo jest tworzony na pierwszej linii, ale to nie będzie w stanie do oczekiwanego finału linia ma zakończone wykonywanie. Co się stanie, gdy inny wątek spróbuje uzyskać dostęp do obiektu przed wykonaniem ostatniego wiersza? Obiekt będzie w stanie półinicjalizowanym.

W drugim przypadku obiekt foo nie istnieje do ostatniego wiersza, gdy jest przypisany z temp. Ponieważ przypisanie odniesienia jest operacją atomową, daje to dokładnie tę samą semantykę, co oryginalna, jednoliniowa instrukcja przypisania. tzn. obiekt foo nigdy nie istnieje w stanie półinicjalizowanym.

+7

+1, świetny przykład – Tenner

2

Z tego powodu: czy możliwe jest zapewnienie, że nie istnieje "znane" odniesienie do niezupełnego (w pełni) zainicjowanego obiektu (z punktu widzenia języka)? Coś takiego jak (pseudo) konstruktorowa semantyka dla inicjatora obiektu? Ale to tylko pomysł ... i nie mogę sobie wyobrazić sposobu użycia referencji i dostępu do niezainicjowanego obiektu, poza tym w środowisku wielowątkowym.

EDIT: zbyt wolno ..

31

odpowiedź Luke'a jest zarówno prawdziwe i dobre, tak dobrze na ciebie. Nie jest jednak kompletny. Jest jeszcze więcej dobrych powodów, dla których to robimy.

W specyfikacji wyraźnie stwierdza się, że jest to właściwy kodek; specyfikacja mówi, że inicjator obiektu tworzy tymczasowy, niewidoczny lokalny, który przechowuje wynik wyrażenia. Ale dlaczego tak to określiliśmy? To jest, dlaczego jest tak, że

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

oznacza

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

a nie bardziej prostą

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

Cóż, jako czysto praktycznego punktu widzenia, to zawsze łatwiej określić zachowanie wyrażenie w oparciu o jego zawartość, a nie kontekst. W tym konkretnym przypadku załóżmy, że określiliśmy, że był to pożądany kodegen do przypisania do lokalnego lub pola. W takim przypadku, foo byłby definitywnie przypisany po(), a zatem mógłby być użyty w inicjalizatorze. Czy NAPRAWDĘ chcesz, aby ma być legalny? Mam nadzieję, że nie. foo nie jest definitywnie przypisany, dopóki nie zostanie wykonana inicjalizacja.

Lub rozważ właściwości.

Frob().MyFoo = new Foo() { Bar = bar }; 

To musi być

Foo temp = new Foo(); 
temp.Bar = bar; 
Frob().MyFoo = temp; 

i nie

Frob().MyFoo = new Foo(); 
Frob().MyFoo.Bar = bar; 

ponieważ nie chcemy FROB() nazywa się dwa razy i nie chcemy nieruchomość MyFoo obejrzano dwa razy, my chcę, aby każdy był dostępny raz.

Teraz, w twoim konkretnym przypadku, możemy napisać przepustkę optymalizacyjną, która wykrywa, że ​​dodatkowy lokalny jest niepotrzebny i optymalizuje go. Ale mamy inne priorytety, a jitter prawdopodobnie wykonuje dobrą robotę optymalizacji mieszkańców.

Dobre pytanie. Zamierzałem blogować ten przez jakiś czas.

Powiązane problemy