2009-11-05 20 views
31

Mentre rispondendo ad una domanda su SO ieri, ho notato che se un oggetto viene inizializzato tramite un inizializzazione degli oggetti, il compilatore crea una variabile in più locale.Quando si utilizzano gli inizializzatori di oggetti, perché il compilatore genera una variabile locale extra?

Si consideri il codice C# 3.0 in seguito, compilato in modalità di rilascio in 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); 
    } 
} 

Utilizzando riflettore, siamo in grado di esaminare il codice per il metodo principale:

.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 
} 

Qui, possiamo vedere che il compilatore ha generato due riferimenti a un'istanza di Class2 (class2 e <>g__initLocal0), ma solo riferimento a un'istanza di Class1 (class1).

Ora, non ho molta familiarità con IL, ma sembra che stia creando un'istanza <>g__initLocal0, prima di impostare class2 = <>g__initLocal0.

Perché succede?

ne consegue quindi, che c'è un sovraccarico di prestazioni quando si utilizzano Inizializzatori oggetti (anche se è molto piccola)?

risposta

58

thread-safe e atomicità.

In primo luogo, prendere in considerazione questa riga di codice:

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

Chiunque leggendo questa affermazione può ragionevolmente supporre che la costruzione dell'oggetto foo sarà atomica. Prima dell'assegnazione l'oggetto non esiste affatto. Una volta completato il compito, l'oggetto esiste ed è nello stato previsto.

consideri ora due possibili modi di tradurre tale codice:

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

Nel primo caso l'oggetto viene creata un'istanza foo sulla prima linea, ma non sarà nello stato previsto finché la linea finale ha terminato l'esecuzione. Cosa succede se un altro thread tenta di accedere all'oggetto prima dell'esecuzione dell'ultima riga? L'oggetto sarà in uno stato semi-inizializzato.

Nel secondo caso l'oggetto foo non esiste fino alla riga finale quando è assegnato da temp. Poiché l'assegnazione di riferimento è un'operazione atomica, ciò fornisce esattamente la stessa semantica dell'istruzione di assegnazione originale a riga singola. cioè, l'oggetto foo non esiste mai in uno stato semi-inizializzato.

+7

+1, ottimo esempio – Tenner

2

Per la Perché: potrebbe essere che è fatto per garantire che nessun "conosciuto" riferimento ad un oggetto non (completamente) inizializzato (dal punto di vista del linguaggio) esiste? Qualcosa come (pseudo-) semantica del costruttore per l'inizializzatore dell'oggetto? Ma questo è solo un'idea .. e non riesco a immaginare un modo per utilizzare il riferimento e accedere l'oggetto non inizializzato oltre che in un ambiente multi-threaded.

EDIT: troppo lento ..

31

risposta di Luca è sia corretta ed eccellente, così bene su di voi. Tuttavia, non è completo. Ci sono ancora più buone ragioni per cui lo facciamo.

La specifica è estremamente chiara che questo è il codegen corretto; la specifica dice che un inizializzatore di oggetti crea un locale temporaneo, invisibile che memorizza il risultato dell'espressione. Ma perché lo abbiamo specificato in questo modo? Vale a dire, perché è che

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

significa

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

e non il più semplice

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

Beh, come una questione puramente pratico, è sempre più facile per specificare il comportamento di un'espressione basata sul suo contenuto, non sul suo contesto. Per questo specifico caso, supponiamo di aver specificato che questo era il codegen desiderato per l'assegnazione a un locale o campo. In tal caso, foo sarebbe assegnato definitivamente a dopo(), e quindi potrebbe essere utilizzato nell'inizializzatore. Vuoi davvero

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

legale? Spero di no. foo non è assegnato definitivamente fino a dopo l'inizializzazione.

Oppure considerare proprietà.

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

Questo deve essere

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

e non

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

perché non vogliamo Frob() chiamato due volte e non vogliamo proprietà MyFoo accede due volte, abbiamo Li voglio tutti acceduti una volta.

Ora, nel tuo caso particolare, potremmo scrivere un passaggio di ottimizzazione che rileva che l'extra locale non è necessario e lo ottimizza. Ma abbiamo altre priorità, e il jitter probabilmente fa un buon lavoro di ottimizzazione dei locali.

Buona domanda. Ho intenzione di blog questo per un po '.

Problemi correlati