Estaba navegando por el árbol de fuentes de .NET Core hoy y me encontré con este patrón en System.Collections.Immutable.ImmutableArray<T>
:
T IList<T>.this[int index] { get { var self = this; self.ThrowInvalidOperationIfNotInitialized(); return self[index]; } set { throw new NotSupportedException(); } }
Este patrón (almacenar this
en una variable local) parece aplicarse consistentemente en this
archivo siempre que se haga referencia a esto 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 probablemente esté relacionada con el rendimiento, así que fui por este camino un poco más... tal vez estoy pasando por alto algo más.
El CIL que se emite para el patrón "almacenar this
en un local" parece parecerse a ldarg.0
, luego ldobj UnderlyingType
, luego stloc.0
para que las referencias posteriores provengan de ldloc.0
en lugar de un ldarg.0
como sería simplemente usar this
varias veces.
Tal vez ldarg.0
es significativamente más lento que ldloc.0
, pero no lo suficiente como para que la traducción de C# a CIL o el JITter busquen oportunidades para optimizar esto para nosotros, por lo que tiene más sentido escribir este aspecto extraño patrón en el código C# cada vez que 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<T> es una estructura :
public partial struct ImmutableArray<T> : ... { ... T IList<T>.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 a la que hace referencia this . ¿Por qué debería necesitar hacer eso? Los comentarios 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.
/// de ser cambiado de un subproceso mientras sus miembros se ejecutan en otros subprocesos
/// porque las estructuras pueden cambiar de lugar simplemente reasignando el campo que contiene
/// esta estructura. Por lo tanto, es sumamente importante que
/// ** Cada miembro solo debe quitar la referencia a esto UNA VEZ. **
/// Si un miembro necesita hacer referencia al campo de la matriz, eso cuenta como una falta de referencia de este.
/// Llamar a otros miembros de la instancia (propiedades o métodos) también cuenta como desreferenciar esto.
/// Cualquier miembro que necesite usar esto más de una vez debe hacerlo
/// asigne esto a una variable local y utilícelo para el resto del código en su lugar.
/// Esto copia efectivamente el campo uno en la estructura a una variable local para que
/// está aislado de otros hilos.
En resumen, si es posible que otros subprocesos realicen cambios en un campo de la estructura o cambien la estructura en su lugar (por ejemplo, al reasignar un campo de miembro de clase de este tipo de estructura) mientras se ejecuta el método get y, por lo tanto, podrían causar efectos secundarios negativos, entonces se vuelve necesario que el método get primero haga 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 inmutables si la ubicación de almacenamiento subyacente es inmutable. Es posible que los tipos de estructura "finjan" ser inmutables, pero .NET permitirá que las instancias de tipo de 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 invocaría 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 muy posible que cualquier subproceso que comenzara primero tuviera su valor ToString()
informando el tiempo escrito por su llamada al constructor y el tiempo informado por la llamada al constructor del otro subproceso, 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 como resultado 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 instrucción como var temp=this;
que ocurre en un subproceso mientras otro sobrescribe la ubicación donde this
había almacenado, 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 un primitivo de 32 bits o más pequeño, se garantiza que una lectura que ocurre simultáneamente con una escritura producirá algún valor que la estructura realmente tenía, e incluso eso puede tener algunas peculiaridades (por ejemplo, al menos en VB.NET, una declaración como someField = New foo("george")
puede borrar someField
antes de llamar al constructor).