2009-11-05 10 views
31

Al responder a una pregunta sobre SO ayer, noté que si un objeto se inicializa con un Inicializador de objetos, el compilador crea una variable local adicional.Al utilizar inicializadores de objetos, ¿por qué el compilador genera una variable local adicional?

Consideremos el siguiente código de C# 3.0, compilado en modo de lanzamiento de 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); 
    } 
} 

El uso del reflector, podemos examinar el código para el método principal:

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

Aquí, podemos ver que el compilador ha generado dos referencias a una instancia de Class2 (class2 y <>g__initLocal0), pero solo una referencia a una instancia de Class1 (class1).

Ahora, no estoy muy familiarizado con IL, pero parece que está instanciando <>g__initLocal0, antes de establecer class2 = <>g__initLocal0.

¿Por qué sucede esto?

¿Sigue entonces, que hay una sobrecarga de rendimiento al usar Inicializadores de objetos (incluso si es muy leve)?

Respuesta

58

Seguridad y atomicidad del hilo.

primer lugar, considerar esta línea de código:

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

cualquiera que lea esta declaración podría razonablemente suponer que la construcción del objeto foo será atómica. Antes de la asignación, el objeto no existe en absoluto. Una vez que la tarea se ha completado, el objeto existe y se encuentra en el estado esperado.

Consideremos ahora dos posibles formas de traducir ese código:

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

En el primer caso, el objeto foo se crea una instancia en la primera línea, pero no va a estar en el estado de esperar hasta que la última línea tiene terminado de ejecutar ¿Qué sucede si otro hilo intenta acceder al objeto antes de que se ejecute la última línea? El objeto estará en un estado semi-inicializado.

En el segundo caso, el objeto foo no existe hasta la última línea cuando se asigna desde temp. Como la asignación de referencia es una operación atómica, proporciona exactamente la misma semántica que la declaración de asignación original de una sola línea. es decir, el objeto foo nunca existe en un estado semiautomatizado.

+7

+1, gran ejemplo – Tenner

2

Por el motivo: ¿podría ser que se hace para asegurarse de que no existe una referencia "conocida" a un objeto no (totalmente) inicializado (desde el punto de vista del lenguaje)? Algo así como (pseudo-) semántica de constructor para el inicializador de objetos? Pero eso es solo una idea ... y no puedo imaginar una forma de usar la referencia y acceder al objeto no inicializado además de en un entorno de subprocesos múltiples.

EDIT: demasiado lento ..

31

respuesta de Lucas es a la vez correcta y excelente, por lo que bien en ti. No es, sin embargo, completo. Hay aún más buenas razones por las que hacemos esto.

La especificación es extremadamente clara que este es el codegen correcto; la especificación dice que un inicializador de objetos crea un local temporal e invisible que almacena el resultado de la expresión. Pero, ¿por qué lo especificamos de esa manera? Es decir, ¿cómo es que

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

significa

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

y no el más sencillo

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

Bueno, como una cuestión puramente práctico, siempre es más fácil para especificar el comportamiento de una expresión basada en su contenido, no en su contexto. Sin embargo, para este caso específico, supongamos que especificamos que este era el codegen deseado para la asignación a un local o campo. En ese caso, foo sería definitivamente asignado después de(), y por lo tanto podría usarse en el inicializador. ¿REALMENTE quiere

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

para ser legal? Espero que no. Foo no está definitivamente asignado hasta después de que la inicialización haya terminado.

O bien, tenga en cuenta las propiedades.

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

Esto tiene que ser

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

y no

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

porque no queremos FROB() llama dos veces y no queremos myFoo propiedad accede dos veces, quiero que cada uno acceda una vez.

Ahora, en su caso particular, podríamos escribir un pase de optimización que detecte que el extra local es innecesario y optimizarlo. Pero tenemos otras prioridades, y el jitter probablemente hace un buen trabajo optimizando a los locales.

Buena pregunta. He querido bloguear este por un tiempo.

Cuestiones relacionadas