Подтвердить что ты не робот

Дженерики: кастинг и типы значений, почему это незаконно?

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

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

Ошибка:

annot преобразует тип '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 и интернет для ответа на этот вопрос и нашел множество объяснений по подобным общим вопросам, связанным с кастингом, но я не могу найти ничего в этом конкретном простом случае.

4b9b3361

Ответ 1

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

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

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

Похоже, что, возможно, больше работы и меньше производительности, чем вы ожидали получить от дженериков, поэтому мы просто объявляем ее вне закона. Если вы действительно хотите, чтобы компилятор снова запустился и выполнил анализ типов, используйте "dynamic" в С# 4; что он делает.

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

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

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

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

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

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

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

Ответ 2

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

  • вентиляционный
  • опущенными
  • бокс
  • распаковка
  • числовое преобразование
  • пользовательское преобразование

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

Вместо написания return (TCastTo)(object)i; вы вынуждаете компилятор делать upcast до 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; }
}

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

Ответ 3

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

Пример ошибки во время выполнения избегает ошибки в вашем первом примере, сначала введя TSource я в объект, откуда все объекты на С#. В то время как компилятор не жалуется (поскольку объект → что-то, что вытекает из него, может быть действительным), поведение кастинга через (синтаксис) переменной будет бросаться, если приведение недействительно. (Та же проблема, что и в примере 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);

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