delegate contravariance not fully implemented

B

Ben Voigt

I've extended the article "Covariance and Contravariance in Delegates" from
the C# Programmers' Guide, and it seems contravariance of delegates is
implemented wrong.

Lambda calculus states that Handler2Method is a subtype of HandlerMethod...
why can't I cast (it should be implicit conversion, actually)? However the
last line does work, but I'm not wanting
to create new delegate objects. I'm actually implementing contravariant
events, and I'm scared about my remove implementation. In

event BaseInterface.baseEvent {
add { derivedEvent += new DerivedDelegate(value); } // add a new
handler, that's ok
remove { derivedEvent -= new DerivedDelegate(value); } // I don't want
to remove a new handler, I want to remove the one I added on the line above!
}

Is removal going to work, or have I created a "new" object that won't match
the existing one????

HELP!




class Mammals
{
}

class Dogs : Mammals
{
}

class Program
{
public delegate void HandlerMethod(Dogs sampleDog);
public delegate void Handler2Method(Mammals sample);

public static void FirstHandler(Mammals elephant)
{
}

public static void SecondHandler(Dogs sheepDog)
{
}

static void Main(string[] args)
{
// Contravariance permits this delegate.
HandlerMethod handler1 = FirstHandler;

HandlerMethod handler2 = SecondHandler;

Handler2Method handler3 = FirstHandler; // ok

HandlerMethod handler4 = handler3; // error CS0029: Cannot
implicitly convert type 'Program.Handler2Method' to 'Program.HandlerMethod'

HandlerMethod handler5 = (HandlerMethod)handler3; // error CS0030:
Cannot convert type 'Program.Handler2Method' to 'Program.HandlerMethod'

HandlerMethod handler6 = new HandlerMethod(handler3); // ok
}
}
 
P

Peter Huang [MSFT]

Hi Ben,

Based on my research, when we try to call the code below, the compile helps
to generate the code that created the delegate.
HandlerMethod handler1 = FirstHandler;


That is to say , the codeline 1 and 2 below will do the same job.
HandlerMethod handler1 = FirstHandler;
HandlerMethod hTest = new HandlerMethod(FirstHandler);
handler1 = hTest;

We can look into the IL code which is the .NET assembly code.
Notice that there will call twice newobj instructions.
That is to say, "HandlerMethod handler1 = FirstHandler;" is an constructor
behavior but not a simple assignment behavior just as code line 3 do.
So code 3 will convert type check.
If the handler1 and hTest is not compatible, the convert will fail. That is
why you will get the error.

method private hidebysig static void Main() cil managed
{
.entrypoint
// Code size 30 (0x1e)
.maxstack 3
.locals init ([0] class TestDelegate.Program/HandlerMethod handler1,
[1] class TestDelegate.Program/HandlerMethod hTest)
IL_0000: nop
IL_0001: ldnull
IL_0002: ldftn class TestDelegate.Mammals
TestDelegate.Program::FirstHandler()
IL_0008: newobj instance void
TestDelegate.Program/HandlerMethod::.ctor(object,

native int)
IL_000d: stloc.0
IL_000e: ldnull
IL_000f: ldftn class TestDelegate.Mammals
TestDelegate.Program::FirstHandler()
IL_0015: newobj instance void
TestDelegate.Program/HandlerMethod::.ctor(object,

native int)
IL_001a: stloc.1
IL_001b: ldloc.1
IL_001c: stloc.0
IL_001d: ret
} // end of method Program::Main



Also you may take a look at the code below.
using System;
using System.Collections.Generic;
using System.Text;

namespace TestDelegate
{
public class EventSource2
{
public delegate Mammals TestEventDelegate();
public TestEventDelegate TestEventHandlers;

public event TestEventDelegate TestEvent
{
add
{
TestEventHandlers += value;

}
remove
{
TestEventHandlers -= value;

}
}
public void RaiseTestEvent()
{
// Safely invoke an event.
TestEventDelegate temp = TestEventHandlers;

if (temp != null)
{
temp();
}
}
}

public class Mammals
{
}

public class Dogs : Mammals
{
}

public class Program
{
// Define the delegate.
public delegate Mammals HandlerMethod();
public static Mammals FirstHandler()
{
Console.WriteLine("FirstHandler");
return null;
}

public static Dogs SecondHandler()
{
Console.WriteLine("SecondHandler");
return null;
}

static void Main()
{
EventSource2 es = new EventSource2();
es.TestEvent += new
EventSource2.TestEventDelegate(FirstHandler);
es.TestEvent += new
EventSource2.TestEventDelegate(SecondHandler);
es.RaiseTestEvent();
es.TestEvent -= new
EventSource2.TestEventDelegate(FirstHandler);
es.RaiseTestEvent();
es.TestEvent -= new
EventSource2.TestEventDelegate(SecondHandler);
es.RaiseTestEvent();
//HandlerMethod handler1 = FirstHandler;
//HandlerMethod hTest = new HandlerMethod(FirstHandler);
//handler1 = hTest;
}
}

}


Best regards,

Peter Huang

Microsoft Online Community Support
==================================================
When responding to posts, please "Reply to Group" via your newsreader so
that others may learn and benefit from your issue.
==================================================
This posting is provided "AS IS" with no warranties, and confers no rights.
 
J

Jon Skeet [C# MVP]

Ben said:
I've extended the article "Covariance and Contravariance in Delegates" from
the C# Programmers' Guide, and it seems contravariance of delegates is
implemented wrong.

Lambda calculus states that Handler2Method is a subtype of HandlerMethod...

(For reference, here are the definitions:
public delegate void HandlerMethod(Dogs sampleDog);
public delegate void Handler2Method(Mammals sample);
)

I don't know about Lambda calculus, but that's certainly not what the
C# spec says. For one thing, I think you've stated it the wrong way
round - HandlerMethod is compatible with the signature (Mammals) but
Handler2Method isn't compatible with the signature (Dogs). In other
words, one might expect to be able to do:

HandlerMethod m1 = ...;
Handler2Method m2 = m1;

but not the other way round - because it's reasonable to call a
Handler2Method with a Dogs, but it's *not* reasonable to call a
HandlerMethod with any old Mammals (eg new Cats()).

Now, that's covariance between a method group and a delegate.
Unfortunately, it doesn't look like the C# spec allows for actual
delegate instances to be converted at all, as far as I can see. Is that
what your issue really is?

Jon
 
?

=?iso-8859-1?Q?Lasse=20V=e5gs=e6ther=20Karlsen?=

event BaseInterface.baseEvent {
add { derivedEvent += new DerivedDelegate(value); } // add a new
handler, that's ok
remove { derivedEvent -= new DerivedDelegate(value); } // I don't
want
to remove a new handler, I want to remove the one I added on the line
above!
}

Is removal going to work, or have I created a "new" object that won't
match the existing one????
<snip>

Removal will work. The .remove method on the event knows how to match this
and will not look at the delegate object itself, but use the target object
and method to match against existing delegates.

In short, the above syntax is the right one.
 
B

Ben Voigt

Jon Skeet said:
(For reference, here are the definitions:
public delegate void HandlerMethod(Dogs sampleDog);
public delegate void Handler2Method(Mammals sample);
)

I don't know about Lambda calculus, but that's certainly not what the
C# spec says. For one thing, I think you've stated it the wrong way
round - HandlerMethod is compatible with the signature (Mammals) but
Handler2Method isn't compatible with the signature (Dogs). In other
words, one might expect to be able to do:

HandlerMethod m1 = ...;
Handler2Method m2 = m1;

but not the other way round - because it's reasonable to call a
Handler2Method with a Dogs, but it's *not* reasonable to call a
HandlerMethod with any old Mammals (eg new Cats()).

Exactly why the assignment above is not permissible.

m1 can be set to a method that requires a Dogs.
m2 can be called with a mammals.
Therefore m2 = m1 is illegal.

However, m2 can only be set to elements that accept any subtype of Mammals
m1 can only be called with a Dogs
Therefore m1 is a subtype of m2, you can always call m2 handlers in an m1
event.
Now, that's covariance between a method group and a delegate.

But subtyping is covariant on return type and contravariant on arguments.
 
J

Jon Skeet [C# MVP]

<snip>

(Jon wrote:)
Exactly why the assignment above is not permissible.

Whoops - yes, I see what you mean. Sorry about that - that's what comes
of posting before work :(

Anyway, back to your previous question - is your issue that there's no
conversion between actual delegates, i.e. the co/contravariance is only
applicable from a method group to a delegate type?
 
B

Ben Voigt

Thanks, that was exactly what I needed to know.... a little
counter-intuitive though.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top