¿Qué ventaja hay para almacenar “esto” en una variable local en un método de estructura?

Hoy exploré el árbol de fonts de .NET Core y encontré este patrón en System.Collections.Immutable.ImmutableArray :

 T IList.this[int index] { get { var self = this; self.ThrowInvalidOperationIfNotInitialized(); return self[index]; } set { throw new NotSupportedException(); } } 

Este patrón (almacenando this en una variable local) parece aplicarse consistentemente en este archivo cuando de otra manera se haría referencia varias veces en el mismo método, pero no cuando solo se hace referencia una vez. Entonces comencé a pensar en cuáles podrían ser las ventajas relativas de hacerlo de esta manera; Me parece que la ventaja está probablemente relacionada con el rendimiento, así que tomé esta ruta un poco más lejos … tal vez estoy pasando por alto algo más.

El CIL que se emite para el patrón “almacenar this en un local” parece tener el aspecto de ldarg.0 , luego ldobj UnderlyingType , luego stloc.0 modo que las referencias posteriores provienen de ldloc.0 lugar de un ldarg.0 como Sería simplemente usar this varias veces.

Tal vez ldarg.0 sea ​​significativamente más lento que ldloc.0 , pero no tanto para la traducción de C # a CIL como para el JITter para buscar oportunidades de optimizar esto para nosotros, de modo que tenga más sentido escribir este aspecto extraño ¿Patrón en el código C # en cualquier momento, de lo contrario, emitiríamos dos instrucciones ldarg.0 en un método de instancia de estructura?

Actualización: o, ya sabes, podría haber mirado los comentarios en la parte superior de ese archivo, que explican exactamente lo que está pasando …

Como ya notó, System.Collections.Immutable.ImmutableArray es una estructura :

 public partial struct ImmutableArray : ... { ... T IList.this[int index] { get { var self = this; self.ThrowInvalidOperationIfNotInitialized(); return self[index]; } set { throw new NotSupportedException(); } } ... 

var self = this; crea una copia de la estructura referida por esto . ¿Por qué debería hacer eso? Los comentarios de la fuente de esta estructura dan una explicación de por qué es necesario:

/// Este tipo debe ser seguro para subprocesos. Como estructura, no puede proteger sus propios campos.
/// se ha cambiado de un subproceso mientras sus miembros se ejecutan en otros subprocesos
/// porque las estructuras pueden cambiar en su lugar simplemente reasignando el campo que contiene
/// esta estructura. Por eso es extremadamente importante que
/// ** Cada miembro solo debe desreferenciar esto una vez. **
/// Si un miembro necesita hacer referencia al campo de matriz, eso cuenta como una desreferencia de esto.
/// Llamar a otros miembros de la instancia (propiedades o métodos) también cuenta como dereferencia de esto.
/// Cualquier miembro que necesite usar esto más de una vez debe hacerlo
/// asigne esto a una variable local y use eso para el rest del código en su lugar.
/// Esto efectivamente copia el campo en la estructura a una variable local para que
/// está aislado de otros hilos.

En resumen, si es posible que otros subprocesos estén realizando cambios en un campo de la estructura o cambiando la estructura en su lugar (al reasignar un campo de miembro de clase de este tipo de estructura, por ejemplo) mientras se ejecuta el método get y, por lo tanto, podría causar efectos secundarios negativos, entonces es necesario que el método de obtención realice primero una copia (local) de la estructura antes de procesarla.

Actualización: lea también la respuesta de supercats , que explica en detalle qué condiciones deben cumplirse para que una operación como hacer una copia local de una estructura (es decir, var self = this; ) sea segura para subprocesos, y qué podría suceder si esas condiciones No se cumplen.

Las instancias de estructura en .NET siempre son mutables si la ubicación de almacenamiento subyacente es mutable, y siempre son inmutables si la ubicación de almacenamiento subyacente es inmutable. Es posible que los tipos de estructura “simulen” ser inmutables, pero .NET permitirá que las instancias de tipo estructura sean modificadas por cualquier cosa que pueda escribir las ubicaciones de almacenamiento en las que residen, y los tipos de estructura en sí mismos no tienen nada que decir al respecto.

Por lo tanto, si uno tuviera una estructura:

 struct foo { String x; override String ToString() { String result = x; System.Threading.Thread.Sleep(2000); return result & "+" & x; } foo(String xx) { x = xx; } } 

y uno fue invocar el siguiente método en dos subprocesos con la misma matriz myFoos de tipo foo[] :

 myFoos[0] = new foo(DateTime.Now.ToString()); var st = myFoos[0].ToString(); 

sería completamente posible que el primer hilo que se inicie primero tenga su valor ToString() informando el tiempo escrito por su llamada de constructor y el tiempo informado por la llamada de constructor del otro hilo, en lugar de informar la misma cadena dos veces. Para los métodos cuyo propósito es validar un campo de estructura y luego usarlo, hacer que el campo cambie entre la validación y el uso daría lugar a que el método use un campo no validado. Copiar el contenido del campo de la estructura (ya sea copiando solo el campo, o copiando toda la estructura) evita ese peligro.

Tenga en cuenta que para las estructuras que contienen un campo de tipo Int64 , UInt64 o Double , o que contienen más de un campo, es posible que una statement como var temp=this; lo que ocurre en un hilo mientras que otro está sobrescribiendo la ubicación donde se almacenó, puede terminar copiando una estructura que contiene una mezcla arbitraria de contenido antiguo y nuevo. Solo cuando una estructura contiene un solo campo de un tipo de referencia, o un solo campo de una primitiva de 32 bits o más pequeña, se garantiza que una lectura que ocurra simultáneamente con una escritura producirá algún valor que la estructura realmente sostuvo, e incluso eso puede tener algunas peculiaridades (por ejemplo, al menos en VB.NET, una statement como someField = New foo("george") puede borrar someField antes de llamar al constructor).