2015-07-23 18 views
34

Czy ktoś mógłby wyjaśnić, dlaczego ten kod działa w pętli nieskończoności? Dlaczego MoveNext() zwraca zawsze true?Dziwne zachowanie Enumerator.MoveNext()

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

Odpowiedz

40

List<T>.GetEnumerator() zwraca wartość typu zmienny (List<T>.Enumerator). Przechowujesz tę wartość w anonimowym typie.

Teraz rzućmy okiem na to, co to robi:

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

to odpowiednik:

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

    // Original loop body 
} 

Teraz pamiętać, co mamy wywołanie MoveNext() on - kopii wartości który jest w typie anonimowym. Nie możesz zmienić wartości w anonimowym typie - wszystko, co masz, to właściwość, którą możesz wywołać, co da ci kopię wartości.

W przypadku zmiany kodu do:

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

... wtedy będziesz skończyć uzyskanie odniesienie w anonimowym typu. Odniesienie do pola zawierającego zmienną wartość. Kiedy zadzwonisz pod numer MoveNext(), wartość wewnątrz tego pola zostanie zmutowana, więc zrobi to, co chcesz.

Do analizy w bardzo podobnej sytuacji (ponownie przy użyciu List<T>.GetEnumerator()) patrz my 2010 blog post "Iterate, damn you!".

+3

Patrzę na to i mam problem z ustaleniem, który element jest tutaj typem wartości, dla którego rozróżnienie kopii/odniesienia faktycznie ma znaczenie. –

+1

Nie dostałem go, jak to działa dobrze, jeśli rzucimy go na '' IEnumerable ''? @JonSkeet można wyjaśnić więcej w prostych słowach –

+1

@ EhsanSajjad: Gdy typem kompilacji 'TempList' jest' IEnumerable ', dostęp do niego po prostu kopiuje odniesienie. Działanie zgodnie z tym odniesieniem zmutuje wartość pudełkową, a następnym razem, gdy skopiujesz odniesienie, nadal będzie ona odnosić się do tej samej wartości pudełkowej, więc widzisz zmianę. Porównaj to z kopiowaniem wartości typu wartości i mutowaniem tej kopii - gdy * następnie * skopiujesz oryginalną wartość, nie zobaczysz poprzedniej zmiany. –

3

Chociaż foreach konstrukt C# i pętla For Each w VB są często używane w rodzaju, które wdrażają IEnumerable<T> będą przyjmować każdego rodzaju, która zawiera GetEnumerator sposób, którego typ stanowi powrót do odpowiedniej MoveNext funkcji i Current własności. Po zwróceniu wartości typu GetEnumerator typ wartości w wielu przypadkach pozwoli na bardziej efektywne wdrożenie foreach niż byłoby to możliwe, gdyby zwrócono IEnumerator<T>.

Niestety, ponieważ nie ma sposobu, dzięki któremu typu może dostarczyć wartość typu wyliczający przy wywołaniu z foreach bez również dostarczanie jednego gdy wywoływany przez wywołanie GetEnumerator metody, autorzy List<T> obliczu niewielki kompromis wydajności a semantyka. W tym czasie, ponieważ C# nie obsługiwał wnioskowania o zmiennym typie, każdy kod wykorzystujący wartość zwróconą z List<T>.GetEnumerator musiałby zadeklarować zmienną typu IEnumerator<T> lub List<T>.Enumerator. Kod wykorzystujący poprzedni typ zachowywałby się tak, jakby typ referencyjny był List<T>.Enumerator, a programista używający tego ostatniego mógł domniemywać, że był to typ struktury. Jednak w przypadku wnioskowania typu C# założenie to przestało obowiązywać. Kod może bardzo łatwo skończyć się przy użyciu typu List<T>.Enumerator bez znajomości przez programistę istnienia tego typu.

Jeśli C# kiedykolwiek zdefiniował atrybut metody struct, który mógłby zostać użyty do oznaczania metod, których nie można zarezerwować w strukturach tylko do odczytu, i gdyby użył ich List<T>.Enumerator, kod taki jak twój mógłby prawidłowo błąd kompilacji podczas wywołania do MoveNext, raczej powodujący fałszywe zachowanie. Nie znam jednak żadnych konkretnych planów dodania takiego atrybutu.