Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generics: casting and value types, why is this illegal?

Why is this a compile time error?

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

Error:

Cannot convert type 'TSource' to 'TCastTo'

And why is this a runtime error?

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

I've searched SO and the internet for an answer to this and found lots of explanations on similar generic related casting issues, but I can't find anything on this particular simple case.

like image 768
Matt Greer Avatar asked Apr 07 '11 21:04

Matt Greer


People also ask

Which of the following type casting will be illegal?

Explanation: Conversion of a float to pointer type is not allowed.

How does a typecast affect a value of primitive type?

Casting between primitive types enables you to convert the value of one type to another primitive type. This most commonly occurs with the numeric types. But one primitive type can never be used in a cast. Boolean values must be either true or false and cannot be used in a casting operation.

What is datatype casting?

A data type that can be changed to another data type is castable from the source data type to the target data type. The casting of one data type to another can occur implicitly or explicitly. The cast functions or CAST specification (see CAST specification) can be used to explicitly change a data type.


2 Answers

Why is this a compile time error?

The problem is that every possible combination of value types has different rules for what a cast means. Casting a 64 bit double to a 16 bit int is completely different code from casting a decimal to a float, and so on. The number of possibilities is enormous. So think like the compiler. What code is the compiler supposed to generate for your program?

The compiler would have to generate code that starts the compiler again at runtime, does a fresh analysis of the types, and dynamically emits the appropriate code.

That seems like perhaps more work and less performance than you expected to get with generics, so we simply outlaw it. If what you really want is for the compiler to start up again and do an analysis of the types, use "dynamic" in C# 4; that's what it does.

And why is this a runtime error?

Same reason.

A boxed int may only be unboxed to int (or int?), for the same reason as above; if the CLR tried to do every possible conversion from a boxed value type to every other possible value type then essentially it has to run a compiler again at runtime. That would be unexpectedly slow.

So why is it not an error for reference types?

Because every reference type conversion is the same as every other reference type conversion: you interrogate the object to see if it is derived from or identical to the desired type. If it's not, you throw an exception (if doing a cast) or result in null/false (if using the "as/is" operators). The rules are consistent for reference types in a way that they are not for value types. Remember reference types know their own type. Value types do not; with value types, the variable doing the storage is the only thing that knows the type semantics that apply to those bits. Value types contain their values and no additional information. Reference types contain their values plus lots of extra data.

For more information see my article on the subject:

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

like image 120
Eric Lippert Avatar answered Sep 27 '22 20:09

Eric Lippert


C# uses one cast syntax for multiple different underlying operations:

  • upcast
  • downcast
  • boxing
  • unboxing
  • numeric conversion
  • user-defined conversion

In generic context, the compiler has no way of knowing which of those is correct, and they all generate different MSIL, so it bails out.

By writing return (TCastTo)(object)i; instead, you force the compiler to do an upcast to object, followed by a downcast to TCastTo. The compiler will generate code, but if that wasn't the right way to convert the types in question, you'll get a runtime error.


Code Sample:

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; }
}

Eric's way sure is shorter, but I think mine should be faster.

like image 28
Ben Voigt Avatar answered Sep 27 '22 20:09

Ben Voigt