Warum kann IEnumerator nicht geklont werden?

8

Bei der Implementierung eines grundlegenden Scheme-Interpreters in C # entdeckte ich zu meinem Entsetzen das folgende Problem:

IEnumerator hat keine Klonmethode! (Genauer gesagt, IEnumerable kann mir keinen "klonbaren" Enumerator zur Verfügung stellen).

Was ich möchte:

%Vor%

Ich kann keine IEnumerable-Implementierung finden, die keinen effizient klonbaren IEnumerator liefern könnte (Vektoren, verknüpfte Listen usw.), die alle eine triviale Implementierung von IEnumerators Clone () liefern könnten, wie oben beschrieben. Es wäre einfacher als eine Reset () -Methode zur Verfügung zu stellen!).

Das Fehlen der Clone-Methode bedeutet, dass jede funktionale / rekursive Idiom der Aufzählung über eine Sequenz nicht funktioniert.

Es bedeutet auch, dass ich IEnumerables nicht "nahtlos" wie Lisp-Listen (für die Sie readcursiv mit auto / cdr auflisten) machen kann. die einzige Implementierung von "(cdr einige IEnumerable )" wäre wohl ineffizient.

Kann jemand ein realistisches, nützliches Beispiel für ein IEnumerable-Objekt vorschlagen, das keine effiziente "Clone ()" - Methode bereitstellen kann? Gibt es ein Problem mit dem "Yield" -Konstrukt?

Kann jemand eine Abhilfe vorschlagen?

    
Paul Hollingsworth 30.04.2009, 16:58
quelle

8 Antworten

23

Die Logik ist unerbittlich! IEnumerable unterstützt nicht Clone , und Sie benötigen Clone , Sie sollten also nicht IEnumerable verwenden.

Oder genauer gesagt, Sie sollten es nicht als grundlegende Grundlage für die Arbeit an einem Scheme-Interpreter verwenden. Warum nicht stattdessen eine triviale unveränderliche verknüpfte Liste erstellen?

%Vor%

Beachten Sie, dass die Methode ToEnumerable Ihnen eine bequeme Verwendung im Standard C # -Weg bietet.

Um Ihre Frage zu beantworten:

  

Kann jemand ein realistisches vorschlagen,   nützlich, Beispiel eines IEnumerable   Objekt, das nicht in der Lage wäre   Bereitstellung einer effizienten "Clone ()" Methode?   Liegt es daran, dass es ein Problem geben würde?   das "Ausbeute" -Konstrukt?

Ein IEnumerable kann überall auf der Welt Daten abrufen. Hier ist ein Beispiel, das Zeilen von der Konsole liest:

%Vor%

Es gibt zwei Probleme damit: Erstens wäre eine Clone -Funktion nicht besonders einfach zu schreiben (und Reset wäre bedeutungslos). Zweitens ist die Reihenfolge unendlich - was absolut zulässig ist. Sequenzen sind faul.

Ein anderes Beispiel:

%Vor%

Für beide Beispiele würde die von Ihnen akzeptierte "Problemumgehung" nicht viel nutzen, da sie nur den verfügbaren Speicher erschöpft oder für immer aufhängt. Aber das sind absolut gültige Beispiele für Sequenzen.

Um C # - und F # -Sequenzen zu verstehen, müssen Sie Listen in Haskell betrachten, nicht Listen in Scheme.

Wenn Sie denken, das Unendliche ist ein Ablenkungsmanöver, wie wäre es mit dem Lesen der Bytes von einem Sockel:

%Vor%

Wenn eine bestimmte Anzahl von Bytes über den Socket gesendet wird, ist dies keine unendliche Sequenz. Und es wäre sehr schwierig, Clone dafür zu schreiben. Wie würde der Compiler die IEnumerable-Implementierung generieren, um dies automatisch zu tun?

Sobald ein Klon erstellt wurde, müssten beide Instanzen nun von einem gemeinsamen Puffersystem aus arbeiten. Es ist möglich, aber in der Praxis wird es nicht benötigt - so werden diese Sequenzen nicht verwendet. Sie behandeln sie rein "funktional", wie Werte, und wenden Filter rekursiv auf sie an, anstatt sich "zwingend" an einen Ort innerhalb der Sequenz zu erinnern. Es ist ein wenig sauberer als Low-Level car / cdr Manipulation.

Weitere Frage:

  

Ich frage mich, was ist das niedrigste Niveau   "primitive (s)" würde ich so brauchen   alles, was ich mit einem tun könnte   IEnumerable in meinem Scheme-Interpreter   könnte eher in schema implementiert werden   als als ein Built-in.

Die kurze Antwort, denke ich, wäre, in Abelson und Sussman und in insbesondere der Teil über Streams . IEnumerable ist ein Stream, keine Liste. Und sie beschreiben, wie Sie spezielle Versionen von Map, Filter, Accumulate usw. benötigen, um mit ihnen zu arbeiten. Sie kommen auch auf die Idee der Vereinigung von Listen und Streams in Abschnitt 4.2.

    
Daniel Earwicker 30.04.2009, 17:28
quelle
4

Um dieses Problem zu umgehen, können Sie problemlos eine Erweiterungsmethode für IEnumerator erstellen, mit der Sie klonen können. Erstellen Sie einfach eine Liste aus dem Enumerator und verwenden Sie die Elemente als Mitglieder.

Sie würden jedoch die Streaming-Funktionen eines Enumerators verlieren, da Sie als neuer "Klon" den ersten Enumerator vollständig auswerten würden.

    
Reed Copsey 30.04.2009 17:03
quelle
2

Wenn Sie den ursprünglichen Enumerator loslassen können, z. Verwenden Sie es nicht mehr, Sie können eine "Klon" -Funktion implementieren, die den ursprünglichen Enumerator verwendet und sie als Quelle für einen oder mehrere Enumeratoren verwendet.

Mit anderen Worten, Sie könnten so etwas aufbauen:

%Vor%

Diese könnten intern den ursprünglichen Enumerator und eine verknüpfte Liste teilen, um die aufgezählten Werte zu verfolgen.

Dies würde ermöglichen:

  • Unendliche Sequenzen, solange beide Enumeratoren Fortschritte machen (die verkettete Liste würde so geschrieben werden, dass, sobald beide Enumeratoren einen bestimmten Punkt passiert haben, diese GC'ed sein können)
  • Lazy-Enumeration, der erste der beiden Enumeratoren, die einen Wert benötigen, der noch nicht vom ursprünglichen Enumerator abgerufen wurde, würde ihn erhalten und in der verknüpften Liste speichern, bevor er ihn liefert

Problem hier ist natürlich, dass es immer noch viel Speicher benötigen würde, wenn einer der Enumeratoren sich weit vor dem anderen bewegen würde.

Hier ist der Quellcode. Wenn Sie Subversion verwenden, können Sie die Visual Studio 2008-Lösungsdatei mit einer Klassenbibliothek mit dem unten stehenden Code sowie einem separaten Unit-Testprojekt herunterladen.

Repository: Ссылка Benutzername und Passwort sind beide 'Gast', ohne die Anführungszeichen.

Beachten Sie, dass dieser Code überhaupt nicht Thread-sicher ist.

%Vor%     
quelle
1

Dies verwendet die Reflektion zum Erstellen einer neuen Instanz und legt dann die Werte für die neue Instanz fest. Ich fand auch dieses Kapitel aus C # in Depth sehr nützlich. Iterator-Block-Implementierungsdetails: Automatisch generierte Status-Computer

%Vor%

Diese Antwort wurde auch für die folgende Frage verwendet: Ist es möglich, eine IEnumerable-Instanz zu klonen und eine Kopie des Iterationsstatus zu speichern?

    
BenMaddox 23.02.2010 01:51
quelle
0

Warum nicht als eine Erweiterungsmethode:

%Vor%

Dies würde grundsätzlich einen neuen Enumerator erstellen und zurückgeben, ohne das Original vollständig auszuwerten.

Edit: Ja, ich habe es falsch gelesen. Paul hat Recht, das würde nur mit IEnumerable funktionieren.

    
Aaron 30.04.2009 17:08
quelle
0

Das könnte helfen. Es benötigt etwas Code, um das Dispose () auf dem IEnumerator aufzurufen:

%Vor%     
tofi9 30.04.2009 22:20
quelle
0

Der Zweck von "clonable" -Enumeratoren besteht hauptsächlich darin, die Iterationsposition speichern zu können und später zu ihr zurückkehren zu können. Das bedeutet, dass der iterierte Container eine reichere Schnittstelle als nur IEnumerable bereitstellen muss. Es ist eigentlich etwas zwischen IEnumerable und IList . Wenn Sie mit IList arbeiten, können Sie den Integer-Index als Enumerator verwenden oder eine einfache unveränderliche Wrapping-Klasse erstellen, die einen Verweis auf die Liste und die aktuelle Position enthält.

Wenn Ihr Container keinen Direktzugriff unterstützt und nur vorwärts iteriert werden kann (wie eine unidirektionale verknüpfte Liste), muss er mindestens die Möglichkeit bieten, das nächste Element zu erhalten, einen Verweis auf den vorherigen oder auf einen "Iterationsstatus" "dass Sie in Ihrem Iterator halten können. So kann die Schnittstelle wie folgt aussehen:

%Vor%

Beachten Sie, dass der Iterator unveränderlich ist, er kann nicht "vorwärts" bewegt werden, wir können nur unseren iterierbaren Container bitten, uns einen neuen Iterator zu geben, der auf die nächste Position zeigt. Der Vorteil davon ist, dass Sie Iteratoren beliebig lange speichern können, zum Beispiel einen Stapel von Iteratoren haben und bei Bedarf zur zuvor gespeicherten Position zurückkehren können. Sie können die aktuelle Position für die spätere Verwendung speichern, indem Sie sie einer Variablen zuweisen, genau wie bei einem Integer-Index.

Die AllRest -Eigenschaft kann nützlich sein, wenn Sie von der angegebenen Position zum Ende des Containers mit den Standardsprachen-Iterationsfunktionen wie foraech oder LinQ iterieren müssen. Es wird die Iteratorposition nicht ändern (denken Sie daran, unser Iterator ist unveränderlich). Die Implementierung kann wiederholt GetNext und yleid return .

Die Methode GetNext kann tatsächlich ein Teil des Iterators selbst sein, so:

%Vor%

Das ist ziemlich genau so. Die Logik zum Bestimmen des nächsten Zustands wird gerade von der Container-Implementierung in den Iterator verschoben Implementierung. Beachten Sie, dass der Iterator immer noch unveränderlich ist. Sie können es nicht "vorwärts bewegen", Sie können nur einen anderen bekommen und auf das nächste Element zeigen.

    
C-F 13.04.2016 23:19
quelle
-2

Es gibt bereits eine Möglichkeit, einen neuen Enumerator zu erstellen - genauso wie Sie den ersten Enumerator erstellt haben: IEnumerable.GetEnumerator. Ich bin mir nicht sicher, warum Sie einen anderen Mechanismus brauchen, um das Gleiche zu tun.

Und im Geiste des DRY-Prinzips bin ich neugierig, warum Sie möchten, dass die Verantwortung für das Erstellen neuer IEnumerator-Instanzen sowohl in Ihrer aufzählbaren als auch in Ihrer Aufzählungsklasse dupliziert wird. Sie würden den Enumerator zwingen, einen zusätzlichen Status zu behalten, der über das hinausgeht, was erforderlich ist.

Stellen Sie sich beispielsweise einen Enumerator für eine verknüpfte Liste vor. Für die grundlegende Implementierung von IEnumerable muss diese Klasse nur einen Verweis auf den aktuellen Knoten beibehalten. Um Ihren Klon zu unterstützen, müssen Sie aber auch einen Verweis auf den Kopf der Liste behalten - etwas, für das er ansonsten nicht gebraucht wird *. Warum sollten Sie diesen zusätzlichen Status dem Enumerator hinzufügen, wenn Sie einfach zur Quelle (IEnumerable) gehen und einen anderen Enumerator erhalten können?

Und warum würden Sie die Anzahl der Codepfade verdoppeln, die Sie testen müssen? Jedes Mal, wenn Sie einen neuen Weg zur Herstellung eines Objekts machen, fügen Sie Komplexität hinzu.

* Sie würden auch den Kopfzeiger benötigen, wenn Sie Reset implementiert haben, aber gemäß den Dokumenten , Reset ist nur für COM Interop, und Sie können eine NotSupportedException werfen.

    
Joe White 30.04.2009 17:27
quelle

Tags und Links