Pourquoi ne capturer une variable struct mutable l'intérieur d'une fermeture dans un changement de déclaration en utilisant son comportement local?

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

Question

Mise à jour : Eh bien, maintenant que je suis allé et fait: je a déposé un rapport de bogue avec Microsoft à ce sujet, comme je doute sérieusement que ce comportement est correct. Cela dit, je ne suis toujours pas sûr à 100% ce qu'il faut croire en ce qui concerne cette question ; donc je peux voir que ce qui est « correct » est ouvert à certains niveau d'interprétation.

Mon sentiment est que soit Microsoft accepte que ce soit un bug, ou bien répondre que la modification d'une variable de type valeur mutables dans une instruction using constitue un comportement non défini.

En outre, pour ce que ça vaut, j'ai au moins un deviner à ce qui se passe ici. Je pense que le compilateur génère une classe de fermeture « lifting », la variable locale à un champ d'instance de cette classe; et comme il est dans un bloc de using, il est fait le champ readonly . Comme LukeH souligné dans un commentaire à l'autre question , cela empêcherait les appels de méthode tels que MoveNext de modifier le champ lui-même (ils seraient plutôt affecter une copie).


Note: J'ai raccourci cette question pour une meilleure lisibilité, mais il est toujours pas exactement court. Pour l'original (plus) question dans son intégralité, voir l'historique des modifications.

J'ai lu ce que je crois, sont les sections pertinentes du ECMA-334 et ne peut pas sembler trouver une réponse concluante à cette question. Je dirai d'abord la question, puis fournir un lien vers quelques commentaires supplémentaires pour ceux qui sont intéressés.

Question

Si j'ai un type de valeur mutables qui implémente IDisposable, je peux (1) appeler une méthode qui modifie l'état de la valeur de la variable locale dans une instruction using et le code se comporte comme je le pense. Une fois que je capture la variable en question à l'intérieur d'une fermeture dans les l'instruction using, cependant, (2) des modifications à la valeur ne sont plus visibles dans la portée locale.

Ce comportement est évident que dans le cas où la variable est capturée dans la fermeture et dans une instruction using; il ne ressort pas lorsque seule une (using) ou l'autre état (fermeture) est présent.

Pourquoi est-ce la capture d'une variable d'un type de valeur mutables à l'intérieur d'une fermeture dans une instruction using changer son comportement local?

Voici des exemples de code illustrant les points 1 et 2. Les deux exemples utiliseront le type de démonstration de valeur Mutable suivante:

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

    public void Dispose() { }
}

1. Une variable de muter type de valeur dans un bloc using

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

Les sorties de code de sortie:

0
1

2. La capture d'une variable de type de valeur à l'intérieur d'une fermeture dans un bloc de 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());
}

Les sorties de code ci-dessus:

0
0

Autres commentaires

Il a été noté que le compilateur Mono fournit le comportement que je pense (modifications de la valeur de la variable locale sont encore visibles dans le using + cas de fermeture). Que ce comportement est correct ou non est pas clair pour moi.

Pour un peu plus de mes réflexions sur cette question, voir ici .

Était-ce utile?

La solution

Il est un bug connu; nous avons découvert il y a quelques années. La solution serait potentiellement briser, et le problème est assez obscur; ce sont des points contre le fixer. Par conséquent, il n'a jamais été une priorité suffisamment élevée pour réellement résoudre.

Cela a été dans ma file d'attente de sujets de blog potentiels pour quelques années; peut-être que je devrais écrire vers le haut.

Et d'ailleurs, votre conjecture sur le mécanisme qui explique le bug est tout à fait exact; agréable débogage psychique, il.

Alors, oui, bug connu, mais merci pour le rapport, quelle que soit!

Autres conseils

Cela a à voir avec la façon dont les types de fermeture sont générés et utilisés. Il semble y avoir un bug subtil dans la façon dont csc utilise ces types. Par exemple, voici l'IL généré par les gmcs de Mono lors de l'appel 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()

Notez qu'il est chargement de l'adresse du champ, ce qui permet l'appel de méthode pour modifier l'instance du type à valeur stockée sur l'objet de fermeture. Voilà ce que je considère être un comportement correct, et cela se traduit par le contenu de la liste recensée très bien.

Voici ce que génère 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()

Donc, dans ce cas, il prend une copie de l'instance de type de valeur et d'appeler la méthode sur la copie. Il ne devrait pas être surprenant pourquoi cela vous mène nulle part. L'appel GET_CURRENT () est tout aussi mal:

      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)

Depuis l'état du recenseur qu'elle copie n'a pas eu MoveNext () appelée, GET_CURRENT () retourne apparemment default(int).

En bref: csc semble être buggy. Il est intéressant que Mono a ce droit en MS.NET n'a pas!

... J'aimerais entendre les commentaires de Jon Skeet sur cette bizarrerie particulière.


Dans une discussion avec Brajkovic dans #mono, il a déterminé que la spécification du langage C # ne fait pas de détails comment le type de fermeture doit être mis en œuvre, ni comment les accès de la population locale qui sont capturés dans la fermeture devrait se traduit. Un exemple d'implémentation dans la spécification semble utiliser la méthode « copie » qui utilise des csc. Par conséquent, la sortie soit compilateur peut être considéré comme correct selon la spécification du langage, mais je dirais que le SCC devrait au moins copier l'arrière locale à l'objet de fermeture après l'appel de la méthode.

EDIT -. Ceci est incorrect, je ne l'ai pas lu la question assez attentivement

Placer la struct dans une fermeture provoque une affectation. Missions sur les types de valeurs se traduisent par une copie du type. Alors qu'est-ce qui se passe est que vous créez une nouvelle Enumerator<int> et Current sur ce recenseur retournera 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);
    }
}

Résultat: 0

Le problème est le recenseur est stocké dans une autre classe pour chaque action travaille avec une copie du recenseur.

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

Sans lambda le code est plus proche de ce que vous attendez.

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 spécifique

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
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top