Perché l'acquisizione di una variabile struct mutevole all'interno di una chiusura all'interno di un cambiamento economico con il suo comportamento locale?

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

Domanda

Aggiorna : Bene, ora sono andato e fatto: I ha presentato un bug report con Microsoft su questo, come ho seri dubbi che si tratta di un comportamento corretto. Detto questo, non sono ancora sicuro al 100% che cosa credere riguardo questa domanda ; così posso vedere che ciò che è "giusto" è aperta a alcuni livello di interpretazione.

La mia sensazione è che Microsoft accetterà che si tratta di un bug, oppure rispondere che la modifica di una variabile di tipo di valore mutevole all'interno di un'istruzione using costituisce comportamento non definito.

Inoltre, per quel che vale, ho almeno una indovinate di ciò che sta accadendo qui. Ho il sospetto che il compilatore genera una classe per la chiusura, "sollevamento", la variabile locale ad un campo di quella classe esempio; e dato che è all'interno di un blocco using, Sta facendo il campo readonly . Come LukeH sottolineato nel un commento per l'altra domanda , ciò impedirebbe metodo chiama come MoveNext di modificare il campo stesso (che avrebbero invece influenzare una copia).


Nota: ho accorciato questa domanda per migliorare la leggibilità, anche se è ancora non esattamente breve. Per la (più lungo) questione nella sua interezza originale, vedere la storia di modifica.

Ho letto attraverso quello che io credo sono le sezioni del ECMA-334 e non riesco a trovare una risposta definitiva a questa domanda. Io stato il problema, poi fornire un link ad alcuni commenti aggiuntivi per coloro che sono interessati.

Domanda

Se ho un tipo di valore mutabile che implementa IDisposable, posso (1) chiamare un metodo che modifica lo stato del valore della variabile locale all'interno di un'istruzione using e si comporta in codice, come mi aspetto. Una volta mi cattura la variabile in questione all'interno di una chiusura a nel la dichiarazione using, tuttavia, (2) le modifiche al valore non è più visibile in ambito locale sono.

Questo comportamento è evidente solo nel caso in cui la variabile viene catturato dentro la chiusura e all'interno di un'istruzione using; non è evidente quando è presente un solo (using) o l'altra condizione (chiusura).

Perché l'acquisizione di una variabile di un tipo di valore mutevole all'interno di una chiusura all'interno di un cambiamento dichiarazione using suo comportamento locale?

Questi sono esempi di codice che illustrano gli articoli 1 e 2. Entrambi gli esempi utilizzerà il seguente tipo di dimostrazione valore Mutable:

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

    public void Dispose() { }
}

1. Mutando una variabile di tipo valore all'interno di un blocco using

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

Le uscite di codice di uscita:

0
1

2. Catturare una variabile di tipo valore all'interno di una chiusura all'interno di un blocco 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());
}

Le uscite sopra codice:

0
0

Ulteriori commenti

E 'stato osservato che il compilatore Mono fornisce il comportamento mi aspetto (le modifiche al valore della variabile locale sono ancora visibili nel using + caso di chiusura). Se questo comportamento è corretto o non è chiaro per me.

Per un po 'dei miei pensieri su questo tema, si veda qui .

È stato utile?

Soluzione

E 'un bug noto; abbiamo scoperto che un paio di anni fa. La correzione sarebbe potenzialmente la rottura, e il problema è piuttosto oscuro; questi sono punti contro risolverlo. Pertanto non è mai stata la priorità alta abbastanza per risolvere il problema in realtà.

Questo è stato nella mia coda di potenziali argomenti blog per un paio di anni ormai; forse dovrei scrivere su.

E per inciso, la congettura per quanto riguarda il meccanismo che spiega il bug è completamente accurato; bello il debug psichica lì.

Quindi, sì, noto bug, ma grazie per il rapporto a prescindere!

Altri suggerimenti

Questo ha a che fare con il modo in cui i tipi di chiusura sono generati e utilizzati. Sembra che ci sia una sottile bug nel modo in cui CSC utilizza questi tipi. Ad esempio, qui è la IL generato da GMCs di Mono quando si richiama 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()

Si noti che sta caricando indirizzo del campo, che consente la chiamata metodo per modificare l'istanza del tipo valore memorizzato sull'oggetto chiusura. Questo è ciò che io considero essere un comportamento corretto, e ciò comporta il contenuto della lista viene enumerato bene.

Ecco cosa csc genera:

      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()

Quindi in questo caso sta prendendo una copia dell'istanza tipo di valore e invocando il metodo sulla copia. Dovrebbe essere una sorpresa perché questo ti porta da nessuna parte. La chiamata GET_CURRENT () è simile sbagliato:

      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)

Poiché lo stato del enumeratore che sta copiando, non ha avuto il MoveNext () chiamato, GET_CURRENT () restituisce apparentemente default(int).

In breve: csc sembra essere buggy. E 'interessante che Mono ha questo diritto, mentre MS.NET no!

... mi piacerebbe sentire i commenti di Jon Skeet su questo particolare stranezza.


In una discussione con Brajkovic in #mono, ha stabilito che le specifiche del linguaggio C # in realtà non dettaglio come il tipo di chiusura dovrebbe essere realizzata, né come accessi di gente del posto che vengono catturati nella chiusura dovrebbe vengono tradotti. Un esempio di implementazione nelle specifiche sembra utilizzare il metodo "copia" che utilizza CSC. Pertanto uscita sia del compilatore può essere considerata corretta secondo le specifiche del linguaggio, anche se direi che la CSC dovrebbe almeno copiare sul retro locale all'oggetto di chiusura dopo la chiamata al metodo.

Modifica -. Questo non è corretto, non ho letto la questione con sufficiente attenzione

Posizionamento della struct in una chiusura provoca un incarico. Assegnazioni di tipi di valore si traducono in una copia del tipo. Così che cosa sta accadendo è che si sta creando un nuovo Enumerator<int>, e Current su quel enumeratore restituirà 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);
    }
}

Risultato: 0

Il problema è che l'enumeratore è memorizzato in un'altra classe così ogni azione sta lavorando con una copia della enumeratore.

[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();
    }
}

Senza lambda il codice è più vicino a quello che ci si aspetterebbe.

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 specifico

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
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top