2016-06-09 6 views
36

suppose que j'ai« rendement » énumérations qui ne reçoivent pas « fini » par l'appelant - ce qui se passe

IEnumerable<string> Foo() 
{ 
    try 
    { 

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

(Ou peut-être que je l'ai fait un « utilisant » - même chose). Que se passe-t-il si mon interlocuteur se déplace

var packet = Foo().First(); 

Je suis juste parti avec une fuite de connexion. Quand est-ce que finalement on se fait invoquer? Ou est-ce la bonne chose arrive toujours par magie

modifier avec la réponse et les pensées

Mon échantillon et d'autres « normal » (foreach, ..) les habitudes d'appel fonctionnera bien parce qu'ils disposent de la IEnumerable (en fait le IEnumerator retourné par GetEnumerator). Je dois donc avoir un appelant quelque part qui fait quelque chose de génial (obtenir explicitement un recenseur et ne pas le disposer ou similaire). Je les aurai tiré

le mauvais code de

J'ai trouvé un appelant à faire

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

changé à

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

Ma meilleure estimation: retourne seulement le premier 'packet' –

+2

@MaciejLos Cela ne répond pas à la question qu'il a posée (?). – Servy

+1

@Servy, je sais. C'est la raison pour laquelle j'ai posté un commentaire. Comme vous pouvez le voir, j'ai ajouté «?» À la fin de la déclaration, parce que je ne suis pas sûr. Merci pour votre commentaire. –

Répondre

37

Je viens de quitter avec une connexion fuite.

Non, vous n'êtes pas.

Quand est-ce que le dernier est appelé?

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 être fait quand ils utilisent un 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 

alors ils fuirait la ressource, mais le bug est dans le code de l'appelant, et non le bloc itérateur.

+0

Cela a du sens. [Documentation MSDN] (https://msdn.microsoft.com/fr-fr/library/9k7k7cf0.aspx) indique la même chose. –

27

Vous ne voudriez pas finir avec une connexion fuite. Les objets Iterator produits par yield return sont IDisposable et les fonctions LINQ veillent à assurer une mise au rebut 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 comment le résultat de source.GetEnumerator() est enveloppé dans using. Cela assure l'appel à Dispose, qui à son tour assure l'appel de votre code dans le bloc finally.

Idem pour les itérations par foreach boucle: le code assure l'élimination de l'énumérateur, que l'énumération soit terminée ou non.

Le seul cas où vous pourriez vous retrouver avec une fuite de connexion est lorsque vous appelez le GetEnumerator et que vous ne parvenez pas à vous en débarrasser correctement. Cependant, il s'agit d'une erreur dans le code en utilisant IEnumerable, pas dans le IEnumerable lui-même.

22

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 puis appelé deux façons:

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

string f = Test().First(); 

qui produit la sortie de débogage suivant

1 
1 
2 
2 
3 
3 
Finally 
Disposed 
1 
Finally 
Disposed 

Comme nous pouvons le voir, il exécute à la fois le bloc finally et le Dispose méthode.

+4

en cas de doute écrire un programme de test :-) @ ty – pm100

+0

PM100 Pour ajouter un peu plus d'info: j'ai précisé que j'utilisais le compilateur 2015, mais je soupçonne que les règles de détermination de la portée de la spécification C# nécessitent ce comportement. Je soupçonne que terminer un recenseur agit implicitement comme si le dernier rendement était simplement un rendement, et toutes les règles de rendement normales s'appliquent. – theB

1

Il n'y a rien de magique. Si vous vérifiez le document sur IEnumerator<T>, vous trouverez qu'il hérite de IDisposable. La construction foreach, comme vous le savez, est du 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 bloc try/finally, appelant l'objet Dispose sur l'énumérateur.

Lorsque le compilateur convertit un procédé d'itération (i. E de la méthode. yield contenant états) dans une mise en œuvre de IEnumerable<T>/IEnumerator<T>, il gère la logique try/finally dans le procédé Dispose de la classe générée.

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