views:

148

answers:

4

My delegate doens't seem to accept a subclass, I think an example is the easiest.

public class A
{
     public A() { }
}

public class B : A
{
     public B() { }
}

public class Program
{
     private delegate void CallBack(A a);
     private static CallBack callBack = new CallBack(Test);

     public Main(string[] args)
     {
          callBack(new B());
     }

     private static void Test(A a)
     {
          Console.WriteLine("Test()");    
     }

     // Compilation error occurs if Test becomes:
     private static void Test(B a)
     {
          Console.WriteLine("Test()");
     }
 }

When I change Test to accept B it throws a compilation error. Isn't this odd because B extends A?

Compiler error:

No overload for Test matches Callback

Is there a way to make my delegate accept a class that extends A?

+2  A: 

It isn't odd because if you have an object of class C that extends A, it wouldn't make sense to pass to Test() if it only accepts a B. Any method used for a Callback has to accept any A, not just a specific subclass. You would need to change the Callback delegate signature to accept B if you want to Test() to accept B as well.

class C : A {};

Callback callback = Test;

callback(new C()); //what if Test() accepted B???
Mark Cidade
So my only option would be to cast `A` to `B` inside of `Test`?
Kevin
@Kevin: No, it’s not the only option. A better option is to declare the delegate as `void CallBack(B b)`. You can still use the method `Test(A a)` then.
Timwi
@Timwi - I can't do that I'm afraid. I've got a class `Event` that everyone can extend and pass to the delegate.
Kevin
@Kevin: Then how do you expect this to be possible? What do you expect to happen if someone passes a `NutellaEvent` to your callback when your method expects a `LoveLetterEvent`?
Timwi
@Timwi - Well err.. You've got a point there. Thanks for opening my eyes!
Kevin
A: 

C# delegates support both covariance and contravariance, so this should work.

The problem is the overload.

 // this delegate supports contravariance - and subclass of A should work
 delegate void CallBack(A a);

 // however this can't pick up either Test because both could be used
 static CallBack callBack = new CallBack(Test);

Which overload method signature (Test(A a) or Test(B b)) has to be resolved at compile time - however both could apply, so an error is thrown.

You could avoid this by splitting out the overload:

 static void TestA(A a)
 {
      Console.WriteLine("Test(a)");    
 }

 // Compilation error occurs if Test becomes:
 static void TestB(B a)
 {
      Console.WriteLine("Test(b)");
 }

 // this is valid because it's an exact match
 static CallBack callBackA = new CallBack(TestA);

 // this is valid because delegates support contravariance 
 static CallBack callBackB = new CallBack(TestB);

In either case you can pass a B:

// B is subclass of A, so can be passed to TestA
callBackA(new B());

// CallBack supports contravariance, so can call TestB
callBackB(new B());

Given that you have this contravariance, why do you need the overloads?

Keith
@Keith - I'm trying to recreate the eventlistener model of Flash in C# :) - Thanks for the answer! The biggest problem is that users need to be able to extend my `Event` class and pass their class to the delegate.
Kevin
@Keith - contravariance only works when the argument is a **less derived** type than the one in the delegate declaration. You can never assign a method taking an argument `B` to the delegate you've declared. On the other hand, if the delegate was declared to take an argument `B`, you would be able to add a method of argument `A`
Mark H
+1  A: 

It's quite easy to understand. Now we have:

class A { }
class B : A { }

Scenario 1 at the beginning

public delegate void CallBack(A a);
public void Test(A a) { }
CallBack cb = new CallBack(Test);
cb(new A()); //good and easy usage

Scenario 2 CallBack(A a) and Test(B b)

//compile error, because Test(B b) has a smaller argument scope than CallBack
//CallBack cb = new CallBack(Test);

Scenario 3 CallBack(B b) and Test(A a)

CallBack cb = new CallBack(Test);
cb(new A());  //no error, becasue B can convert to A
Danny Chen
@Danny Chen - Thanks for clearing it up. I guess I will have to cast `A` to `B` then, too bad..
Kevin
+5  A: 

Isn't this odd because B extends A?

You have the right idea, but in the wrong direction. Let's consider an example that is easier to reason about:

class Animal {}
class Reptile : Animal {}
class Snake : Reptile {}
class Mammal : Animal {}
class Tiger : Mammal {}
class Giraffe : Mammal {}
delegate void D(Mammal m);
static void DoAnimal(Animal a) {}
static void DoMammal(Mammal m) {}
static void DoTiger(Tiger t) {}

D dm = DoMammal;
dm(new Tiger());

That's clearly legal. dm needs to be a method that takes a Mammal, and it is.

D dt = DoTiger;
dt(new Giraffe());

That's clearly got to be illegal. You cannot assign a method that takes a tiger to a delegate that takes a mammal, because a delegate that takes a mammal can take any mammal, not just a tiger. If this were legal then it would be possible to pass a giraffe to a method that takes a tiger.

What about this?

D da = DoAnimal;
da(new Giraffe());

That's fine. da is a delegate to a method that takes any mammal. A method that takes any animal clearly also takes any mammal. You can assign DoAnimal(Animal) to a delegate D(Mammal) because Mammal extends Animal. You see now how you got the direction of extension backwards?

Return types on the other hand work the way you think they do:

delegate Mammal F();
static Animal GetAnimal() {...}
static Mammal GetMammal() {...}
static Tiger GetTiger() {...}

F fm = GetMammal; 
Mammal m = fm();

No problem there.

F ft = GetTiger;
Mammal t = ft();

No problem there; GetTiger returns a Tiger, so you can assign it to a delegate that requires that its target returns a mammal.

F fa = GetAnimal;
Mammal a = fa();

That's no good. GetAnimal might return a Snake, and now you have a variable typed as Mammal that contains a Snake. This has to be illegal.

This feature is called "covariance and contravariance of member group conversions" and it was introduced in C# 2.0. For more information on this topic see my article on it:

http://blogs.msdn.com/b/ericlippert/archive/2007/10/19/covariance-and-contravariance-in-c-part-three-member-group-conversion-variance.aspx

Eric Lippert