Warum erbt IEnumerator <T> von IDisposable, während der nicht generische IEnumerator dies nicht tut?

77

Ich habe festgestellt, dass das Generikum IEnumerator<T>von IDisposable erbt, die nicht generische Schnittstelle IEnumerator jedoch nicht. Warum ist es so gestaltet?

Normalerweise verwenden wir foreach-Anweisungen, um eine IEnumerator<T>Instanz zu durchlaufen . Der generierte Code von foreach hat tatsächlich einen try-finally-Block, der Dispose () in finally aufruft.

Morgan Cheng
quelle

Antworten:

84

Im Grunde war es ein Versehen. In C # 1.0, foreach nie genannt Dispose 1 . Mit C # 1.2 (eingeführt in VS2003 - es gibt keine 1.1, bizarrerweise) foreachbegann im finallyBlock zu prüfen , ob der Iterator implementiert war oder nicht IDisposable- sie mussten dies auf diese Weise tun, da eine nachträgliche IEnumeratorErweiterung IDisposabledie Implementierung aller gebrochen hätte IEnumerator. Wenn sie herausgefunden hätten, dass es nützlich ist foreach, Iteratoren überhaupt zu entsorgen, hätte ich mich sicher IEnumeratorerweitert IDisposable.

Als C # 2.0 und .NET 2.0 herauskamen, hatten sie jedoch eine neue Gelegenheit - neue Schnittstelle, neue Vererbung. Es ist viel sinnvoller, die Schnittstelle IDisposableso zu erweitern , dass Sie keine Ausführungszeitprüfung im finally-Block benötigen, und jetzt weiß der Compiler, dass der Iterator, wenn er ein Iterator ist, IEnumerator<T>einen bedingungslosen Aufruf an senden kann Dispose.

BEARBEITEN: Es ist unglaublich nützlich Dispose, am Ende der Iteration aufgerufen zu werden (wie auch immer es endet). Dies bedeutet, dass der Iterator an Ressourcen festhalten kann - was es ihm ermöglicht, beispielsweise eine Datei Zeile für Zeile zu lesen. Iteratorblöcke generieren DisposeImplementierungen, die sicherstellen, dass alle finallyBlöcke, die für den "aktuellen Ausführungspunkt" des Iterators relevant sind, ausgeführt werden, wenn er entsorgt wird. Sie können also normalen Code in den Iterator schreiben und die Bereinigung sollte angemessen erfolgen.


1 Rückblickend auf die 1.0-Spezifikation wurde sie bereits spezifiziert. Ich konnte diese frühere Aussage, dass die 1.0-Implementierung nicht aufgerufen wurde, noch nicht überprüfen Dispose.

Jon Skeet
quelle
muss ich damit rechnen, dass auch ein IEnumerable.GetEnumerator(nicht generischer) vorhanden ist IDisposable?
Shimmy Weitzhandler
1
@Shimmy: Code, der willkürliche Implementierungen von nicht generischen Objekten akzeptiert, IEnumerableist verpflichtet, sicherzustellen, dass alle von zurückgegebenen Einwegobjekte GetEnumeratorentsorgt werden. Code, der nicht als fehlerhaft angesehen werden sollte.
Supercat
@supercat: Seit ich diese Antwort geschrieben habe, habe ich festgestellt, dass sie bereits in der 1.0-Spezifikation enthalten war. Ich habe es nicht geschafft, eine 1.0-Installation zu bekommen, um zu überprüfen, ob ich tatsächlich Recht hatte oder nicht. Wird bearbeitet.
Jon Skeet
1
@JonSkeet: Jede Version von C #, in der keine DisposeLogik vorhanden war, foreachhätte sich gegenüber der VisualBasic.CollectionKlasse sehr schlecht verhalten (was in vielerlei Hinsicht ärgerlich und eigenartig ist, aber - anders als bei allen anderen Microsoft-Sammlungen dieser Zeit - das Entfernen von Elementen ermöglicht während der Aufzählung). Die CollectionKlasse vermeidet es, starke Verweise auf herausragende Enumeratoren zu speichern, und bereinigt sie, wenn sie durch Müll gesammelt werden. Wenn eine Sammlung jedoch zwischen den GC-Zyklen viele Male aufgelistet wird und diese Enumeratoren nicht bereinigt werden, wird sie sehr langsam.
Superkatze
@supercat: Das heißt natürlich nicht, dass es nicht passiert ist ...;) Deshalb möchte ich es überprüfen.
Jon Skeet
5

IEnumerable <T> erbt IDisposable nicht. IEnumerator <T> erbt jedoch IDisposable, während der nicht generische IEnumerator dies nicht tut. Selbst wenn Sie foreach für eine nicht generische IEnumerable verwenden (die IEnumerator zurückgibt), generiert der Compiler dennoch eine Prüfung auf IDisposable und ruft Dispose () auf, wenn der Enumerator die Schnittstelle implementiert.

Ich denke, der generische Enumerator <T> erbt von IDisposable, so dass keine Laufzeit-Typprüfung erforderlich ist. Er kann einfach Dispose () aufrufen, das eine bessere Leistung haben sollte, da es wahrscheinlich optimiert werden kann, wenn das Der Enumerator hat eine leere Dispose () -Methode.

Mark Cidade
quelle
Wenn der Compiler die IEnumerableat-Kompilierung so kennt , dass er den Aufruf an Disposeoptimieren kann, kann er auch eine Typprüfung optimieren.
Edward Brey
5

Ich weiß, dass dies eine alte Diskussion ist, aber ich habe erneut eine Bibliothek geschrieben, in der ich IEnumerable von T / IEnumerator von T verwendet habe, wobei Benutzer der Bibliothek benutzerdefinierte Iteratoren implementieren konnten, die nur IEnumerator von T implementieren sollten.

Ich fand es sehr seltsam, dass IEnumerator von T von IDisposable erben würde. Wir implementieren IDisposable, wenn wir unveränderte Ressourcen freigeben möchten, oder? Es wäre also nur für Enumeratoren relevant, die tatsächlich nicht verwaltete Ressourcen enthalten - wie z. B. einen E / A-Stream usw. Warum lassen Sie Benutzer nicht einfach sowohl IEnumerator of T als auch IDisposable auf ihrem Enumerator implementieren, wenn dies sinnvoll ist? In meinem Buch verstößt dies gegen das Prinzip der Einzelverantwortung - Warum Enumerator-Logik mischen und Objekte entsorgen?

JAXN
quelle
1
Wenn GetEnumeratorein Objekt zurückgegeben wird, das bereinigt werden muss (z. B. weil Datenzeilen aus einer Datei gelesen werden, die geschlossen werden muss), muss eine Entität, die weiß, wann der Enumerator nicht mehr benötigt wird, über eine Möglichkeit verfügen, diese Informationen an eine Entität zu übermitteln, die dies kann Führen Sie die Bereinigung durch. IDisposableverhält sich in Bezug auf das Liskov-Substitutionsprinzip rückwärts, da eine Fabrik, die Dinge zurückgibt, die möglicherweise bereinigt werden müssen, nicht sicher durch eine Fabrik ersetzt werden kann, die verspricht, Dinge zurückzugeben, die dies nicht tun, aber die umgekehrte Substitution wäre sicher und sollte legitim sein.
Supercat
Ich fand auch IDisposableauf IEnumerator<T>ein wenig verwirrend zu sein, ich fand es hilfreich zu überprüfen, wie die Enumeratorvon List<T>implementiertIDisposable in der .NET - Quelle. Beachten Sie, wie das Enumerator structeine DisposeMethode hat, aber es ist nichts drin. Beachten Sie, dass dieses IDisposableVerhalten in keiner Weise impliziert, dass "A List<Bitmap>sein Bitmaps in entsorgen sollte foreach! "
jrh
@supercat gute Verwendung des Liskov-Substitutionsprinzips zur Änderung des Grundsatzes der Schnittstellentrennung;)
mireazma
@mireazma: Es wäre hilfreich, wenn Containertypen eine geringfügig andere Hierarchie als Objektinstanztypen hätten, wobei Container unter anderem danach klassifizierbar wären, ob sie exklusive Referenzen besitzen, exponierte Referenzen besitzen, auf Objekte verweisen, die anderen gehören oder auf die sie verweisen Besitzerlose Objekte. Die Art und Weise, wie eine Sammlung Methoden für Dinge wie die Gleichheitsprüfung implementieren sollte, sollte von solchen Unterscheidungen abhängen, aber es gibt keine Möglichkeit für die Sprache, sie zu vermitteln.
Supercat
0

Erbt IEnumerable` IDisposing? Laut .NET Reflektor oder MSDN . Sind Sie sicher, dass Sie es nicht mit IEnumerator verwechseln ? Das verwendet IDisposing, weil es nur zum Auflisten einer Sammlung dient und nicht für die Langlebigkeit gedacht ist.

Aaron Powell
quelle
IDisposing? Meinst du auch "Nicht nach ..."?
Peter Ritchie
2
Ich habe Ihnen eine +1 gegeben, um der -1 entgegenzuwirken, da Ihre Nachricht zwischen dem Zeitpunkt, an dem das Originalposter IEnumerable falsch angegeben hat, und IEnumerator veröffentlicht wurde und wahrscheinlich das OP dazu veranlasst hat, seine Frage zu korrigieren. Da die Frage längst behoben ist, können Sie Ihre "Antwort" auch löschen, da sie sicherlich nicht mehr anwendbar ist.
Supercat
0

Es ist ein bisschen schwierig, diesbezüglich endgültig zu sein, es sei denn, Sie erhalten eine Antwort von AndersH selbst oder von jemandem, der ihm nahe steht.

Ich vermute jedoch, dass es sich um das Schlüsselwort "ield "handelt, das gleichzeitig in C # eingeführt wurde. Wenn Sie sich den vom Compiler generierten Code ansehen, wenn "Yield Return x" verwendet wird, sehen Sie die Methode in einer Hilfsklasse, die IEnumerator implementiert. Wenn IEnumerator von IDisposable abstammt, wird sichergestellt, dass er nach Abschluss der Aufzählung bereinigt werden kann.

Bevan
quelle
Mit Yield generiert der Compiler lediglich Code für eine Zustandsmaschine, für die keine Entsorgung über den normalen GC hinaus erforderlich ist
Mark Cidade,
@marxidad: Völlig falsch. Überlegen Sie, was passiert, wenn in einem Iteratorblock eine "using" -Anweisung auftritt. Siehe csharpindepth.com/Articles/Chapter6/…
Jon Skeet
@ Jon: Nicht ganz falsch. Obwohl IDisposable nicht unbedingt erforderlich ist, wenn die Verwendung nicht verwendet wird, ist es einfacher, alle neuen Enumeratoren verfügbar zu machen und Dispose () jedes Mal für alle Fälle aufzurufen.
Mark Cidade
Ich wollte meinen Kommentar umformulieren, um ihn ein bisschen weicher als "vollständig falsch" zu machen, aber die Behauptung, dass der Compiler Code generiert, der nicht entsorgt werden muss, ist IMO immer noch ungenau. Es manchmal tut, aber Ihre pauschale Aussage war falsch.
Jon Skeet
Die VisualBasic.CollectionKlasse wird eine schlechte Leistung erbringen (mehrere Größenordnungen langsamer als normal), wenn viele Enumeratoren erstellt und aufgegeben werden, ohne dass sie entsorgt werden. Die Notwendigkeit, sich Disposemit zu verbinden, IEnumerablewar somit offensichtlich, bevor die Veröffentlichung von .net 1.0, but probably late enough that changing IEnumerable` zum Erben IDisposableden Veröffentlichungsplan beeinflusst hätte.
Superkatze
-2

IIRC Die ganze Sache mit IEnumerable<T>und IEnumerableist das Ergebnis von IEnumerable.Nets Vorlagenmaterial. Ich vermute, dass Ihre Frage genauso ist.

BCS
quelle