Enumerable.Cast InvalidCastException desconcertante

Lo siguiente arroja una InvalidCastException .

 IEnumerable list = new List() { 1 }; IEnumerable castedList = list.Cast(); Console.WriteLine(castedList.First()); 

¿Por qué?

Estoy usando Visual Studio 2008 SP1.

¡Eso es muy extraño! Hay una publicación de blog aquí que describe cómo se cambió el comportamiento de Cast() entre .NET 3.5 y .NET 3.5 SP1, pero aún no explica la InvalidCastException, que incluso se obtiene si reescribe su código de esta manera:

 var list = new[] { 1 }; var castedList = from long l in list select l; Console.WriteLine(castedList.First()); 

Obviamente puedes evitarlo haciendo el casting tú mismo

 var castedList = list.Select(i => (long)i); 

Esto funciona, pero no explica el error en primer lugar. Intenté lanzar la lista para abreviar y flotar, y aquellos arrojaron la misma excepción.

Editar

¡Esa publicación del blog explica por qué no funciona!

Cast() es un método de extensión en IEnumerable lugar de IEnumerable . Eso significa que para cuando cada valor llega al punto en el que se está emitiendo, ya se ha vuelto a guardar en un System.Object. En esencia, está tratando de hacer esto:

 int i = 1; object o = i; long l = (long)o; 

Este código arroja la InvalidCastException que está recibiendo. Si intentas lanzar un int directamente a un largo, estás bien, pero no funcionas devolver un int encuadrado a un largo.

Sin duda una rareza!

El método de Enumerable.Cast se define de la siguiente manera:

 public static IEnumerable Cast( this IEnumerable source ) 

Y no hay información sobre el tipo inicial de elementos de IEnumerable, así que creo que cada uno de tus datos se convierte inicialmente en System.Object a través del boxeo y luego se intenta que se desempaquete en una variable larga y esto es incorrecto.

Código similar para reproducir esto:

 int i = 1; object o = i; // boxing long l = (long)o; // unboxing, incorrect // long l = (int)o; // this will work 

Entonces la solución para su problema será:

 ints.Select(i => (long)i) 

Hmm … rompecabezas interesante. Aún más interesante dado que lo ejecuté en Visual Studio 2008 y no arrojó nada.

No estoy usando el Service Pack 1, y puede que sí, así que ese podría ser el problema. Sé que hubo algunas “mejoras de rendimiento” en .Cast () en la versión SP1 que podrían estar causando el problema. Algunos leyendo:

Entrada de blog 1

Entrada de blog 2

¡Estoy de nuevo!
Aquí está la solución para todos sus problemas de conversión de List y Enumerable . ~ 150 líneas de código
Solo asegúrese de definir al menos un operador de conversión explícito o implícito para los tipos de entrada / salida involucrados (si es que uno no existe), ¡como debería estar haciendo de todos modos!

 using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; using System.Reflection; namespace System.Collections.Generic //purposely in same namespace as List,IEnumerable, so extension methods are available with them { public static class Enumerable { public static List ConvertAll( this IEnumerable input ) { return BuildConvertedList( input, GetConverterDelegate() ); } public static IEnumerable ConvertAll( this IEnumerable input, bool lazy ) { if (lazy) return new LazyConverter( input, GetConverterDelegate() ); return BuildConvertedList( input, GetConverterDelegate() ); } public static List ConvertAll( this IEnumerable input, Converter converter ) { return BuildConvertedList( input, converter ); } public static List ConvertAll( this List input ) { Converter converter = GetConverterDelegate(); return input.ConvertAll( converter ); } public static IEnumerable ConvertAll( this List input, Converter converter, bool lazy ) { if (lazy) return new LazyConverter( input, converter ); return input.ConvertAll( converter ); } public static List ConvertAll( this List input, Converter converter ) { return input.ConvertAll( converter ); } //Used to manually build converted list when input is IEnumerable, since it doesn't have the ConvertAll method like the List does private static List BuildConvertedList( IEnumerable input, Converter converter ){ List output = new List(); foreach (TInput input_item in input) output.Add( converter( input_item ) ); return output; } private sealed class LazyConverter: IEnumerable, IEnumerator { private readonly IEnumerable input; private readonly Converter converter; private readonly IEnumerator input_enumerator; public LazyConverter( IEnumerable input, Converter converter ) { this.input = input; this.converter = converter; this.input_enumerator = input.GetEnumerator(); } public IEnumerator GetEnumerator() {return this;} //IEnumerable Member IEnumerator IEnumerable.GetEnumerator() {return this;} //IEnumerable Member public void Dispose() {input_enumerator.Dispose();} //IDisposable Member public TOutput Current {get {return converter.Invoke( input_enumerator.Current );}} //IEnumerator Member object IEnumerator.Current {get {return Current;}} //IEnumerator Member public bool MoveNext() {return input_enumerator.MoveNext();} //IEnumerator Member public void Reset() {input_enumerator.Reset();} //IEnumerator Member } private sealed class TypeConversionPair: IEquatable { public readonly Type source_type; public readonly Type target_type; private readonly int hashcode; public TypeConversionPair( Type source_type, Type target_type ) { this.source_type = source_type; this.target_type = target_type; //precalc/store hash, since object is immutable; add one to source hash so reversing the source and target still produces unique hash hashcode = (source_type.GetHashCode() + 1) ^ target_type.GetHashCode(); } public static bool operator ==( TypeConversionPair x, TypeConversionPair y ) { if ((object)x != null) return x.Equals( y ); if ((object)y != null) return y.Equals( x ); return true; //x and y are both null, cast to object above ensures reference equality comparison } public static bool operator !=( TypeConversionPair x, TypeConversionPair y ) { if ((object)x != null) return !x.Equals( y ); if ((object)y != null) return !y.Equals( x ); return false; //x and y are both null, cast to object above ensures reference equality comparison } //TypeConversionPairs are equal when their source and target types are equal public bool Equals( TypeConversionPair other ) { if ((object)other == null) return false; //cast to object ensures reference equality comparison return source_type == other.source_type && target_type == other.target_type; } public override bool Equals( object obj ) { TypeConversionPair other = obj as TypeConversionPair; if ((object)other != null) return Equals( other ); //call IEqualityComparer implementation if obj type is TypeConversionPair return false; //obj is null or is not of type TypeConversionPair; Equals shall not throw errors! } public override int GetHashCode() {return hashcode;} //assigned in constructor; object is immutable } private static readonly Dictionary conversion_op_cache = new Dictionary(); //Uses reflection to find and create a Converter delegate for the given types. //Once a delegate is obtained, it is cached, so further requests for the delegate do not use reflection* //(*the typeof operator is used twice to look up the type pairs in the cache) public static Converter GetConverterDelegate() { Delegate converter; TypeConversionPair type_pair = new TypeConversionPair( typeof(TInput), typeof(TOutput) ); //Attempt to quickly find a cached conversion delegate. lock (conversion_op_cache) //synchronize with concurrent calls to Add if (conversion_op_cache.TryGetValue( type_pair, out converter )) return (Converter)converter; //Get potential conversion operators (target-type methods are ordered first) MethodInfo[][] conversion_op_sets = new MethodInfo[2][] { type_pair.target_type.GetMethods( BindingFlags.Static | BindingFlags.Public | BindingFlags.FlattenHierarchy ), type_pair.source_type.GetMethods( BindingFlags.Static | BindingFlags.Public | BindingFlags.FlattenHierarchy ) }; //Find appropriate conversion operator, //favoring operators on target type in case functionally equivalent operators exist, //since the target type's conversion operator may have access to an appropriate constructor //or a common instance cache (ie immutable objects may be cached and reused). for (int s = 0; s < conversion_op_sets.Length; s++) { MethodInfo[] conversion_ops = conversion_op_sets[s]; for (int m = 0; m < conversion_ops.Length; m++) { MethodInfo mi = conversion_ops[m]; if ((mi.Name == "op_Explicit" || mi.Name == "op_Implicit") && mi.ReturnType == type_pair.target_type && mi.GetParameters()[0].ParameterType.IsAssignableFrom( type_pair.source_type )) //Assuming op_Explicit and op_Implicit always have exactly one parameter. { converter = Delegate.CreateDelegate( typeof(Converter), mi ); lock (conversion_op_cache) //synchronize with concurrent calls to TryGetValue conversion_op_cache.Add( type_pair, converter ); //Cache the conversion operator reference for future use. return (Converter)converter; } } } return (TInput x) => ((TOutput)Convert.ChangeType( x, typeof(TOutput) )); //this works well in the absence of conversion operators for types that implement IConvertible //throw new InvalidCastException( "Could not find conversion operator to convert " + type_pair.source_type.FullName + " to " + type_pair.target_type.FullName + "." ); } } } 

Uso de muestra:

 using System; using System.Collections.Generic; namespace ConsoleApplication1 { class Program { static void Main(string[] args) { List list = new List(new string[] { "abcde", "abcd", "abc"/*will break length constraint*/, "ab", "a" }); //Uncomment line below to see non-lazy behavior. All items converted before method returns, and will fail on third item, which breaks the length constraint. //List constrained_list = list.ConvertAll(); IEnumerable constrained_list = list.ConvertAll( true ); //lazy conversion; conversion is not attempted until that item is read foreach (ConstrainedString constrained_string in constrained_list) //will not fail until the third list item is read/converted System.Console.WriteLine( constrained_string.ToString() ); } public class ConstrainedString { private readonly string value; public ConstrainedString( string value ){this.value = Constrain(value);} public string Constrain( string value ) { if (value.Length > 3) return value; throw new ArgumentException("String length must be > 3!"); } public static explicit operator ConstrainedString( string value ){return new ConstrainedString( value );} public override string ToString() {return value;} } } } 

Ojalá hubieran podido hacer algo inteligente, como usar cualquier operador de reparto implícito o explícito definido para el tipo. El comportamiento actual y la inconsistencia son inaceptables. Absolutamente inútil en su estado actual.

Después de darme cuenta de que Cast lanzaba una excepción en lugar de utilizar los operadores de conversión que definí para el tipo, me irrité y encontré este hilo. Si está definido para IEnumerable , ¿por qué no implementarlo solo para usar el reflection para obtener el tipo de objeto, obtener el tipo de destino, descubrir los operadores de conversión estáticos disponibles y encontrar uno adecuado para realizar el reparto? Podría convertir un IEnumerable heterogéneo en un IEnumerable .

La siguiente implementación es una idea de trabajo …

 public static class EnumerableMinusWTF { public static IEnumerable Cast(this IEnumerable source) { Type source_type = typeof(TSource); Type target_type = typeof(TResult); List methods = new List(); methods.AddRange( target_type.GetMethods( BindingFlags.Static | BindingFlags.Public ) ); //target methods will be favored in the search methods.AddRange( source_type.GetMethods( BindingFlags.Static | BindingFlags.Public ) ); MethodInfo op_Explicit = FindExplicitConverstion(source_type, target_type, methods ); List results = new List(); foreach (TSource source_item in source) results.Add((TResult)op_Explicit.Invoke(null, new object[] { source_item })); return results; } public static MethodInfo FindExplicitConverstion(Type source_type, Type target_type, List methods) { foreach (MethodInfo mi in methods) { if (mi.Name == "op_Explicit") //will return target and take one parameter if (mi.ReturnType == target_type) if (mi.GetParameters()[0].ParameterType == source_type) return mi; } throw new InvalidCastException( "Could not find conversion operator to convert " + source_type.FullName + " to " + target_type.FullName + "." ); } } 

Entonces puedo llamar a este código con éxito:

  //LessonID inherits RegexConstrainedString, and has explicit conversion operator defined to convert string to LessonID List lessons = new List(new string[] {"l001,l002"}); IEnumerable constrained_lessons = lessons.Cast(); 

Aquí hay algunas cosas para pensar …

  1. ¿Quieres lanzar o convertir?
  2. ¿Desea el resultado como una List o un IEnumerable .
  3. Si el resultado es un IEnumerable , ¿quieres que el lanzamiento / conversión se aplique de forma perezosa (es decir, el lanzamiento / conversión no ocurrirá hasta que el iterador llegue a cada elemento)?

Distinción útil entre conversión / conversión, ya que un operador de conversión a menudo implica la construcción de un nuevo objeto y podría considerarse una conversión:
Las implementaciones de “Cast” deberían aplicar automáticamente operadores de conversión definidos para los tipos involucrados; un nuevo objeto puede o no puede ser construido .
Las implementaciones de “Convertir” deberían permitir especificar un delegado System.Converter .

Encabezados de métodos potenciales:

 List Cast(IEnumerable input); List Convert(IEnumerable input, Converter converter); IEnumerable Cast(IEnumerable input); IEnumerable Convert(IEnumerable input, Converter converter); 

Implementaciones problemáticas de “Cast” utilizando el marco existente; supongamos que pasa como entrada una List , que desea convertir con cualquiera de los métodos anteriores.

 //Select can return only a lazy read-only iterator; also fails to use existing explicit cast operator, because such a cast isn't possible in c# for a generic type parameter (so says VS2008) list.Select( (TInput x) => (TOutput)x ); //Cast fails, unless TOutput has an explicit conversion operator defined for 'object' to 'TOutput'; this confusion is what lead to this topic in the first place list.Cast(); 

Implementaciones problemáticas de “Convertir”

 //Again, the cast to a generic type parameter not possible in c#; also, this requires a List as input instead of just an IEnumerable. list.ConvertAll( new Converter( (TInput x) => (TOutput)x ) ); //This would be nice, except reflection is used, and must be used since c# hides the method name for explicit operators "op_Explicit", making it difficult to obtain a delegate any other way. list.ConvertAll( (Converter)Delegate.CreateDelegate( typeof(Converter), typeof(TOutput).GetMethod( "op_Explicit", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public ) ) ); 

Resumen:
Los métodos de conversión / conversión deben incluir operadores de conversión explícitos definidos, o permitir que uno especifique un delegado de conversión. La especificación de lenguaje de C # para los operadores de conversión, específicamente su falta de un nombre de método, hace que sea difícil obtener un delegado, excepto a través de la reflexión. La alternativa es encapsular o replicar el código de conversión, aumentando innecesariamente la complejidad (de mantenimiento) de su código, ya que las conversiones reales, posibles / permitidas están implícitas en la presencia o ausencia de operadores de conversión, y deben ser manejadas por el comstackdor. No deberíamos tener que buscar manualmente las definiciones con nombre críptico (por ejemplo, “op_Explicit”) de los operadores de conversión apropiados con reflection en RUN TIME sobre los tipos implicados. Además, los métodos Cast / Convert para conversiones bulk / list utilizando operadores de conversión explícitos deberían ser realmente una característica de framework, y con List.ConvertAll , son … excepto que la especificación de idioma hace que sea difícil obtener un delegado para la conversión operadores de manera eficiente !!!

Por supuesto, lo más sensato es usar Select(i => (long)i) y eso es lo que recomendaría para las conversiones entre los tipos de valores incorporados y las conversiones definidas por el usuario.

Pero al igual que una observación curiosa , desde .NET 4 es posible hacer su propio método de extensión que también funciona con este tipo de conversiones. Pero requiere que esté dispuesto a usar la palabra clave dynamic . Simplemente es así:

 public static IEnumerable CastSuper(this IEnumerable source) { foreach (var s in source) yield return (TResult)(dynamic)s; } 

Como dije, funciona con conversiones integrales (reduciendo o ampliando conversiones), conversiones numéricas a / desde / entre tipos de coma flotante y “métodos” de conversión de los tipos implicit operator y explicit operator .

Y, por supuesto, todavía funciona con las buenas conversiones de referencia antiguas y las conversiones de unboxing como System.Enumerable.Cast .