Brent Arias
Brent Arias

Reputation: 30165

A Generic Pass-Through Interface. Is this possible?

What I wouldn't give to have this work:

public interface ICallBack
{
    void Handle<T>(T arg);
}

public class MessageHandler : ICallBack
{
    public void Handle<T>(T arg)
    {
        string name = typeof(T).Name;
        Console.WriteLine(name);
    }

    public void Handle(int arg)
    {
        string name = "wow, an int";
        Console.WriteLine(name);
    }
}

public class Worker
{
    public void DoSomething(ICallBack cb)
    {
        cb.Handle(55);
    }
}

//...
Worker foo = new Worker();
ICallBack boo = new MessageHandler();

//I want this to print "Wow, an int"
foo.DoSomething(boo)

Unfortunately, it calls the generic entry point rather than the "specialized" entry point. Well, thats interfaces for you.

I've also tried the same approach but replacing the int-specific signature with a generic signature that is Mojo specific:

public void Handle<T>(T arg) where T : Mojo {}

I was hoping this would be sufficient to form a "special override" if the argument was of type Mojo. But now the compiler complains that I have two methods with the same signature (one that is Mojo specific, the other open-ended). Well, I was actually hoping it would think it was "the same signature" so that both would fulfill the interface and the "best" would be selected at run-time. Ah well.

In effect, I'm trying to achieve is vaguely similar to "Traits," which are the "else-if-then of C++". I guess it could also be considered a form of "interface signature contravariance."

I'd love to discover there is a special C# keyword that enables this capability, or that it is a featured addition to C# in .net 4.5.

Yes, no? Comments?

Upvotes: 3

Views: 540

Answers (2)

Matthew Watson
Matthew Watson

Reputation: 109567

Try changing your Worker class to this:

public class Worker
{
    public void DoSomething(ICallBack cb)
    {
        ((dynamic)cb).Handle(55);
    }
}

[EDIT]

Just so you know, adding that innocuous-looking "dynamic" severely changes the output code. It effectively invokes the compiler at run-time to do the dynamic thing.

I also draw your attention to the comments and other answers here. I recommend you read them and understand why doing the above might not be such a great idea.

Further, as noted in the answer below, constraining the argument type to ICallBack will still allow runtime errors if the Handle() method is implemented explicitly.

Here's the IL for that simple-looking method:

.method public hidebysig instance void DoSomething(class ConsoleApplication1.ICallBack cb) cil managed
{
    .maxstack 9
    .locals init (
        [0] class [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo[] CS$0$0000)
    L_0000: nop 
    L_0001: ldsfld class [System.Core]System.Runtime.CompilerServices.CallSite`1<class [mscorlib]System.Action`3<class [System.Core]System.Runtime.CompilerServices.CallSite, object, int32>> ConsoleApplication1.Worker/<DoSomething>o__SiteContainer0::<>p__Site1
    L_0006: brtrue L_0058
    L_000b: ldc.i4 256
    L_0010: ldstr "Handle"
    L_0015: ldnull 
    L_0016: ldtoken ConsoleApplication1.Worker
    L_001b: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
    L_0020: ldc.i4.2 
    L_0021: newarr [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo
    L_0026: stloc.0 
    L_0027: ldloc.0 
    L_0028: ldc.i4.0 
    L_0029: ldc.i4.0 
    L_002a: ldnull 
    L_002b: call class [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo::Create(valuetype [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfoFlags, string)
    L_0030: stelem.any [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo
    L_0035: ldloc.0 
    L_0036: ldc.i4.1 
    L_0037: ldc.i4.3 
    L_0038: ldnull 
    L_0039: call class [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo::Create(valuetype [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfoFlags, string)
    L_003e: stelem.any [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo
    L_0043: ldloc.0 
    L_0044: call class [System.Core]System.Runtime.CompilerServices.CallSiteBinder [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.Binder::InvokeMember(valuetype [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpBinderFlags, string, class [mscorlib]System.Collections.Generic.IEnumerable`1<class [mscorlib]System.Type>, class [mscorlib]System.Type, class [mscorlib]System.Collections.Generic.IEnumerable`1<class [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo>)
    L_0049: call class [System.Core]System.Runtime.CompilerServices.CallSite`1<!0> [System.Core]System.Runtime.CompilerServices.CallSite`1<class [mscorlib]System.Action`3<class [System.Core]System.Runtime.CompilerServices.CallSite, object, int32>>::Create(class [System.Core]System.Runtime.CompilerServices.CallSiteBinder)
    L_004e: stsfld class [System.Core]System.Runtime.CompilerServices.CallSite`1<class [mscorlib]System.Action`3<class [System.Core]System.Runtime.CompilerServices.CallSite, object, int32>> ConsoleApplication1.Worker/<DoSomething>o__SiteContainer0::<>p__Site1
    L_0053: br L_0058
    L_0058: ldsfld class [System.Core]System.Runtime.CompilerServices.CallSite`1<class [mscorlib]System.Action`3<class [System.Core]System.Runtime.CompilerServices.CallSite, object, int32>> ConsoleApplication1.Worker/<DoSomething>o__SiteContainer0::<>p__Site1
    L_005d: ldfld !0 [System.Core]System.Runtime.CompilerServices.CallSite`1<class [mscorlib]System.Action`3<class [System.Core]System.Runtime.CompilerServices.CallSite, object, int32>>::Target
    L_0062: ldsfld class [System.Core]System.Runtime.CompilerServices.CallSite`1<class [mscorlib]System.Action`3<class [System.Core]System.Runtime.CompilerServices.CallSite, object, int32>> ConsoleApplication1.Worker/<DoSomething>o__SiteContainer0::<>p__Site1
    L_0067: ldarg.1 
    L_0068: ldc.i4.s 12
    L_006a: callvirt instance void [mscorlib]System.Action`3<class [System.Core]System.Runtime.CompilerServices.CallSite, object, int32>::Invoke(!0, !1, !2)
    L_006f: nop 
    L_0070: ret 
}

Upvotes: 3

Lasse V. Karlsen
Lasse V. Karlsen

Reputation: 391336

No this is not possible.

When the compiler compiles the type implementing the interface, it will create an interface map detailing which methods of the type that is linked to each method of the interface. This cannot be changed at runtime at will.

This means that whenever you call your Handle method through that interface, it will always go to the same method on the underlying type, regardless of any other methods you feel should be more appropriate.

If you want the underlying type to call specific methods internally, depending on the specific type of the generic parameter, you will have to implement that yourself, either using dynamic dispatch, or by using if-statements or similar to detect which type of T you have and call the appropriate method.

The answer here that says you can cast the type you're calling the method on to dynamic means you're using reflection to bypass the interface alltogether. The interface might as well not have any methods at all for this particular scenario, the cast to dynamic will still "work".

I don't recommend this approach. You're effectively writing code that assumes it has carte blanche access to all methods of the underlying type, even though it specifically says "I only need this interface".

Additionally, if the only goal was to avoid runtime errors, consider what will happen if you implement the method explicitly in the class:

void Main()
{
    Worker foo = new Worker();
    ICallBack boo = new MessageHandler();

    foo.DoSomething(boo);
}

public interface ICallBack
{
    void Handle<T>(T arg);
}

public class MessageHandler : ICallBack
{
    void ICallBack.Handle<T>(T arg)
    {
        string name = typeof(T).Name;
        Console.WriteLine(name);
    }
}

public class Worker
{
    public void DoSomething(ICallBack cb)
    {
        ((dynamic)cb).Handle(55);
    }
}

This will crash at runtime with:

RuntimeBinderException:
'UserQuery.MessageHandler' does not contain a definition for 'Handle'

You can test the above code in LINQPad.

Upvotes: 5

Related Questions