Home > database >  Cannot cast to base generic type in C# but can use 'as' operator
Cannot cast to base generic type in C# but can use 'as' operator

Time:10-20

why casts don't work on constrained generic type as shown below?

class B { }

class B1 : B { }

class G<T> where T : B
{ 
    void x()
    {
        T b1 = new B1();    // why implicit conversion doesn't compile?
        T b2 = (T)new B1(); // why explicit conversion doesn't compile either?
        T b3 = new B1() as T;  // this works!
    }
}

CodePudding user response:

B1 is not constrained to be assignable to T. For example:

void Main()
{
    new G<C1>().x();
}

class B { }

class B1 : B { }

class C1 : B { }

class G<T> where T : B
{ 
    public void x()
    {
        T b3 = new B1() as T;
        
        b3.Dump(); // null, because B1 cannot be converted to C1
    }
}

Just because T is constrained to be a B doesn't mean you can cast any descendant of B to any possible T.

Why not allow that anyway? In my thinking, it doesn't make any sense. If you wanted to make sure B1 is assignable to T, you shouldn't be using generics. It's way too easy to make this kind of mistake, and if possible, you should avoid casting around generics in the first place. They're made (primarily) to make static typing more powerful, while keeping the type safety (and performance benefits).

However, there's definitely obviously wrong cases that don't get caught, because

T b2 = (T)new B();

does compile, even though it actually has the same problem and you will get a runtime cast error if T isn't B.

Of course, in cases like this, it's helpful to check the C# specification, and clearly enough, that says:

The above rules do not permit a direct explicit conversion from an unconstrained type parameter to a non-interface type, which might be surprising. The reason for this rule is to prevent confusion and make the semantics of such conversions clear.

While this only really seems to make sense for value-types, this explains both why you can do a direct cast of (T)new B();, and why you cannot do (T)new B1(); - even though both have the same problem with T not necessarily being B.

Remember, operators in C# are not virtual - they depend on the compile-time type of the expressions. For value type arguments, you actually get a variant for each value type you use (i.e. List<long> uses different code than List<int>) - so you get the correct cast, like when casting from int to long, you get a long with the same value as the int, rather than a casting error.

For reference types, this isn't true. In your case, you could have a custom cast operator from B to B that would actually be invoked in the (T) new B() case, but not a cast from B1 to B, because the reified generic type for G<B> and G<B1> is actually the same. Since this is an implementation detail that can change at any time, you really want to avoid the confusion and potential change of behaviour.

CodePudding user response:

T b1 = new B1();    // why implicit conversion doesn't compile?

Just because both T and B1 are derived from B doesn't mean B1 is assignable to T. They both are assignable to B, so

B b1 = new B1();  // should work

.

T b2 = (T)new B1(); // why explicit conversion doesn't compile either?

As explained above, two classes with a common base class do not ensure type compatibility so explicit casting won't work either

   T b3 = new B1() as T;  // this works!

it works but you probably assign null to b3 since B1 is not convertible to T. The as operator just returns null instead of emitting a compiler warning or throwing an exception.

CodePudding user response:

The answer is, because there are no guarantees you supplied B1 as the generic parameter, you can't do this with generics, lets explore the reason why...

Given

class Animal { }
class Dog : Animal { }
class Cat : Animal { }

class Something<T> where T : Animal
{ 
   public T Animal {get;set;}

   void x()
   {
       Animal = (T)new Cat(); 
   }
}

Now, what if you used your class like this

var dog = new Something<Dog>();

dog.X() // internally you are trying to cast a Cat to a Dog

Dog dog = dog.Animal; // now you just tried to mash a Cat into a dog
Dog.Bark() // what the...

Constraints are a minimum contract, that's it. They allow you to use the generic parameter on that contract you specified.

Its exactly the same as why you cant do the following

Dog dog = new Cat();

Even if they both inherit from Animal, does not mean they are the same... They have a different memory layout internally, they have different methods and properties, they cant be mashed together statically typed like this.

In short, you likely need to rethink your problem.

  • Related