代码之家  ›  专栏  ›  技术社区  ›  jason

令人困惑的Enumerable。强制转换无效Cast异常

  •  48
  • jason  · 技术社区  · 16 年前

    下面抛出一个 InvalidCastException .

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

    为什么?

    我使用的是Visual Studio 2008 SP1。

    7 回复  |  直到 16 年前
        1
  •  62
  •   Matt Hamilton    16 年前

    太奇怪了!有一篇博客文章 here 它描述了 Cast<T>() 在两者之间发生了变化。NET 3.5和。NET 3.5 SP1,但它仍然没有解释InvalidCastException,如果你这样重写代码,你甚至会遇到这个异常:

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

    显然,你可以通过自己选演员来解决这个问题

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

    这是有效的,但它并不能从一开始就解释错误。我尝试将列表转换为short和float,但它们都抛出了相同的异常。

    编辑

    那篇博客文章确实解释了为什么它不起作用!

    演员阵容<T>() 是上的扩展方法 IEnumerable 而不是 IEnumerable<T> 这意味着,当每个值到达其被转换的点时,它已经被打包回系统中。对象。本质上,它试图做到这一点:

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

    这段代码抛出您收到的InvalidCastException。如果你试图将int直接转换为long,你就可以了,但将盒装int转换回long是行不通的。

    当然,这很奇怪!

        2
  •  29
  •   okutane    16 年前

    可枚举。铸造方法定义如下:

    public static IEnumerable<TResult> Cast<TResult>(
        this IEnumerable source
    )
    

    而且没有关于IEnumerable项的初始类型的信息,所以我认为您的每个int最初都转换为System。对象通过装箱,然后试图将其解包为长变量,这是不正确的。

    类似的代码来重现这一点:

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

    因此,您的问题的解决方案将是:

    ints.Select(i => (long)i)
    
        3
  •  4
  •   Triynko    16 年前

    我又来了!
    以下是您所有问题的解决方案 List<T> Enumerable<T> 转换问题。 约150行代码
    只要确保为所涉及的输入/输出类型定义至少一个显式或隐式转换运算符(如果不存在),你无论如何都应该这样做!

    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<T>,IEnumerable<T>, so extension methods are available with them
    {
        public static class Enumerable
        {
            public static List<TOutput> ConvertAll<TInput,TOutput>( this IEnumerable<TInput> input ) {
                return BuildConvertedList<TInput,TOutput>( input, GetConverterDelegate<TInput,TOutput>() );
            }
    
            public static IEnumerable<TOutput> ConvertAll<TInput,TOutput>( this IEnumerable<TInput> input, bool lazy ) {
                if (lazy) return new LazyConverter<TInput,TOutput>( input, GetConverterDelegate<TInput,TOutput>() );
                return BuildConvertedList<TInput,TOutput>( input, GetConverterDelegate<TInput,TOutput>() );
            }
    
            public static List<TOutput> ConvertAll<TInput,TOutput>( this IEnumerable<TInput> input, Converter<TInput, TOutput> converter ) {
                return BuildConvertedList<TInput,TOutput>( input, converter );
            }
    
            public static List<TOutput> ConvertAll<TInput, TOutput>( this List<TInput> input ) {
                Converter<TInput, TOutput> converter = GetConverterDelegate<TInput,TOutput>();
                return input.ConvertAll<TOutput>( converter );
            }
    
            public static IEnumerable<TOutput> ConvertAll<TInput, TOutput>( this List<TInput> input, Converter<TInput, TOutput> converter, bool lazy ) {
                if (lazy) return new LazyConverter<TInput, TOutput>( input, converter );
                return input.ConvertAll<TOutput>( converter );
            }
    
            public static List<TOutput> ConvertAll<TInput, TOutput>( this List<TInput> input, Converter<TInput, TOutput> converter ) {
                return input.ConvertAll<TOutput>( 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<TOutput> BuildConvertedList<TInput,TOutput>( IEnumerable<TInput> input, Converter<TInput, TOutput> converter ){
                List<TOutput> output = new List<TOutput>();
                foreach (TInput input_item in input)
                    output.Add( converter( input_item ) );
                return output;
            }
    
            private sealed class LazyConverter<TInput, TOutput>: IEnumerable<TOutput>, IEnumerator<TOutput>
            {
                private readonly IEnumerable<TInput> input;
                private readonly Converter<TInput, TOutput> converter;
                private readonly IEnumerator<TInput> input_enumerator;
    
                public LazyConverter( IEnumerable<TInput> input, Converter<TInput, TOutput> converter )
                {
                    this.input = input;
                    this.converter = converter;
                    this.input_enumerator = input.GetEnumerator();
                }
    
                public IEnumerator<TOutput> GetEnumerator() {return this;} //IEnumerable<TOutput> 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<TOutput> 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<TypeConversionPair>
            {
                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<TypeConversionPair> 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<TypeConversionPair,Delegate> conversion_op_cache = new Dictionary<TypeConversionPair,Delegate>();
    
            //Uses reflection to find and create a Converter<TInput, TOutput> 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<TInput, TOutput> GetConverterDelegate<TInput, TOutput>()
            {
                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<TInput, TOutput>)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 (i.e. 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<TInput, TOutput>), 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<TInput, TOutput>)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 + "." );
            }
        }
    }
    

    样品使用:

    using System;
    using System.Collections.Generic;
    
    namespace ConsoleApplication1
    {
        class Program
        {
            static void Main(string[] args)
            {
                List<string> list = new List<string>(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<ConstrainedString> constrained_list = list.ConvertAll<string,ConstrainedString>();
                IEnumerable<ConstrainedString> constrained_list = list.ConvertAll<string,ConstrainedString>( 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;}
            }
        }
    }
    
        4
  •  3
  •   Randolpho    16 年前

    嗯……有趣的谜题。更有趣的是,我刚刚在Visual Studio 2008中运行了它,它 没有 扔在所有。

    我没有使用Service Pack 1,你可能也没有,所以这可能是问题所在。我知道中有一些“性能增强”。SP1版本中的Cast()可能会导致此问题。一些阅读:

    Blog Entry 1

    Blog Entry 2

        5
  •  2
  •   Triynko    16 年前

    我希望他们能做一些聪明的事情,比如使用为该类型定义的任何隐式或显式转换运算符。目前的行为和不一致是不可接受的。在目前的情况下完全无用。

    在意识到这一点之后 Cast<Type> 在抛出异常而不是使用我为该类型定义的强制转换运算符时,我变得很恼火,发现了这个线程。如果它被定义为 IEnumerable ,为什么他们不直接实现它,使用反射来获取对象的类型,获取目标类型,发现任何可用的静态转换运算符,并找到一个合适的运算符来进行强制转换。它可能会产生异质性 IEnumerable 进入A IEnumerable<T> .

    以下实现是一个可行的想法。..

    public static class EnumerableMinusWTF
    {
        public static IEnumerable<TResult> Cast<TResult,TSource>(this IEnumerable<TSource> source)
        {
            Type source_type = typeof(TSource);
            Type target_type = typeof(TResult);
    
            List<MethodInfo> methods = new List<MethodInfo>();
            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<TResult> results = new List<TResult>();
            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<MethodInfo> 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 + "." );
        }
    }
    

    然后,我可以成功调用此代码:

        //LessonID inherits RegexConstrainedString, and has explicit conversion operator defined to convert string to LessonID
    List<string> lessons = new List<String>(new string[] {"l001,l002"});
    IEnumerable<LessonID> constrained_lessons = lessons.Cast<LessonID, string>();
    
        6
  •  2
  •   Triynko    16 年前

    以下是一些需要考虑的事情。..

    1. 你想转换还是转换?
    2. 你想得到结果吗 List<T> 或a IEnumerable<T> .
    3. 如果结果是 IEnumerable<T> ,您是否希望延迟应用强制转换/转换(即强制转换/转化在迭代器到达每个元素之前实际上不会发生)?

    强制转换/转换之间的有用区别,因为强制转换运算符通常涉及构造新对象,可以被视为转换:
    “Cast”实现应自动应用为所涉及类型定义的转换运算符; 可以构造新对象,也可以不构造新对象 .
    “Convert”实现应允许指定 System.Converter<TInput,TOutput> 代表。

    潜在的方法头:

    List<TOutput> Cast<TInput,TOutput>(IEnumerable<TInput> input);
    List<TOutput> Convert<TInput,TOutput>(IEnumerable<TInput> input, Converter<TInput,TOutput> converter);
    IEnumerable<TOutput> Cast<TInput,TOutput>(IEnumerable<TInput> input);
    IEnumerable<TOutput> Convert<TInput,TOutput>(IEnumerable<TInput> input, Converter<TInput,TOutput> converter);
    

    使用现有框架的“Cast”实现存在问题;假设您将a作为输入传递 List<string> ,您想使用前面的任何方法进行转换。

    //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,TOutput>( (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<TOuput>();
    

    有问题的“转换”实现

    //Again, the cast to a generic type parameter not possible in c#; also, this requires a List<T> as input instead of just an IEnumerable<T>.
    list.ConvertAll<TOutput>( new Converter<TInput,TOuput>( (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<TOutput>(
        (Converter<TInput,TOutput>)Delegate.CreateDelegate(
            typeof(Converter<TInput,TOutput>),
            typeof(TOutput).GetMethod( "op_Explicit", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public )
        )
    );
    

    摘要:
    Cast/Convert方法应包含已定义的显式转换运算符,或允许指定转换委托。C#对转换运算符的语言规范——特别是它们缺少方法名——使得除了通过反射之外很难获得委托。另一种方法是封装或复制转换代码,不必要地增加代码的(维护)复杂性,因为实际上, 可能/允许 转换是隐式的,无论是否存在转换运算符,都应该由编译器处理。我们不应该手动搜索具有反射的适当转换运算符的神秘命名定义(例如“op_Explicit”) 运行时间 关于所涉及的类型。此外,使用显式转换运算符进行批量/列表转换的Cast/Convert方法应该是一个框架功能,并且 List.ConvertAll<T> ,他们是。..除了语言规范使得很难有效地获得转换运算符的委托!!!

        7
  •  1
  •   Jeppe Stig Nielsen    12 年前

    当然,明智的做法是使用 Select(i => (long)i) 这就是我建议用于内置值类型之间的转换和用户定义的转换。

    但正如 好奇的 评论,因为。NET 4可以创建自己的扩展方法,该方法也适用于这些类型的转换。但它要求你愿意使用 dynamic 关键字。事情很简单:

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

    正如我所说的,它适用于整数转换(缩窄或加宽转换)、浮点类型之间的数值转换以及这类转换“方法” implicit operator explicit operator .

    当然,它仍然适用于旧的引用转换和拆箱转换,就像原始的一样 System.Enumerable.Cast<TResult> .

    推荐文章