35 votes

Les énumérations 'yield' qui ne sont pas 'terminées' par l'appelant - que se passe-t-il ?

Je suppose que j'ai

IEnumerable<string> Foo()
{
     try
     {

         /// open a network connection, start reading packets
         while(moredata)
         {
            yield return packet; 
        }
     }
     finally
      {
        // close connection 
      }
}

(Ou peut-être que j'ai fait une "utilisation" - même chose). Que se passe-t-il si mon interlocuteur va

var packet = Foo().First();

Je me retrouve avec une connexion qui fuit. Quand est-ce que l'on invoque enfin ? Ou est-ce que la bonne chose se produit toujours par magie

éditer avec la réponse et les pensées

Mon exemple et d'autres modèles d'appel "normaux" (foreach, ..) fonctionnent bien car ils se débarrassent de l'IEnumerable (en fait l'IEnumerator renvoyé par GetEnumerator). Je dois donc avoir un appelant quelque part qui fait quelque chose de funky (obtenir explicitement un énumérateur et ne pas le disposer ou autre). Je vais les faire tirer

le mauvais code

J'ai trouvé un appelant faisant

IEnumerator<T> enumerator = foo().GetEnumerator();

changé en

using(IEnumerator<T> enumerator = foo().GetEnumerator())

36voto

Servy Points 93720

Je me retrouve avec une connexion qui fuit.

Non, tu ne l'es pas.

Quand le "finally" est-il invoqué ?

Lorsque le IEnumerator<T> est disposé, ce qui First va faire après avoir obtenu le premier élément de la séquence (comme tout le monde devrait le faire lorsqu'il utilise une fonction IEnumerator<T> ).

Maintenant si quelqu'un a écrit :

//note no `using` block on `iterator`
var iterator = Foo().GetEnumerator();
iterator.MoveNext();
var first = iterator.Current;
//note no disposal of iterator

puis ils feraient fuir la ressource, mais là, le bogue se trouve dans le code de l'appelant, pas dans le bloc de l'itérateur.

27voto

dasblinkenlight Points 264350

Vous n'obtiendrez pas de fuite au niveau de la connexion. Les objets Iterator produits par yield return sont IDisposable et les fonctions LINQ font l'objet d'une attention particulière afin d'assurer une élimination appropriée.

Par exemple, First() est mis en œuvre comme suit :

public static TSource First<TSource>(this IEnumerable<TSource> source) {
    if (source == null) throw Error.ArgumentNull("source");
    IList<TSource> list = source as IList<TSource>;
    if (list != null) {
        if (list.Count > 0) return list[0];
    }
    else {
        using (IEnumerator<TSource> e = source.GetEnumerator()) {
            if (e.MoveNext()) return e.Current;
        }
    }
    throw Error.NoElements();
}

Notez que le résultat de source.GetEnumerator() est enveloppée dans using . Cela garantit que l'appel à Dispose qui, à son tour, garantit l'appel de votre code dans le fichier finally bloc.

Il en va de même pour les itérations par foreach boucle : le code garantit l'élimination de l'énumérateur, que l'énumération soit terminée ou non.

Le seul cas où vous risquez d'avoir des fuites de connexion est celui où vous appelez GetEnumerator et ne pas s'en débarrasser correctement. Cependant, il s'agit d'une erreur dans le code qui utilise la fonction IEnumerable et non dans le IEnumerable même.

22voto

theB Points 2311

Ok, cette question pourrait utiliser un peu de données empiriques.

En utilisant VS2015 et un projet scratch, j'ai écrit le code suivant :

private IEnumerable<string> Test()
{
    using (TestClass t = new TestClass())
    {
        try
        {
            System.Diagnostics.Debug.Print("1");
            yield return "1";
            System.Diagnostics.Debug.Print("2");
            yield return "2";
            System.Diagnostics.Debug.Print("3");
            yield return "3";
            System.Diagnostics.Debug.Print("4");
            yield return "4";
        }
        finally
        {
            System.Diagnostics.Debug.Print("Finally");
        }
    }
}

private class TestClass : IDisposable
{
    public void Dispose()
    {
        System.Diagnostics.Debug.Print("Disposed");
    }
}

Et l'a appelé de deux façons :

foreach (string s in Test())
{
    System.Diagnostics.Debug.Print(s);
    if (s == "3") break;
}

string f = Test().First();

Ce qui produit la sortie de débogage suivante

1
1
2
2
3
3
Finally
Disposed
1
Finally
Disposed

Comme on peut le voir, il exécute à la fois la fonction finally et le bloc Dispose méthode.

1voto

Il n'y a pas de magie spéciale. Si vous consultez la doc sur IEnumerator<T> vous verrez qu'il hérite de IDisposable . Le site foreach comme vous le savez, est un sucre syntaxique qui est décomposé par le compilateur en une séquence d'opérations sur un énumérateur, et le tout est enveloppé dans un fichier de type try / finally en appelant le bloc Dispose sur l'objet énumérateur.

Quand le compilateur convertit une méthode d'itérateur (c'est-à-dire une méthode contenant yield ) en une mise en œuvre de IEnumerable<T> / IEnumerator<T> il traite les try / finally logique dans le Dispose de la classe générée.

Vous pouvez essayer d'utiliser ILDASM pour analyser le code généré dans votre cas. Cela va être assez complexe mais cela vous donnera une idée.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X