I’m slightly baffled by some behaviour I don’t understand from the compiler. I’ve reduced it to the following code sample:
public interface IFoo { }
public interface IBar<T> : IFoo { }
public delegate void DHandler<T>(IBar<T> arg);
public static class Demo
{
static void Garply<T>(DHandler<T> handler) { }
public static void DoStuffWithInt()
{
Garply<int>(Handler);
}
static void Handler(IFoo arg) { }
}
My problem is that I don’t expect the code to compile, but it does. I don’t expect it to compile because DHandler<int> requires IBar<int> in the signature, but the Handler method declares IFoo, which is not IBar<int> (although the converse is true). Hence Handler is not a DHandler<int> and so a delegate to it wouldn’t be usable as an argument to a Garply<int> call.
If I change the code to read Handler(IBar<int> arg) it compiles. If I change it to read Handler(IBar<string> arg) it doesn’t. Both those behaviours are as I’d expect.
The practical problem that prompts this question is that when the signature is Handler(IBar<int> arg), the compiler complains that I need to explicitly specify the type parameter for the Garply call. Trivial in this example, but in the actual code it’ll be a real nuisance. I was mystified, since the argument to Garply is a method with the signature (IBar<int> arg), so the delegate to it would be a DHandler<int>, so the chosen Garply<T> would unambiguously be Garply<int>. But apparently the compiler saw an ambiguity. It was investigating that, that led me to the above puzzle and I can only guess that perhaps the compiler is thinking “well, to Jason’s surprise, I’d have accepted an IFoo for this IBar<T>, so a T has to be specified to allow me to know that I should compile it as IBar<T> and not as IFoo“. That might explain why it wants the type parameter. But can anyone cast light on this?
This is delegate variance, which was introduced in C# 2. This is not the same as the generic variance introduced in C# 4.
Here’s an even simpler example:
The point is that we can create an instance of the
Foodelegate from theBarmethod becauseBarwill work provided it’s given anyobject. When aFoodelegate is called, it will always provide astringreference, and there’s a reference conversion fromstringtoobject. So if I have:… that call would always be appropriate for
Bar.Likewise in your case, any call that
Garply<T>makes withhandlerwould definitely be a valid call toHandler– so the compiler is happy to create an appropriateDHandler<T>instance.The problem when
Handleronly accepts anIBar<int>is that the compiler doesn’t use possible method group conversions for arguments when inferring type parameters. This is an area where type inference certainly could be stronger, and indeed it improved – in a very similar area, although I can never remember the details – between C# 3 and C# 4.