Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cast from Generics<T> to Specific SubClass

Tags:

c#

generics

I have a class as such

public class MyClass<T> where T : OneType
{
   T MyObj { get; set; }

   public MyCLass(T obj)
   {
   }
}

public class SubClass: MyClass<TwoType>
{
}

// snip for other similar class definition

where, TwoType is derived from OneType.

Now, I have this utility method

public static MyClass<T> Factory<T>(T vd)
 where T : OneType
{
   switch(vd.TypeName)
   {
      case Constant.TwoType
          return new SubClass((TwoType)vd);
     // snip for other type check
   }
}

Which function is, obviously, checks the type of vd, and the creates an appropriate MyClass type. The only problem is the above code won't compile, and I don't know why

The error is

Cannot cast expression of T to TwoType

like image 432
Graviton Avatar asked Jan 08 '10 14:01

Graviton


2 Answers

As Grzenio correctly notes, an expression of type T is not convertible to TwoType. The compiler does not know that the expression is guaranteed to be of type TwoType -- that is guaranteed by your "if" statement, but the compiler does not consider the implication of the if statement when analysing types. Rather, the compiler assumes that T can be any type satisfying the constraint, including ThreeType, a type derived from OneType but not TwoType. Obviously there is no conversion from ThreeType to TwoType, and so there is no conversion from T to TwoType either.

You can fool the compiler into allowing it by saying "well, treat the T as object, and then cast the object to TwoType". Every step along the way is legal -- T is convertible to object, and there might be a conversion from object to TwoType, so the compiler allows it.

You'll then get the same problem converting from SubClass to MyClass<T>. Again, you can solve the problem by casting to object first.

However, this code is still wrong:

public static MyClass<T> Factory<T>(T vd) 
 where T:OneType 
{ 
   switch(vd.TypeName) 
   { 
      case Constant.TwoType 
       // WRONG
       return (MyClass<T>)(object)(new SubClass((TwoType)(object)vd)); 
     // snip for other type check 
   } 
} 

Why is it wrong? Well, consider everything that could go wrong here. For example: you say

class AnotherTwoType : TwoType { }
...
x = Factory<TwoType>(new AnotherTwoType());

What happens? We do not call the SubClass constructor because the argument is not exactly of type TwoType, it is of a type derived from TwoType. Instead of a switch, you probably want

public static MyClass<T> Factory<T>(T vd) 
  where T:OneType 
{ 
  if (vd is TwoType)
       // STILL WRONG
       return (MyClass<T>)(object)(new SubClass((TwoType)(object)vd)); 
  // snip for other type check 
} 

This is still wrong. Again, think about what could go wrong:

x = Factory<OneType>(new TwoType());

Now what happens? the argument is TwoType, we make a new SubClass, and then try to convert it to MyClass<OneType>, but there is no conversion from SubClass to MyClass<OneType> so this will crash and die at runtime.

The correct code for your factory is

public static MyClass<T> Factory<T>(T vd) 
  where T:OneType 
{ 
  if (vd is TwoType && typeof(T) == typeof(TwoType))
       return (MyClass<T>)(object)(new SubClass((TwoType)(object)vd)); 
  // snip for other type check 
} 

Is that now all clear? You might consider a different approach entirely; when you need this many casts and runtime type checks to convince the compiler that the code is correct, that's evidence that the whole thing is a bad code smell.

like image 76
Eric Lippert Avatar answered Nov 15 '22 10:11

Eric Lippert


Its not going to work in .Net 3.5 and below - SubClass is not of type MyClass<T> for any T, its only of type MyClass<TwoType>. And the generic classes do not follow the inheritance of their template type, e.g. MyClass<string> is not a subclass of MyClass<object> - they are completely different classes in C#.

Unfortunately I don't know any reasonable way to write your factory method.

like image 45
Grzenio Avatar answered Nov 15 '22 09:11

Grzenio