¿Por qué la captura de una variable estructura mutable dentro de un cierre dentro de una declaración utilizando el cambio de su comportamiento local?

StackOverflow https://stackoverflow.com/questions/4642665

Pregunta

Actualizar : Bueno, ahora me he ido y lo ha hecho: I presentó un informe de error con Microsoft sobre esto, ya que serias dudas de que se trata de un comportamiento correcto. Dicho esto, todavía no estoy 100% seguro de lo que creemos con respecto esta pregunta ; por lo que puedo ver que lo que es "correcta" está abierto a algunos nivel de interpretación.

Mi sensación es que, o bien Microsoft va a aceptar que esto es un error, o responder otra cosa que la modificación de una variable de tipo valor mutable dentro de una sentencia using constituye un comportamiento indefinido.

Además, por si sirve de algo, tengo al menos un adivinar en cuanto a lo que está pasando aquí. Sospecho que el compilador está generando una clase para el cierre, "lifting" la variable local a un campo de instancia de esa clase; y puesto que es dentro de un bloque using, se trata de hacer el campo readonly . Como LukeH señaló en un comentario a la otra pregunta , esto evitaría que las llamadas a métodos tales como MoveNext de la modificación del campo en sí (que afectarían en lugar de una copia).


Nota: He acortado esta pregunta para facilitar la lectura, a pesar de que todavía no es exactamente corta. Para la pregunta original (más larga) en su totalidad, ver el historial de edición.

He leído a través de lo que creo que son las secciones pertinentes de la ECMA-334 y parece que no puede encontrar una respuesta definitiva a esta pregunta. Voy a exponer la cuestión en primer lugar, a continuación, ofrecer un enlace con algunos comentarios adicionales para los que están interesados.

Pregunta

Si tengo un tipo de valor mutable que implementa IDisposable, que puede (1) llamar a un método que modifica el estado del valor de la variable local dentro de una declaración using y se comporta el código como lo esperan. Una vez capturo la variable en cuestión dentro de un cierre de en la declaración using, sin embargo, (2) modificaciones en el valor ya no es visible en el ámbito local son.

Este comportamiento es sólo aparente en el caso en que la variable es capturado en el interior del cierre y dentro de una instrucción using; no se desprende cuando se presente sólo una (using) o la otra condición (cierre).

¿Por qué la captura de una variable de un tipo de valor mutable dentro de un plazo de cierre de un cambio comunicado using su comportamiento local?

A continuación se presentan ejemplos de código que ilustran los puntos 1 y 2. Ambos ejemplos utilizará el siguiente tipo de valor Mutable demostración:

struct Mutable : IDisposable
{
    int _value;
    public int Increment()
    {
        return _value++;
    }

    public void Dispose() { }
}

1. La mutación de una variable de tipo de valor dentro de un bloque using

using (var x = new Mutable())
{
    Console.WriteLine(x.Increment());
    Console.WriteLine(x.Increment());
}

Las salidas de código de salida:

0
1

2. La captura de una variable de tipo de valor dentro de un cierre dentro de un bloque using

using (var x = new Mutable())
{
    // x is captured inside a closure.
    Func<int> closure = () => x.Increment();

    // Now the Increment method does not appear to affect the value
    // of local variable x.
    Console.WriteLine(x.Increment());
    Console.WriteLine(x.Increment());
}

Las salidas de código de arriba:

0
0

Comentarios adicionales

Se ha observado que el compilador Mono proporciona el comportamiento espero (cambios en el valor de la variable local son todavía visibles en el caso using + cierre). Si este comportamiento es correcto o no está claro para mí.

Para algunos más de mis pensamientos sobre este tema, véase aquí .

¿Fue útil?

Solución

Es un error conocido; descubrimos que hace un par de años. La solución sería potencialmente romper, y el problema es bastante oscuro; estos son puntos en contra de la fijación de la misma. Por lo tanto, nunca se ha dado prioridad suficientemente alta como para realmente solucionarlo.

Esto ha sido en mi cola de potenciales temas de blog para un par de años; tal vez debería escribir para arriba.

Y por cierto, su conjetura en cuanto al mecanismo que explica el error es completamente exacto; buena depuración psíquica allí.

Así que, sí, error conocido, pero gracias por el informe sin tener en cuenta!

Otros consejos

Esto tiene que ver con la forma en que los tipos de cierre se generan y utilizan. Parece que hay un error sutil en la forma CSC utiliza estos tipos. Por ejemplo, aquí es la IL generada por las GMC de Mono al invocar MoveNext ():

      IL_0051:  ldloc.3
      IL_0052:  ldflda valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> Foo/'<Main>c__AnonStorey0'::enumerator
      IL_0057:  call instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::MoveNext()

Tenga en cuenta que se carga la dirección del campo, lo que permite que la llamada a un método para modificar la instancia del tipo de valor almacenado en el objeto de cierre. Esto es lo que yo considero que es un comportamiento correcto, y esto se traduce en los contenidos de la lista empadronado muy bien.

Esto es lo que genera csc:

      IL_0068:  ldloc.3
      IL_0069:  ldfld valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> Tinker.Form1/'<>c__DisplayClass3'::enumerator
      IL_006e:  stloc.s 5
      IL_0070:  ldloca.s 5
      IL_0072:  call instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::MoveNext()

Así que en este caso se trata de tomar una copia de la instancia de tipo de valor e invocando el método en la copia. No debe ser ninguna sorpresa por qué esto se consigue en ninguna parte. El get_current () es similarmente equivocado:

      IL_0052:  ldloc.3
      IL_0053:  ldfld valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> Tinker.Form1/'<>c__DisplayClass3'::enumerator
      IL_0058:  stloc.s 5
      IL_005a:  ldloca.s 5
      IL_005c:  call instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::get_Current()
      IL_0061:  call void class [mscorlib]System.Console::WriteLine(int32)

Dado que el estado del empadronador que la copia no ha tenido MoveNext () llamada, get_current () devuelve al parecer default(int).

En resumen: csc parece ser buggy. Es interesante que el Mono tiene este derecho, mientras que MS.NET no lo hizo!

... me encantaría escuchar los comentarios de Jon Skeet en esta rareza en particular.


En una discusión con Brajkovic en #mono, determinó que la especificación del lenguaje C # en realidad no detalle ¿Cómo el tipo de cierre, debe aplicarse, ni cómo accesos de los locales que son capturados en el cierre debe se traducen. Un ejemplo de implementación de la especificación parece utilizar el método de "copia" que los usos csc. Por lo tanto, ya sea de salida del compilador puede considerarse correcta de acuerdo con la especificación del lenguaje, aunque yo diría que csc al menos debería copiar la parte posterior local para el objeto cierre después de la llamada al método.

Editar -. Esto es incorrecto, no he leído la pregunta con suficiente cuidado

Colocación de la struct en un cierre provoca una asignación. Asignaciones de tipos de valor como resultado una copia de ese tipo. Así que lo que sucede es que está creando un nuevo Enumerator<int>, y en ese Current empadronador devolverá 0.

using System;
using System.Collections.Generic;

class Program
{
    static void Main(string[] args)
    {
        List<int> l = new List<int>();
        Console.WriteLine(l.GetEnumerator().Current);
    }
}

Resultado: 0

El problema es el empadronador se almacena en otra clase para cada acción está trabajando con una copia del empadronador.

[CompilerGenerated]
private sealed class <>c__DisplayClass3
{
    // Fields
    public List<int>.Enumerator enumerator;

    // Methods
    public int <Main>b__1()
    {
        return this.enumerator.Current;
    }
}

public static void Main(string[] args)
{
    List<int> <>g__initLocal0 = new List<int>();
    <>g__initLocal0.Add(1);
    <>g__initLocal0.Add(2);
    <>g__initLocal0.Add(3);
    List<int> list = <>g__initLocal0;
    Func<int> CS$<>9__CachedAnonymousMethodDelegate2 = null;
    <>c__DisplayClass3 CS$<>8__locals4 = new <>c__DisplayClass3();
    CS$<>8__locals4.enumerator = list.GetEnumerator();
    try
    {
        if (CS$<>9__CachedAnonymousMethodDelegate2 == null)
        {
            CS$<>9__CachedAnonymousMethodDelegate2 = new Func<int>(CS$<>8__locals4.<Main>b__1);
        }
        while (CS$<>8__locals4.enumerator.MoveNext())
        {
            Console.WriteLine(CS$<>8__locals4.enumerator.Current);
        }
    }
    finally
    {
        CS$<>8__locals4.enumerator.Dispose();
    }
}

Sin la lambda el código está más cerca de lo que cabría esperar.

public static void Main(string[] args)
{
    List<int> <>g__initLocal0 = new List<int>();
    <>g__initLocal0.Add(1);
    <>g__initLocal0.Add(2);
    <>g__initLocal0.Add(3);
    List<int> list = <>g__initLocal0;
    using (List<int>.Enumerator enumerator = list.GetEnumerator())
    {
        while (enumerator.MoveNext())
        {
            Console.WriteLine(enumerator.Current);
        }
    }
}

IL específico

L_0058: ldfld valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> Machete.Runtime.Environment/<>c__DisplayClass3::enumerator
L_005d: stloc.s CS$0$0001
L_005f: ldloca.s CS$0$0001
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top