2015-07-23 22 views
34

Könnte jemand erklären, warum dieser Code in der Endlosschleife läuft? Warum MoveNext() immer true zurückgeben?Merkwürdiges Verhalten von Enumerator.MoveNext()

var x = new { TempList = new List<int> { 1, 3, 6, 9 }.GetEnumerator() }; 
while (x.TempList.MoveNext()) 
{ 
    Console.WriteLine("Hello World"); 
} 

Antwort

40

List<T>.GetEnumerator() liefert eine veränderbare Werttyp (List<T>.Enumerator). Sie speichern diesen Wert im anonymen Typ.

Nun lassen Sie uns einen Blick auf das, was das bedeutet:

while (x.TempList.MoveNext()) 
{ 
    // Ignore this 
} 

Das äquivalent ist:

while (true) 
{ 
    var tmp = x.TempList; 
    var result = tmp.MoveNext(); 
    if (!result) 
    { 
     break; 
    } 

    // Original loop body 
} 

Nun, was beachten wir fordern MoveNext() auf - die Kopie des Wertes Das ist im anonymen Typ. Sie können den Wert im anonymen Typ tatsächlich nicht ändern - Sie können nur eine Eigenschaft aufrufen, die Ihnen eine Kopie des Wertes liefert.

Wenn Sie den Code ändern:

var x = new { TempList = (IEnumerable<int>) new List<int> { 1, 3, 6, 9 }.GetEnumerator() }; 

... dann werden Sie eine Referenz im anonymen Typ am Ende immer. Ein Verweis auf eine Box, die den veränderbaren Wert enthält. Wenn Sie MoveNext() für diese Referenz aufrufen, wird der Wert innerhalb der Box mutiert, so dass es tun wird, was Sie wollen.

Zur Analyse einer sehr ähnlichen Situation (wieder unter Verwendung List<T>.GetEnumerator()) siehe my 2010 blog post "Iterate, damn you!".

+3

Ich schaue mir das an und habe Probleme herauszufinden, welches Element hier ein Werttyp ist, für den der Kopier-/Referenzunterschied tatsächlich einen Unterschied macht. –

+1

Ich habe es nicht verstanden, wie es funktioniert, wenn wir es auf 'IEnumerable ' '?? @ JonSkeet können Sie es mehr in einfachen Worten zu erklären –

+1

@EhsanSajjad: Wenn der Compile-Time-Typ von 'TempList' ist IEnumerable ', Zugriff darauf kopiert nur eine Referenz. Wenn Sie auf diese Referenz einwirken, wird der Boxed-Wert mutiert, und wenn Sie das nächste Mal eine Referenz kopieren, wird immer noch auf denselben Boxed-Wert Bezug genommen, sodass Sie die Änderung sehen. Vergleichen Sie das mit dem Kopieren des Werts des Werttyps und der Mutation dieser Kopie - wenn Sie * next * den ursprünglichen Wert kopieren, wird die vorherige Änderung nicht angezeigt. –

3

Während die foreach Konstrukts in C# und die For Each Schleife in VB.NET werden häufig mit Typen verwendet, die IEnumerable<T> implementieren, werden sie jede Art annehmen, die eine GetEnumerator Methode, deren Rückgabetyp umfasst eine zutreffende MoveNext Funktion und Current Eigenschaft. Wenn Sie einen GetEnumerator Rückgabewert zurückgeben, wird in vielen Fällen die Implementierung von foreach effizienter, als dies bei einer Rückgabe von IEnumerator<T> möglich wäre.

Leider, denn es gibt kein Mittel, mit dem ein Typ Wert-Typ Enumerator liefern kann, wenn sie von foreach aufgerufen, ohne auch eine Versorgung, wenn sie von einem GetEnumerator Methodenaufruf aufgerufen, die Autoren von List<T> konfrontiert einen leichten Kompromiß der Leistung versus Semantik. Da C# zu diesem Zeitpunkt keine Inferenz vom Typ "Variable" unterstützt, müsste jeder Code, der den von List<T>.GetEnumerator zurückgegebenen Wert verwendet, eine Variable vom Typ IEnumerator<T> oder List<T>.Enumerator deklarieren. Code, der den früheren Typ verwendet, würde sich so verhalten, als ob List<T>.Enumerator ein Referenztyp wäre, und ein Programmierer, der letzteres verwendet, könnte annehmen, dass es sich um einen Strukturtyp handelt. Wenn C# Hinzufügung vom Typ hinzugefügt wurde, hörte diese Annahme jedoch auf zu halten. Code könnte sehr einfach mit dem Typ List<T>.Enumerator enden, ohne dass der Programmierer von der Existenz dieses Typs weiß.

Wenn C# jemals ein struct-method-Attribut definieren würde, das zum Markieren von Methoden verwendet werden könnte, die in schreibgeschützten Strukturen nicht aufrufbar sein sollten, und wenn List<T>.Enumerator davon Gebrauch macht, könnte Code wie Ihr ordnungsgemäß a liefern Kompilierzeitfehler beim Aufruf an MoveNext eher das ergebende Scheinverhalten. Ich kenne jedoch keine besonderen Pläne, ein solches Attribut hinzuzufügen.

Verwandte Themen