2010-09-14 17 views
7

Mam kolekcję zerowych ints.Zmienna iteracyjna innego typu niż kolekcja?

Dlaczego kompilator dopuszcza zmienną iteracyjną typu int nie int??

 List<int?> nullableInts = new List<int?>{1,2,3,null}; 
     List<int> normalInts = new List<int>(); 


     //Runtime exception when encounter null value 
     //Why not compilation exception? 
     foreach (int i in nullableInts) 
     { 
     //do sth 
     } 

Oczywiście muszę zwrócić uwagę na to, co iterację, ale byłoby miło, gdyby kompilator skarcił mnie :) Podoba tutaj:

 foreach (bool i in collection) 
     { 
      // do sth 
     } 

     //Error 1 Cannot convert type 'int' to 'bool' 
+0

Ten błąd wygląda normalnie. Możesz przekonwertować z int? do int, ale nie możesz przekonwertować int do bool. –

+0

@Adrian Nie sądzę, że to wyjaśnienie jest poprawne. Co gdybym miał string zamiast bool? Int może zostać przekonwertowane na ciąg znaków, ale kompilator będzie protestował. – nan

+1

Int nie może zostać przekonwertowany na ciąg. Nie zdefiniowano żadnej niejawnej ani jawnej konwersji. Jeśli używasz metody ToString(), to jest inna rzecz. –

Odpowiedz

3

Aktualizacja

OK, pierwotnie powiedziałem "kompilator dodaje rzuca do foreach pętli". Nie jest to dokładne dopasowanie: nie będzie zawsze dodawać rzutów. Oto, co się naprawdę dzieje.

Przede wszystkim, kiedy masz ten foreach pętlę:

foreach (int x in collection) 
{ 
} 

... tutaj jest podstawowy zarys (w pseudo-C#), co powoduje kompilator:

int x; 
[object] e; 
try 
{ 
    e = collection.GetEnumerator(); 
    while (e.MoveNext()) 
    { 
     x = [cast if possible]e.Current; 
    } 
} 
finally 
{ 
    [dispose of e if necessary] 
} 

Co? Słyszę, że mówisz. Co masz na myśli przez [object]?”

Oto co mam na myśli. Pętla foreach rzeczywiście wymaga żadnego interfejsu, co oznacza, że ​​to rzeczywiście trochę magiczne. Wymaga jedynie, że typ obiektu wyliczone odsłania GetEnumerator metoda, która z kolei musi zapewnić wystąpienie pewnego rodzaju, który zapewnia MoveNext i właściwość Current.

Więc napisałem [object] ponieważ typ e niekoniecznie muszą być implementacja IEnumerator<int> lub nawet IEnumerator - co oznacza również, że nie musi koniecznie wykonywać IDisposable (stąd część [dispose if necessary]).

Częścią kodu, na którym zależy nam w celu udzielenia odpowiedzi na to pytanie, jest część, w której napisałem: [cast if possible]. Oczywiście, ponieważ kompilator nie wymaga rzeczywistej implementacji IEnumerator<T> lub IEnumerator, nie można przyjąć, że typ e.Current to T, object lub cokolwiek pomiędzy.Zamiast tego kompilator określa typ e.Current na podstawie typu zwróconego przez GetEnumerator w czasie kompilacji. Następnie wykonywane są następujące kroki:

  1. Jeżeli typ jest typ zmiennej lokalnej (x w powyższym przykładzie), prosty zadanie jest używany.
  2. Jeśli typ to zamienny na typ zmiennej lokalnej (przez co mam na myśli, akt prawny istnieje od typu e.Current do typu x), włożona jest obsada.
  3. W przeciwnym razie kompilator zgłosi błąd.

Więc w scenariuszu wyliczanie nad List<int?>, mamy do kroku 2 i kompilator widzi, że Current własność List<int?>.Enumerator Type jest typu int?, które mogą być wyraźnie oddane do int.

Więc linia może zostać skompilowany do równowartości to:

x = (int)e.Current; 

Teraz, co robi wygląd explicit operator jak dla Nullable<int>?

Według Reflektor:

public static explicit operator T(T? value) 
{ 
    return value.Value; 
} 

Więc the behavior described by Kent jest, o ile mogę powiedzieć, po prostu optymalizacja kompilatora: the (int)e.Current wyraźny obsada jest inlined.

Jako ogólną odpowiedź na twoje pytanie, trzymam się mojego stwierdzenia, że ​​kompilator wstawia odlewy w pętli foreach tam, gdzie jest to konieczne.


Original Odpowiedź

Kompilator automatycznie wstawia rzuca gdzie potrzebne w foreach pętli dla tego prostego powodu, że przed generycznych nie było IEnumerable<T> interfejs, tylko IEnumerable *. Interfejs IEnumerable udostępnia IEnumerator, który z kolei zapewnia dostęp do właściwości Current typu object.

Więc jeśli kompilator nie wykonał obsady dla ciebie, w dawnych czasach, jedynym sposobem, w jaki mogłeś użyć foreach, byłaby zmienna lokalna typu object, która oczywiście by się zassała.

* I rzeczywiście, foreach doesn't require any interface at all - tylko metoda GetEnumerator i towarzyszący typ z MoveNext i Current.

+0

Domyślnie nie jest wywoływany 'IEnumerable ' 'GetEnumerator', czyniąc rodzajowy" foreach' "? – strager

+0

@strager: Tak, ale nadal będzie obsada. – SLaks

+0

@Slaks: nie ma obsady, a jedna neguje niektóre zalety wydajności generycznych.Sprawdź mój post. –

4

Ponieważ kompilator C# dereferences się Nullable<T> dla Ciebie.

Jeśli napisać ten kod:

 var list = new List<int?>() 
     { 
      1, 
      null 
     }; 

     foreach (int? i in list) 
     { 
      if (!i.HasValue) 
      { 
       continue; 
      } 

      Console.WriteLine(i.GetType()); 
     } 

     foreach (int i in list) 
     { 
      Console.WriteLine(i.GetType()); 
     } 

C# kompilator produkuje:

foreach (int? i in list) 
{ 
    if (i.HasValue) 
    { 
     Console.WriteLine(i.GetType()); 
    } 
} 
foreach (int? CS$0$0000 in list) 
{ 
    Console.WriteLine(CS$0$0000.Value.GetType()); 
} 

Uwaga wyraźne wyłuskania z Nullable<int>.Value. Jest to świadectwo tego, jak zakorzeniona jest struktura środowiska wykonawczego.

+1

Podany link oznacza, że ​​operator jest jawny. Czy czegoś brakuje ...? – strager

+0

@strager: nie, masz rację. To 'T' ->' Nullable 'jest niejawne. Zaktualizowałem moją odpowiedź. –

+0

"Jest to świadectwo tego, jak zakorzeniona jest struktura' Nullable 'w środowisku wykonawczym." Nie widzę w ogóle, co to ma wspólnego z runtime. Myślę, że twoja odpowiedź jest poprawna, z wyjątkiem twojego wniosku. – strager

3

Zachowanie, które obserwujesz, jest zgodne z sekcją 8.8.4 Instrukcja foreach specyfikacji języka C#. Ta sekcja definiuje semantykę instrukcji foreach w następujący sposób:

[...] Powyższe kroki, jeśli się powiedzie, jednoznacznie wytworzą kolekcję typu C, moduł wyliczający typ E i typ elementu T. foreach zestawienie postaci

foreach (V v in x) embedded-statement 

zostaje rozszerzona do:

{ 
    E e = ((C)(x)).GetEnumerator(); 
    try { 
     V v; 
     while (e.MoveNext()) { 
      v = (V)(T)e.Current; 
      embedded-statement 
     } 
    } 
    finally { 
     // Dispose e 
    } 
} 

Zgodnie z zasadami określonymi w specyfikacji, w próbce typ kolekcja byłoby List<int?> The moduł wyliczający typ byłby List<int?>.Enumerator, a typem elementu byłby int?.

Jeśli wypełnisz tę informację w powyższym fragmencie kodu, zobaczysz, że int? jest jawnie rzutowany na int, dzwoniąc pod numer Nullable<T> Explicit Conversion (Nullable<T> to T). Implementacja tego jawnego operatora jest, jak to opisał Kent, po prostu zwrócić właściwość Nullable<T>.Value.

+0

Gah, spędziłem zbyt długo aktualizując moją odpowiedź, aby uświadomić sobie, że w zasadzie pokrywasz to samo w sobie! Jedno jednak: myślę, że * typ wylicznika * w tym przykładzie to w rzeczywistości 'List .Enumerator', * nie *' IEnumerator '. Czy mam rację? –

+0

@ Dan Tao: Tak, masz rację (jak rozumiem specyfikację). –