Дженерики: типы приведения и значения, почему это незаконно? - PullRequest
12 голосов
/ 08 апреля 2011

Почему это ошибка времени компиляции?

public TCastTo CastMe<TSource, TCastTo>(TSource i)
{
     return (TCastTo)i;
}

Ошибка:

Не конвертировать тип 'TSource' в 'TCastTo'

И почему это ошибка во время выполнения?

public TCastTo CastMe<TSource, TCastTo>(TSource i)
{
     return (TCastTo)(object)i;
}

int a = 4;
long b = CastMe<int, long>(a); // InvalidCastException

// this contrived example works
int aa = 4;
int bb = CastMe<int, int>(aa);

// this also works, the problem is limited to value types
string s = "foo";
object o = CastMe<string, object>(s);

Я искал в SO и интернете ответ на этот вопрос и нашел множество объяснений по сходным родственным вопросам, связанным с кастингом, но ничего не могу найти наэтот конкретный простой случай.

Ответы [ 3 ]

23 голосов
/ 08 апреля 2011

Почему это ошибка времени компиляции?

Проблема в том, что каждая возможная комбинация типов значений имеет различные правила для того, что означает приведение.Приведение 64-битного двойного к 16-битному целому - это совершенно другой код, от преобразования десятичного числа к плавающему и так далее.Количество возможностей огромно.Так что думай как компилятор. Какой код должен сгенерировать компилятор для вашей программы?

Компилятор должен будет сгенерировать код, который снова запускает компилятор во время выполнения, выполняет новый анализ типов,и динамически генерирует соответствующий код .

Возможно, это больше работы и меньше производительности, чем вы ожидаете получить с помощью дженериков, поэтому мы просто запрещаем его.Если вы действительно хотите, чтобы компилятор снова запустился и провел анализ типов, используйте «динамический» в C # 4;это то, что он делает.

И почему это ошибка времени выполнения?

По той же причине.

Штриховка int может быть распакована только в int (или int?) По той же причине, что и выше;если CLR пытается выполнить все возможные преобразования из упакованного в тип значения в любой другой возможный тип значения, то по существу он должен снова запустить компилятор во время выполнения .Это было бы неожиданно медленно.

Так почему же это не ошибка для ссылочных типов?

Поскольку каждое преобразование ссылочного типа совпадает с любой другой ссылкойпреобразование типа : вы запрашиваете объект, чтобы определить, является ли он производным от заданного типа или идентичен ему.Если это не так, вы генерируете исключение (если выполняете приведение) или приводите к нулевому / ложному (если используете операторы "как / есть").Правила согласуются для ссылочных типов так, как они не относятся к типам значений.Помните, ссылочные типы знают свой собственный тип .Типы значений не имеют;с типами значений переменная, выполняющая хранение, является единственной вещью, которая знает семантику типов, которая применяется к этим битам .Типы значений содержат свои значения и не содержат никакой дополнительной информации .Типы ссылок содержат свои значения и множество дополнительных данных.

Для получения дополнительной информации см. Мою статью на эту тему:

http://ericlippert.com/2009/03/03/representation-and-identity/

7 голосов
/ 08 апреля 2011

C # использует один синтаксис приведения для нескольких различных базовых операций:

  • upcast
  • downcast
  • boxing
  • unboxing
  • числовое преобразование
  • пользовательское преобразование

В общем контексте у компилятора нет возможности узнать, какой из них является правильным, и все они генерируют разные MSIL, поэтому он выручает.

Вместо этого, написав return (TCastTo)(object)i;, вы заставляете компилятор выполнять повышение до object, а затем понижать до TCastTo.Компилятор сгенерирует код, но если это неправильный способ преобразования рассматриваемых типов, вы получите ошибку времени выполнения.


Пример кода:

public static class DefaultConverter<TInput, TOutput>
{
    private static Converter<TInput, TOutput> cached;

    static DefaultConverter()
    {
        ParameterExpression p = Expression.Parameter(typeof(TSource));
        cached = Expression.Lambda<Converter<TSource, TCastTo>(Expression.Convert(p, typeof(TCastTo), p).Compile();
    }

    public static Converter<TInput, TOutput> Instance { return cached; }
}

public static class DefaultConverter<TOutput>
{
     public static TOutput ConvertBen<TInput>(TInput from) { return DefaultConverter<TInput, TOutput>.Instance.Invoke(from); }
     public static TOutput ConvertEric(dynamic from) { return from; }
}

Конечно, путь Эрика короче, но я думаю, что мой путь должен быть быстрее.

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

Ошибка компиляции вызвана тем, что TSource не может быть неявно приведен к TCastTo.Эти два типа могут совместно использовать ветку в их дереве наследования, но нет никакой гарантии.Если вы хотите вызывать только типы, которые имеют общий предок, вы должны изменить сигнатуру CastMe (), чтобы использовать тип предка вместо универсальных.

Пример ошибки времени выполнения позволяет избежать ошибки в вашем первом примере при первом приведенииTSource i для объекта, от которого происходят все объекты в C #.Хотя компилятор не жалуется (потому что объект -> что-то, производное от него, может быть допустимым), поведение приведения через синтаксис переменной (Type) будет выбрасываться, если приведение недопустимо.(Та же самая проблема, которую компилятор не допустил в примере 1).

Другое решение, которое делает нечто похожее на то, что вы ищете ...

    public static T2 CastTo<T, T2>(T input, Func<T, T2> convert)
    {
        return convert(input);
    }

Вы быНазовите это так.

int a = 314;
long b = CastTo(a, i=>(long)i);

Надеюсь, это поможет.

...