2010-09-14 6 views
7

Tengo una colección de entradas anulables.Variable de iteración de tipo diferente de la colección?

¿Por qué el compilador permite que la variable de iteración sea del tipo int no 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 
     } 

Por supuesto que debe prestar atención a lo que iterar pero sería bueno si me reprendió compilador :) Como aquí:

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

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

El error me parece normal. Puede convertir desde int? a int, pero no puede convertir un int a bool. –

+0

@Adrian No creo que esta explicación sea correcta. ¿Qué pasa si tengo una cuerda en lugar de bool? Int se puede convertir en cadena, pero el compilador seguiría protestando. – nan

+1

Int no se puede convertir en cadena. No se define conversión implícita o explícita. Si te estás refiriendo al método ToString(), entonces eso es algo diferente. –

Respuesta

3

actualización

bien, al principio me dijo "el compilador añade arroja a un bucle foreach." Esto no es estrictamente exacto: no siempre agregar moldes. Esto es lo que realmente está sucediendo.

En primer lugar, cuando se tiene este foreach bucle:

foreach (int x in collection) 
{ 
} 

... aquí es el esquema básico (en pseudo-C#) de lo que crea el compilador:

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

¿Qué? Te oigo decir. ¿Qué quiere decir por [object]?"

Esto es lo que quiero decir. El bucle foreach requiere realmente ninguna interfaz, lo que significa que en realidad es un poco mágico. Sólo se requiere que el tipo de objeto que se enumeró expone una GetEnumerator método, que a su vez debe proporcionar una instancia de algún tipo que proporciona una MoveNext y una Current propiedad.

Así que escribió [object] porque el tipo de e no necesariamente tiene que ser una implementación de IEnumerator<int>, o incluso IEnumerator - que también significa que no necesariamente tiene que implementar IDisposable (de ahí la parte [dispose if necessary]).

La parte del código que nos interesa con el propósito de responder a esta pregunta es la parte donde escribí [cast if possible]. Claramente, dado que el compilador no requiere una implementación real de IEnumerator<T> o IEnumerator, no se puede suponer que el tipo de e.Current sea T, object o cualquier cosa intermedia.En cambio, el compilador determina el tipo de e.Current según el tipo devuelto por GetEnumerator en tiempo de compilación. Entonces ocurre lo siguiente:

  1. Si el tipo de es del tipo de la variable local (x en el ejemplo anterior), se usa una asignación de recta.
  2. Si el tipo es convertible al tipo de la variable local (lo cual quiero decir, existe un reparto legal del tipo de e.Current al tipo de x), se inserta un yeso.
  3. De lo contrario, el compilador generará un error.

Así, en el escenario de enumerar más de un List<int?>, obtenemos al paso 2 y el compilador ve que la propiedad de tipo List<int?>.EnumeratorCurrent es de tipo int?, que puede convertirse explícitamente a int.

Así, la línea puede ser compilado por el equivalente a esto:

x = (int)e.Current; 

Ahora, lo que hace la mirada explicit operador como por Nullable<int>?

De acuerdo con reflector:

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

Así es the behavior described by Kent, por lo que yo puedo decir, simplemente una optimización del compilador: el (int)e.Current conversión explícita se colocarán en línea.

Como respuesta general a su pregunta, me atengo a mi afirmación de que el compilador inserta moldes en un bucle foreach donde sea necesario.


respuesta original

El compilador inserta automáticamente arroja cuando se requieran en un bucle foreach por la sencilla razón de que antes de que los genéricos no había IEnumerable<T> interfaz, solamente IEnumerable *. La interfaz IEnumerable expone un IEnumerator, que a su vez proporciona acceso a una propiedad Current del tipo object.

Así que, a menos que el compilador haya realizado el lanzamiento para usted, en el pasado, la única manera en que podría haber usado foreach hubiera sido con una variable local del tipo object, que obviamente habría chupado.

* Y, de hecho, foreach doesn't require any interface at all - sólo el método GetEnumerator y un tipo de acompañamiento con un MoveNext y una Current.

+0

De forma predeterminada, ¿no se llamará a 'IEnumerable ' 'GetEnumerator', por lo que' foreach' es "genérico"? – strager

+0

@strager: Sí, pero aún habrá un elenco. – SLaks

+0

@Slaks: no hay yeso y tener uno anularía algunos de los beneficios de rendimiento de los genéricos.Revisa mi publicación –

4

Debido a que el compilador de C# elimina referencia a la Nullable<T> para usted.

Si se escribe este código:

 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()); 
     } 

El compilador de C# produce:

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()); 
} 

Nota desreferenciar explícita de Nullable<int>.Value. Esto es un testimonio de cómo está arraigada la estructura Nullable<T> en el tiempo de ejecución.

+1

El enlace que proporcionó explícitamente escribe que el operador es 'explicit'. Me estoy perdiendo de algo...? – strager

+0

@strager: no, tienes razón. Es 'T' ->' Nullable 'que está implícito. He actualizado mi respuesta. –

+0

"Esto es un testimonio de lo arraigada que está la estructura' Nullable 'en el tiempo de ejecución." No veo en absoluto qué tiene esto que ver con el tiempo de ejecución. Creo que tu respuesta es correcta, excepto tu conclusión. – strager

3

El comportamiento que está observando está de acuerdo con la sección 8.8.4 La declaración forecech de la especificación de lenguaje C#. Esta sección define la semántica de la declaración foreach de la siguiente manera:

[...] Los pasos anteriores, si son exitosos, producen inequívocamente un tipo de colección C, enumerador tipo E y elemento tipo T. Un foreach enunciado de la forma

foreach (V v in x) embedded-statement 

A continuación se expande a:

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

De acuerdo con las reglas definidas en la especificación, en su muestra del tipo colección sería List<int?>, la enumerador tipo sería List<int?>.Enumerator y el tipo de elemento sería int?.

Si completa esta información en el fragmento de código anterior, verá que int? se transfiere explícitamente a int llamando al Nullable<T> Explicit Conversion (Nullable<T> to T). La implementación de este operador de conversión explícita es, como lo describe Kent, simplemente devolver la propiedad Nullable<T>.Value.

+0

Gah, ¡pasé demasiado tiempo actualizando mi respuesta para darme cuenta de que básicamente cubriste el mismo terreno en el tuyo! Una cosa, sin embargo: creo que * el tipo de enumerador * en el ejemplo es en realidad 'List .Enumerator', * not *' IEnumerator '. ¿Estoy en lo cierto? –

+0

@Dan Tao: Sí, tienes razón (según entiendo la especificación). –

Cuestiones relacionadas