Загадочный Enumerable.Cast InvalidCastException - PullRequest
44 голосов
/ 15 января 2009

следующие броски InvalidCastException.

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

Почему?

Я использую Visual Studio 2008 SP1.

Ответы [ 7 ]

59 голосов
/ 15 января 2009

Это очень странно! Есть запись в блоге здесь , которая описывает, как изменилось поведение Cast<T>() между .NET 3.5 и .NET 3.5 с пакетом обновления 1 (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);

Это работает, но в первую очередь не объясняет ошибку. Я попытался привести список к короткому и плавающему, и те бросили то же исключение.

Редактировать

Этот пост в блоге объясняет, почему он не работает!

Cast<T>() - это метод расширения для IEnumerable, а не IEnumerable<T>. Это означает, что к тому моменту, когда каждое значение достигает точки, где оно преобразуется, оно уже помещается обратно в System.Object. По сути, он пытается сделать это:

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

Этот код вызывает исключение InvalidCastException, которое вы получаете. Если вы попытаетесь привести int напрямую к long, все в порядке, но приведение упакованного int обратно к long не работает.

Конечно, странность!

27 голосов
/ 15 января 2009

Метод 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)
3 голосов
/ 09 апреля 2009

Я снова за этим!
Вот решение всех ваших проблем с конверсией 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;}
        }
    }
}
3 голосов
/ 15 января 2009

Хм ... интересная головоломка. Тем более интересно, что я только что запустил его в Visual Studio 2008, и он не выдал вообще.

Я не пользуюсь Service Pack 1, и вы можете использовать его, так что это может быть проблемой. Я знаю, что в .Cast () в выпуске SP1 были некоторые «улучшения производительности», которые могли быть причиной проблемы. Некоторое чтение:

Запись в блоге 1

Запись в блоге 2

2 голосов
/ 08 апреля 2009

Вот о чем подумать ...

  1. Вы хотите разыграть или преобразовать?
  2. Хотите ли вы результат как List<T> или IEnumerable<T>.
  3. Если результатом является IEnumerable<T>, хотите ли вы, чтобы приведение / преобразование применялось лениво (т.е. преобразование / преобразование фактически не будет происходить, пока итератор не достигнет каждого элемента)?

Полезное различие между приведением / преобразованием, поскольку оператор приведения часто включает создание нового объекта и может рассматриваться как преобразование:
Реализации «приведения» должны автоматически применять операторы преобразования, определенные для задействованных типов; новый объект может или не может быть построен .
Реализации "преобразования" должны позволять указывать 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" с использованием существующего фреймворка; Предположим, вы передаете в качестве входных данных 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 )
    )
);

Резюме:
Методы преобразования / преобразования должны включать определенные явные операторы преобразования или позволять указывать делегата преобразования. Спецификация языка C # для операторов преобразования - в частности, из-за отсутствия имени метода - затрудняет получение делегата, кроме как через отражение. Альтернатива заключается в инкапсуляции или репликации кода преобразования, что излишне увеличивает сложность (обслуживания) вашего кода, поскольку действительно возможные / разрешенные преобразования подразумеваются при наличии или отсутствии операторов преобразования и должны обрабатываться. компилятором. Нам не нужно вручную искать зашифрованные определения (например, «op_Explicit») соответствующих операторов преобразования с отражением в ВРЕМЯ РАБОТЫ о задействованных типах. Кроме того, методы Cast / Convert для массовых / списочных преобразований, использующих явные операторы преобразования, действительно должны быть фреймворком, а с List.ConvertAll<T> они ... за исключением того, что спецификация языка затрудняет эффективное получение делегата для операторов преобразования! !!

2 голосов
/ 08 апреля 2009

Хотелось бы, чтобы они могли сделать что-то умное, например, использовать любые неявные или явные операторы приведения, определенные для типа. Текущее поведение и несоответствие недопустимы. Абсолютно бесполезен в своем нынешнем состоянии.

Поняв, что Cast<Type> выдает исключение вместо использования операторов приведения, которые я определил для типа, я разозлился и нашел этот поток. Если он определен для IEnumerable, почему бы ему просто не реализовать его, чтобы использовать отражение, чтобы получить тип объекта, получить целевой тип, обнаружить любые доступные операторы статического преобразования и найти подходящий для выполнения приведения. Это может привести к неоднородности IEnumerable в 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>();
1 голос
/ 21 ноября 2012

Конечно, разумно использовать 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>.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...