2009-04-22 13 views
33

Jak rozumiem, zmienna iteracyjna C# na foreach jest niezmienna.Dlaczego zmienna Iteracyjna w instrukcji C# foreach jest tylko do odczytu?

Oznacza to, że nie można modyfikować iteracyjnej tak:

foreach (Position Location in Map) 
{ 
    //We want to fudge the position to hide the exact coordinates 
    Location = Location + Random();  //Compiler Error 

    Plot(Location); 
} 

nie można zmodyfikować zmienną iteracyjnej bezpośrednio i zamiast tego konieczne użycie pętli

for (int i = 0; i < Map.Count; i++) 
{ 
    Position Location = Map[i]; 
    Location = Location + Random(); 

    Plot(Location);   
    i = Location; 
} 

Coming z tła C++ widzę foreach jako alternatywę dla pętli for. Ale z powyższym ograniczeniem, zwykle używam pętli for.

Jestem ciekawy, jakie jest uzasadnienie dla niezmienności iteratora?


Edit:

To pytanie jest bardziej pytanie ciekawość, a nie jako problem kodowania. Doceniam odpowiedzi na kodowanie, ale nie mogę ich oznaczyć jako odpowiedzi.

Również powyższy przykład został nadmiernie uproszczony. Oto przykład C++, co chcę zrobić:

// The game's rules: 
// - The "Laser Of Death (tm)" moves around the game board from the 
//  start area (index 0) until the end area (index BoardSize) 
// - If the Laser hits a teleporter, destroy that teleporter on the 
//  board and move the Laser to the square where the teleporter 
//  points to 
// - If the Laser hits a player, deal 15 damage and stop the laser. 

for (int i = 0; i < BoardSize; i++) 
{ 
    if (GetItem(Board[i]) == Teleporter) 
    { 
     TeleportSquare = GetTeleportSquare(Board[i]); 
     SetItem(Board[i], FreeSpace); 
     i = TeleportSquare; 
    } 

    if (GetItem(Board[i]) == Player) 
    { 
     Player.Life -= 15; 
     break; 
    } 
} 

nie mogę zrobić wyżej w C# 's foreach ponieważ iterator i jest niezmienna. Myślę (popraw mnie, jeśli się mylę), jest to specyficzne dla projektu foreach w językach.

Jestem zainteresowany tym, dlaczego iterator foreach jest niezmienny.

+1

Dobre znalezisko. Jestem trochę zaskoczony, że nigdy wcześniej tego nie zauważyłem. –

+2

Tak naprawdę nie zmieniasz elementu tablicy w swoim drugim przykładzie. –

+0

@Jakob, naprawdę nie jestem zainteresowany zmianą elementu tablicy. Jestem bardziej zainteresowany tym, dlaczego iterator jest niezmienny. Chyba muszę edytować pytanie, aby to wyjaśnić. – MrValdez

Odpowiedz

24

Zacznijmy się z głupim, ale obrazowym przykładzie:

Object o = 15; 
o = "apples"; 

W żadnym momencie nie możemy odnieść wrażenie, że po prostu zamienił numer 15 na ciąg jabłek. Wiemy, że o jest po prostu wskaźnikiem. Teraz pozwala to zrobić w formularzu iteratora.

int[] nums = { 15, 16, 17 }; 

foreach (Object o in nums) { 
    o = "apples"; 
} 

Ponownie, to naprawdę nic nie daje. Lub przynajmniej to będzie osiągnąć nic, aby go skompilować. Z pewnością nie wstawiłoby to naszego ciągu do tablicy int - to nie jest dozwolone, i wiemy, że tak czy inaczej, o jest tylko wskaźnikiem.

Weźmy swój przykład:

foreach (Position Location in Map) 
{ 
    //We want to fudge the position to hide the exact coordinates 
    Location = Location + Random();  //Compiler Error 

    Plot(Location); 
} 

Były to do kompilacji The Location w przykładzie gwiazd na odnoszące się do wartości w Map, ale potem go zmienić, aby odnieść się do nowego Position (domyślnie tworzony przez operator dodający). Funkcjonalnie jest to równoznaczne z tym (co skompilować):

foreach (Position Location in Map) 
{ 
    //We want to fudge the position to hide the exact coordinates 
    Position Location2 = Location + Random();  //No more Error 

    Plot(Location2); 
} 

Więc dlaczego Microsoft uniemożliwiać ponowne przypisanie wskaźnik używany do iteracji? Jasność z jednej rzeczy - nie chcesz, aby ludzie przypisywali to myśląc, że zmienili twoją pozycję w pętli. Łatwość implementacji dla innej: Zmienna może ukryć pewną wewnętrzną logikę wskazującą stan pętli w toku.

Ale co ważniejsze, nie ma powodu, aby chcesz przypisać do niego. Reprezentuje bieżący element sekwencji pętli. Przypisanie wartości łamie "zasadę odpowiedzialności pojedynczej" lub Curly's Law, jeśli podążasz za przerażeniem kodowania. Zmienna powinna oznaczać tylko jedną rzecz.

+3

Tyler, mogę się mylić, ale nie zgadzam się z twoim stwierdzeniem, że "Lokalizacja Postionu" lub "Obiekt O" to iteratory. Zamiast tego są to zmienne iteracyjne, na których działają iteratory. Jeśli tak jest rzeczywiście, to myślę, że twoje rozumowanie może być nieco wypaczone. Zgadzam się, że zmiana wartości zmiennej iteracyjnej może powodować problemy z jasnością, ale chciałbym, żeby Microsoft zostawił to, aby deweloper mógł zdecydować. Znalazłem kilka przypadków, w których warto byłoby zmienić wartość zmiennej iteracji foreach. –

+0

@AnthonyGatlin Nie zgadzam się, że powinni pozwolić twórcy zdecydować w tym przypadku. Jest bardzo realny koszt mylącego kodu, a Microsoft ma dość ludzi kodujących w języku C#, że mają oni bardzo realną zachętę do tworzenia języka, który w największym możliwym stopniu uniemożliwia deweloperowi tworzenie błędów. – tylerl

+0

"* Zmienna powinna oznaczać tylko jedną rzecz *". Dobra koncepcja. W mojej głowie jednak byłoby lepiej, gdyby oznaczało po prostu "* bieżący element *" zamiast "* referencja *" lub "* wskaźnik *".Lub, w przypadku op 'Map [i] ', czego właśnie intuicyjnie oczekujemy. – cregox

12

Jeśli zmienna była zmienna, może to spowodować nieprawidłowe wrażenie. Na przykład:

string[] names = { "Jon", "Holly", "Tom", "Robin", "William" }; 

foreach (string name in names) 
{ 
    name = name + " Skeet"; 
} 

Niektórzy ludzie może pomyśleć, że zmiany zawartości tablicy. Trochę sięga, ale może to być powód. Sprawdzę to w dzisiejszym komentarzu ...

+1

To byłby punkt, który Jon próbuje zilustrować, tak myślę. Zwróć uwagę na "Jeśli". –

+1

Brian, był twój komentarz w odpowiedzi na inny komentarz, który został usunięty? –

+3

Czy kiedykolwiek sprawdziłeś to w swojej specyfikacji? Przypuszczam, że projektanci stworzyli wartość tylko dla jasności, jak sugerujesz, ale byłoby ciekawie wiedzieć, czy były inne względy! –

2

Instrukcja foreach działa z programem Enumerable. Rzeczą, która różni się od niej, jest to, że nie wie o kolekcji jako całości, jest prawie ślepa.

Oznacza to, że nie wie, co będzie dalej, a nawet jeśli będzie cokolwiek do następnego cyklu iteracji. To dlatego widzisz .ToList() bardzo, więc ludzie mogą pobrać liczbę zmiennych.

Z jakiegoś powodu, nie jestem pewien, oznacza to, że należy upewnić się, że kolekcja nie zmienia się podczas próby przeniesienia modułu wyliczającego.

+2

Ale zmiana * zmiennej * i tak by nie zmieniła modułu wyliczającego. To tylko lokalna kopia aktualnej wartości. –

+1

Z pewnością zależy to od tego, czy jest to referencja czy typ wartości? Ale tak, rozumiem, że patrzysz na wartość i być może nie dostosowujesz samego modułu wyliczającego ... hmm – Ian

+0

Oczywiście, nie jest to "trudny" powód, aby nie pozwolić na modyfikację zmiennej iteratora. Kompilator jest już zakodowany na stałe, szukając 'IEnumerable' /' IEnumerable ', aby zdecydować, czy zezwolić na pętlę' foreach' w pierwszej kolejności. * Jeśli * projektanci języka zdecydowali się zezwolić na modyfikację zmiennej iteracji, kompilator mógł zostać zaakceptowany w przypadku, gdy pętla 'foreach' jest używana do iteracji ponad' IList'/'IList '. –

2

Po zmodyfikowaniu kolekcji modyfikacja może mieć nieprzewidywalne skutki uboczne. Modułu wyliczającego nie ma sposobu, aby wiedzieć, jak poprawnie postępować z tymi efektami ubocznymi, więc sprawili, że zbiory były niezmienne.

Zobacz też to pytanie: What is the best way to modify a list in a foreach

+0

Czy możesz wymienić co najmniej jeden konkretny przykład takiego efektu ubocznego, który musiałby obsłużyć moduł wyliczający (który nie może wystąpić, gdy zmienna iteracji jest niezmienna)? W przeciwnym razie ta odpowiedź brzmi wyjątkowo niejasno. –

1

Warunkiem pracy z IEnumerable obiektów jest to, że zbiór bazowy nie musi się zmieniać, gdy łączysz go z przeliczalny. Można założyć, że obiekt Enumerable jest ujęciem z oryginalnej kolekcji.Więc jeśli spróbujesz zmienić kolekcję podczas wyliczania, wyrzuci wyjątek. Jednak pobrane obiekty w Wyliczeniu nie są w ogóle niezmienne.

Ponieważ zmienna użyta w pętli foreach jest lokalna dla bloku pętli, ta zmienna nie jest jednak dostępna poza blokiem.

10

Myślę, że to sztuczne ograniczenie, naprawdę nie trzeba tego robić. Aby to udowodnić, weź ten kod pod uwagę i rozważ rozwiąż problem. Problemem jest asign, ale wewnętrzne zmiany obiektów nie stanowią problemu:

using System; 
using System.Collections.Generic; 

namespace ConsoleApplication1 
{ 
    class Program 
    { 
     static void Main(string[] args) 
     { 

      List<MyObject> colection =new List<MyObject>{ new MyObject{ id=1, Name="obj1" }, 
                new MyObject{ id=2, Name="obj2"} }; 

      foreach (MyObject b in colection) 
      { 
      // b += 3;  //Doesn't work 
       b.Add(3); //Works 
      } 
     } 

     class MyObject 
     { 
      public static MyObject operator +(MyObject b1, int b2) 
      { 
       return new MyObject { id = b1.id + b2, Name = b1.Name }; 
      } 

      public void Add(int b2) 
      { 
       this.id += b2; 
      } 
      public string Name; 
      public int id; 
     } 
    } 
} 

ja nie o tym zjawisk, bo zawsze modyfikowanie obiektów drodze opisałem.

+2

+1 za przyzwoitą pracę. – ashes999

Powiązane problemy