28

I'm trying to cast a contravariant delegate but for some reason I can only do it using the "as" operator.

interface MyInterface { }
delegate void MyFuncType<in InType>(InType input);

class MyClass<T> where T : MyInterface
{
    public void callDelegate(MyFuncType<MyInterface> func)
    {
        MyFuncType<T> castFunc1 = (MyFuncType <T>) func; //Error
        MyFuncType<T> castFunc2 = func as MyFuncType<T>; 
        MyFuncType<T> castFunc3 = func is MyFuncType<T> ? (MyFuncType<T>)func : (MyFuncType<T>)null; //Error
    }
}

castFunc2 works fine but castFunc1 and castFunc3 cause the error:

Cannot convert type 'delegateCovariance.MyFuncType<myNamespace.MyInterface>' to myNamespace.MyFuncType<T>'

The MSDN article on the as operator states that castFunc2 and castFunc3 are "equivalent" so I don't understand how only one of them could cause an error. Another piece of this that is confusing me is that changing MyInterface from an interface to a class gets rid of the error.

Can anyone help me understand what is going on here? Thanks!

John Saunders
  • 160,644
  • 26
  • 247
  • 397
rob
  • 17,995
  • 12
  • 69
  • 94
  • 2
    This compiles fine for me here: http://ideone.com/5SjUxV Am I missing something? (not the most familiar with ideone). I get the stated error if I remove `in` from the `MyFuncType` declaration. – Paul Bellora Dec 11 '12 at 04:49
  • hmm. I thought that maybe you weren't getting the erros because ideone uses mono but I tried using mono from visual studio and I am still getting the errors. – rob Dec 11 '12 at 05:17
  • @rob I used mono targeting .Net 4 and got no errors. Which version of the .Net framework are you targeting? – Mike Zboray Dec 11 '12 at 05:19
  • @mike z. I am also targeting .Net 4.0. I just did a fresh install of Mono 2.10.8 and followed these instructions for using it from VS http://erictummers.wordpress.com/2012/01/25/target-mono-from-visual-studio/ . Are you saying that you didn't get any errors from Visual Studio or from ideone? – rob Dec 11 '12 at 05:25
  • 1
    @rob I ran it on mono 2.10.8.1 on ubuntu and I received no errors in monodevelop and was able to execute without exception. However, in VS 2010 targeting .Net 4 I do indeed get the compiler errors you cite. – Mike Zboray Dec 11 '12 at 07:53

3 Answers3

16

Add a constraint such that T must be a class.

class MyClass<T> where T: class, MyInterface

This gives the compiler enough information to know that T is convertible. You don't need the explicit cast either.

Variance only applies to reference types. T is allowed to be a value type without the constraint which breaks the compilers ability to prove that T is compatible for contravariance.

The reason the second statement works is because as actually can perform a null conversion. For example:

class SomeClass { }
interface SomeInterface { }
static void Main(string[] args)
{
   SomeClass foo = null;
   SomeInterface bar = foo as SomeInterface;
}

Foo is obviously not directly convertable to SomeInterface, but it still succeeds because a null conversion can still take place. Your MSDN reference may be correct for most scenarios, but the generated IL code is very different which means they are fundamentally different from a technical perspective.

Joshua Enfield
  • 17,642
  • 10
  • 51
  • 98
  • 1
    +1. You could also do this to avoid the compiler error: `MyFuncType castFunc1 = (MyFuncType )(object)func;` but that will blow up at run-time for value types anyway, so this is a better solution. – Eren Ersönmez Dec 11 '12 at 07:51
  • 1
    **Good explanation.** Regarding your last example: Here, with no generics, an explicit cast would also compile. That is `bar = (SomeInterface)foo` would be allowed compile-time. However, when a generic parameter like `T` is involved, the compiler is more strict. This is to help people not mis-use generics and do excessive casting. Note in your `foo`/`bar` example that it is very important that `SomeClass` is unsealed. If it were `sealed` the compiler would know that a `SomeClass` could never be a `SomeInterface`. – Jeppe Stig Nielsen Dec 20 '12 at 17:16
4

Eric Lippert gave a great explanation of this issue in his recent posts: An "is" operator puzzle, part one, An "is" operator puzzle, part two.

Main rationale behind this behavior is following: "is" (or "as") operators are not the same as a cast. "as" operator can result non-null result event if corresponding cast would be illegal, and this is especially true when we're dealing with type arguments.

Basically, cast operator in your case means (as Eric said) that "I know that this value is of the given type, even though the compiler does not know that, the compiler should allow it" or "I know that this value is not of the given type; generate special-purpose, type-specific code to convert a value of one type to a value of a different type."

Later case deals with value conversions like double-to-integer conversion and we can ignore this meaning in current context.

And generic type arguments are not logical in the first context neither. If you're dealing with a generic type argument, than why you're not stating this "contract" clearly by using generic type argument?

I'm not 100% sure what you're want to achieve, but you can omit special type from your method and freely use generic argument instead:

class MyClass<T> where T : MyInterface
{
    public void callDelegate(Action<T> func)
    {
    }
}

class MyClass2
{
    public void callDelegate<T>(Action<T> func)
        where T : MyInterface
    {
    }
}

Otherwise you should use as operator with check for null instead of type check.

Sergey Teplyakov
  • 11,477
  • 34
  • 49
-1

Your class says that T implements MyInterface because MyInterface is not a instance type. Therefore, MyFuncType<T> is not guaranteed to be MyFuncType<MyInterface>. It could be MyFuncType<SomeType> and SomeType : MyInterface, but that wouldn't be the same as SomeOtherType : MyInterface. Make sense?

Paul Bellora
  • 54,340
  • 18
  • 130
  • 181
Jake
  • 82
  • 3
  • "`MyFuncType` is not guaranteed to be `MyFuncType`" That's true, but I think you're confusing casting with assignment. – Paul Bellora Dec 11 '12 at 04:20
  • Thanks for the edit. Didn't realize I messed it up. No confusion on casting and assignment here. My point is that generics are templates and `T` will never be guaranteed to be `MyInterface`. – Jake Dec 11 '12 at 04:36