【问题标题】:Puzzling Enumerable.Cast InvalidCastException令人费解的 Enumerable.Cast InvalidCastException
【发布时间】:2010-10-01 12:31:50
【问题描述】:

以下抛出InvalidCastException

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

为什么?

我使用的是 Visual Studio 2008 SP1。

【问题讨论】:

  • 我很惊讶没有一个答案真正回答了为什么这个问题。答案是因为从 int 到 long 的转换不是强制转换。这是一个转换。不幸的是,C# 对这两者使用相同的语法,因为它只会使人感到困惑(显然)。您也不能使用.Cast&lt;T&gt;() 调用自定义的显式转换运算符,因为这也不是强制转换。
  • @Timwi:抱歉,没有。问题是装箱,(long)o 其中o 是装箱的int 将抛出。如果这里以某种方式避免拳击,这将不会抛出。此外,强制转换是一种语言结构,而转换是由该结构调用的运行时行为。
  • 你不能使用.Cast&lt;T&gt;在不同的整数类型之间进行转换(new int[] { 1 }.Cast&lt;long&gt;() throws),你也不能使用它来调用显式转换运算符(new XAttribute[] { new XAttribute("X", "Y") }.Cast&lt;string&gt;() throws 即使@ 987654330@ 没有)。即使装箱解释了前者(它没有 - 没有需要在那里装箱;如果内部实现使用它,它会无偿使用它),它也没有解释后者(它是所有引用类型)。
  • 此外,当您以两种不同的含义使用“语言结构”一词时,请注意。当然有强制转换syntax,它是由语言定义的,并且对于所有三个操作都是相同的,但是语言规范也定义了该语法的语义,并且它明确指出“数字转换”、“隐式/显式转换运算符”和“引用转换”(强制转换)是三个独立的操作。

标签: c# .net exception


【解决方案1】:

当然,明智的做法是使用Select(i =&gt; (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 operatorexplicit operator 类型的转换“方法”。

当然,它仍然适用于良好的旧参考转换和拆箱转换,如原始 System.Enumerable.Cast&lt;TResult&gt;

【讨论】:

  • 如果您使用dynamic,预计性能会受到影响,因为对使用哪种转换的分析必须在运行时完成。这就是为什么推荐.Select(i =&gt; (long)i)
【解决方案2】:

我又来了!
这是您所有List&lt;T&gt;Enumerable&lt;T&gt; 转换问题的解决方案。 约 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;}
        }
    }
}

【讨论】:

    【解决方案3】:

    这里有一些事情要考虑......

    1. 您要转换还是转换?
    2. 您希望结果为List&lt;T&gt; 还是IEnumerable&lt;T&gt;
    3. 如果结果是 IEnumerable&lt;T&gt;,您是否希望延迟应用强制转换/转换(即,直到迭代器到达每个元素时才会真正发生强制转换/转换)?

    转换/转换之间有用的区别,因为转换运算符通常涉及构造新对象,并且可以被视为转换:
    “Cast”实现应该自动应用为所涉及的类型定义的转换运算符; 可能会或可能不会构造一个新对象
    “转换”实现应该允许指定一个System.Converter&lt;TInput,TOutput&gt; 委托。

    可能的方法标头:

    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”实现;假设您将 List&lt;string&gt; 作为输入传递,您想使用之前的任何方法进行转换。

    //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”),并在RUN TIME对所涉及的类型进行反射。此外,使用显式转换运算符进行批量/列表转换的 Cast/Convert 方法确实应该是一个框架功能,并且对于 List.ConvertAll&lt;T&gt;,它们是......除了语言规范使得很难有效地获得转换运算符的委托! !!

    【讨论】:

    • 在“有问题的转换实现”下,我使用 typeof(TOutput) 来获取转换运算符的 MethodInfo,但它可以在 typeof(TInput) 或 niether 上定义,因此这种理想方法的实际逻辑将变得更复杂。我的重点是,它不应该那么复杂,也不应该在运行时完成。
    【解决方案4】:

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

    在意识到Cast&lt;Type&gt; 抛出异常而不是使用我为该类型定义的强制转换运算符后,我很生气并找到了这个线程。如果它是为IEnumerable 定义的,他们为什么不直接实现它以使用反射来获取对象的类型、获取目标类型、发现任何可用的静态转换运算符,并找到合适的进行转换。它可以将异构的IEnumerable 转换为IEnumerable&lt;T&gt;

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

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

    【讨论】:

    • 请注意,这对于原始问题(int 到 long)中的转换完全没有帮助,这些转换是由语言定义的。
    • 确实如此。我对 .net 和 c# 越来越恼火,因为有很多方法可以转换事物,但没有一种采用明显的方法来应用已定义的显式转换运算符。此外,反射似乎是一般应用显式转换运算符的唯一方法。
    【解决方案5】:

    Enumerable.Cast 方法定义如下:

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

    并且没有关于 IEnumerable 项目的初始类型的信息,所以我认为您的每个 int 最初都是通过装箱转换为 System.Object,然后尝试将其拆箱为长变量,这是不正确的。

    重现此的类似代码:

    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)
    

    【讨论】:

      【解决方案6】:

      这很奇怪!有一篇博客文章 here 描述了 Cast&lt;T&gt;() 的行为在 .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 并且抛出了相同的异常。

      编辑

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

      Cast&lt;T&gt;()IEnumerable 而不是IEnumerable&lt;T&gt; 的扩展方法。这意味着当每个值到达它被强制转换的点时,它已经被装箱回 System.Object 中。本质上它正在尝试这样做:

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

      此代码会引发您得到的 InvalidCastException。如果您尝试将一个 int 直接转换为 long 就可以了,但是将一个装箱的 int 转换回 long 是行不通的。

      当然是个怪人!

      【讨论】:

      • +1 - 我遇到了这个确切的问题并阅读了博客但无法解决。您的编辑清除了它。太好了!
      • 一点也不奇怪。 .Cast&lt;T&gt; 进行 cast。从 int 到 long 的转换不是强制转换。遗憾的是 C# 选择对不同的操作使用相同的语法。
      • @Timwi:抱歉,没有。请参阅我对您上述评论的评论。
      • @Jason:对不起,但是是的。当你不理解这个问题时,你会如此积极地回应,这是一种耻辱。你甚至没有解决我关于显式转换运算符的评论。
      • 如果有人想知道他们是否可以编写自己的 Cast&lt;T&gt; 方法来“修复”这个问题,请参阅我最近的回答。
      【解决方案7】:

      嗯...有趣的谜题。考虑到我只是在 Visual Studio 2008 中运行它并且它没有完全抛出。

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

      Blog Entry 1

      Blog Entry 2

      【讨论】:

      • 是的,从 SP1 开始,使用强制转换而不是转换已“修复”(根据您链接到的第一篇博文)。
      猜你喜欢
      • 1970-01-01
      • 2012-10-31
      • 2012-08-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-10-30
      • 2018-07-02
      相关资源
      最近更新 更多