Cast<T> для набора элементов приводящихся к Т

148
12 июня 2019, 05:40

Есть класс, содержащий оператор приведения типа int к типу этого класса

class Item
{
    public int ID;
    public static implicit operator Item(int id)
    {
        return new Item { ID = id };
    }
}

Если написать так

int id = 1;
Item item = id;

то приведение типа срабатывает.

Теперь я хочу набор int-ов преобразовать в набор Item-ов, соответственно я делаю

int[] ids = new int[] { 1, 2, 3 };
Item[] items = ids.Cast<Item>().ToArray();

и получаю

An unhandled exception of type 'System.InvalidCastException' occurred in System.Core.dll Additional information: Unable to cast object of type 'System.Int32' to type 'Item'.

Замена implicit на explicit не помогла.

Как это решить наиболее изящно, и (главное) почему Cast<T> не срабатывает?

Answer 1

Cast не срабатывает, потому что, если открыть его исходник, мы увидим следующее:

static IEnumerable<TResult> CastIterator<TResult>(IEnumerable source) {
    foreach (object obj in source) yield return (TResult)obj;
}

т.е. по сути это эквивалентно

int id = 1;
Item item = (Item)(object)id;

что, естественно, не срабатывает, т. к. object нельзя преобразовать в ваш тип, да и оператор для явного/неявного преобразования из object вообще запрещено создавать.

Для того, чтобы ваш оператор использовался, компилятор должен видеть его и генерировать вызов при компиляции:

int[] ids = new int[] { 1, 2, 3 };
Item[] items = ids.Select(i => (Item)i).ToArray();

с уже скомпилированными сборками это не сработает.

PS:

Вы, конечно, можете попытаться написать свой метод для каста:

public static class MyExtensions
{
    public static IEnumerable<TOut> MyCast<TIn, TOut>(this IEnumerable<TIn> sourse)
    {
        foreach (TIn e in sourse) yield return (TOut)e;
    }
}

Но это не скомпилируется, т. к. нельзя произвольный тип TIn преобразовать в произвольный TOut.

Вы можете попытаться добавить ограничение на типы:

public static class MyExtensions
{
    public static IEnumerable<TOut> MyCast<TIn, TOut>(
        this IEnumerable<TIn> sourse) where TOut : TIn
    {
        foreach (TIn e in sourse) yield return (TOut)e;
    }
}

и этот класс даже скомпилируется, но вы не сможете им воспользоваться:

Item[] items = ids.MyCast<int, Item>().ToArray();

потому что Item не является (наследником) int.

Ну и, конечно, если уж вам на столько сильно нужно решение этой задачи, что вы готовы воспользоваться рефлексией или динамической типизацией, то можно придумать какое-то такое решение, которое найдет метод по его сигнатуре, создаст и него делегат и закеширует его:

public static class MyExtensions
{
    public static IEnumerable<TOut> MyCast<TIn, TOut>(this IEnumerable<TIn> sourse)
    {
        var castMethod = CastImpl<TIn, TOut>.CastMethod;
        foreach (TIn e in sourse) yield return castMethod(e);
    }
    static class CastImpl<TIn, TOut>
    {
        public static readonly Func<TIn, TOut> CastMethod;
        static CastImpl()
        {
            var attr = MethodAttributes.Static
                     | MethodAttributes.Public
                     | MethodAttributes.HideBySig
                     | MethodAttributes.SpecialName;
            var names = new[] { "op_Implicit", "op_Explicit" };
            var method = typeof(TIn).GetMethods()
                .Concat(typeof(TOut).GetMethods())
                .Where(mi => mi.Attributes == attr
                    && names.Contains(mi.Name)
                    && mi.ReturnType == typeof(TOut)
                    && mi.GetParameters() is ParameterInfo[] pi
                    && pi.Length == 1
                    && pi[0].ParameterType == typeof(TIn))
                .FirstOrDefault();
            if (method == null) throw new NotSupportedException(
                $"Cast {typeof(TIn)} => {typeof(TOut)} not supported!");
            CastMethod = (Func<TIn, TOut>)method.CreateDelegate(typeof(Func<TIn, TOut>));
        }
    }
}

Тогда, конечно, это будет работать:

int[] ids = new int[] { 1, 2, 3 };
Item[] items = ids.MyCast<int, Item>().ToArray();

То же самое с использованием Expression будет выглядеть короче, за счет того, что метод для конвертации будет найден автоматически:

public static class MyExtensions
{
    public static IEnumerable<TOut> MyCast<TIn, TOut>(this IEnumerable<TIn> sourse)
    {
        var castMethod = CastImpl<TIn, TOut>.CastMethod;
        foreach (TIn e in sourse) yield return castMethod(e);
    }
    static class CastImpl<TIn, TOut>
    {
        public static readonly Func<TIn, TOut> CastMethod;
        static CastImpl()
        {
            var param = Expression.Parameter(typeof(TIn), "i");
            var body = Expression.Convert(param, typeof(TOut));
            var lambda = Expression.Lambda(body, new[] { param });
            CastMethod = (Func<TIn, TOut>)lambda.Compile();
        }
    }
}

Причем этот метод получается даже более универсальным, в нем сработает и привычный каст (в отличие от предыдущего):

var cars = items.MyCast<Vehicle, Car>();
Answer 2
int[] ids = new int[] { 1, 2, 3 };
var test = Array.ConvertAll(ids, (p => (Item)p));//Вариант 1
var test2 = ids.Select(p => (Item)p).ToArray();  //Вариант 2

Проверял на твоем же коде.

Абсолютно успешно срабатывают оба. Выбирай любой который тебе больше по душе.

Не забудь подключить неймспейс: using System.Linq

READ ALSO
Проксирование запроса в cqrs приложениях

Проксирование запроса в cqrs приложениях

Пишу тестовый веб-сайт, где пробую разные аспекты CQRS (сначала это была самопальная реализация cqrs, потом попробовал MediatR)

97
Как удалить подсветку Label в Menu?

Как удалить подсветку Label в Menu?

В меню вставлен LabelПодсвечивается голубым цветом

162
Как использовать select с массивами в c# (mysql)?

Как использовать select с массивами в c# (mysql)?

У меня имеется база данных и мне надо отсортировать данные в нейЯ создал массив строк и хочу сравнить их с нужным столбцом в каждой строке...

134
Как узнать внешний IP-адрес клиента?

Как узнать внешний IP-адрес клиента?

Можно ли узнать с каким IP пользователь выходит в глобальную сеть? Использование сервисов не вариант потому, что например если он использует...

100