Scrumbling my head around basics.
I have a couple of (for the example let's say) message classes sharing a common base class. And I have an interface that accepts the base class as argument. So far I thought it would easily be possible to use method overloading to handle specific types of messages in seperated methods.
How could you get this following sample running:
using System;
namespace MethodOverloading
{
// for the example: we are sending messages
// which have a common base class
public class MessageBase
{
public readonly string Value;
public MessageBase() { Value = GetType().Name; }
}
// and there are a couple of concrete instances
public class Message1000 : MessageBase { }
public class Message2000 : MessageBase { }
public class Message3000 : MessageBase { }
public class Message4000 : MessageBase { }
public class Message5000 : MessageBase { }
// and of cource we have an interface receiving all messages but only with one method for the base defined
public interface IHandler
{
void ReceiveMessage(MessageBase msg);
}
// the handlers should do some method overloading so a overloaded method can be implemented for each supported message
// and the base message catches all unsupported messages (e.g. log: ey, missed an overload for this type)
// Handler 1 tries to overload the interface method
public class Handler1 : IHandler
{
public void ReceiveMessage(MessageBase msg) { Console.WriteLine($"Handler1.ReceiveMessage(MessageBase:{msg.Value})"); }
public void ReceiveMessage(Message1000 msg) { Console.WriteLine($"Handler1.ReceiveMessage(Message1000:{msg.Value})"); }
public void ReceiveMessage(Message2000 msg) { Console.WriteLine($"Handler1.ReceiveMessage(Message2000:{msg.Value})"); }
public void ReceiveMessage(Message3000 msg) { Console.WriteLine($"Handler1.ReceiveMessage(Message3000:{msg.Value})"); }
public void ReceiveMessage(Message4000 msg) { Console.WriteLine($"Handler1.ReceiveMessage(Message4000:{msg.Value})"); }
// intentionally no overload for Message5000
}
// Handler 2 provides one interface method and has protected overloads...
public class Handler2 : IHandler
{
public void ReceiveMessage(MessageBase msg)
{
Console.Write($"Handler2.ReceiveMessage(MessageBase:{msg.Value}) > ");
HandleMessage(msg);
}
protected void HandleMessage(MessageBase msg) { Console.WriteLine($"Handler2.HandleMessage(MessageBase:{msg.Value})"); }
protected void HandleMessage(Message1000 msg) { Console.WriteLine($"Handler2.HandleMessage(Message1000:{msg.Value})"); }
protected void HandleMessage(Message2000 msg) { Console.WriteLine($"Handler2.HandleMessage(Message2000:{msg.Value})"); }
protected void HandleMessage(Message3000 msg) { Console.WriteLine($"Handler2.HandleMessage(Message3000:{msg.Value})"); }
protected void HandleMessage(Message4000 msg) { Console.WriteLine($"Handler2.HandleMessage(Message4000:{msg.Value})"); }
// intentionally no overload for Message5000
}
class Program
{
static void Main(string[] args)
{
// so lets give it a try ....
Console.WriteLine("Testing method overloads");
MessageBase msgBase = new MessageBase();
Message1000 msg1000 = new Message1000();
Message2000 msg2000 = new Message2000();
Message3000 msg3000 = new Message3000();
Message4000 msg4000 = new Message4000();
Message5000 msg5000 = new Message5000();
Console.WriteLine("Handler1:");
Handler1 handler1 = new Handler1();
handler1.ReceiveMessage(msgBase);
handler1.ReceiveMessage(msg1000);
handler1.ReceiveMessage(msg2000);
handler1.ReceiveMessage(msg3000);
handler1.ReceiveMessage(msg4000);
handler1.ReceiveMessage(msg5000);
Console.WriteLine("iHandler1:");
IHandler ihandler1 = new Handler1();
ihandler1.ReceiveMessage(msgBase);
ihandler1.ReceiveMessage(msg1000);
ihandler1.ReceiveMessage(msg2000);
ihandler1.ReceiveMessage(msg3000);
ihandler1.ReceiveMessage(msg4000);
ihandler1.ReceiveMessage(msg5000);
Console.WriteLine("Handler2:");
Handler2 handler2 = new Handler2();
handler2.ReceiveMessage(msgBase);
handler2.ReceiveMessage(msg1000);
handler2.ReceiveMessage(msg2000);
handler2.ReceiveMessage(msg3000);
handler2.ReceiveMessage(msg4000);
handler2.ReceiveMessage(msg5000);
Console.WriteLine("iHandler2:");
IHandler ihandler2 = new Handler2();
ihandler2.ReceiveMessage(msgBase);
ihandler2.ReceiveMessage(msg1000);
ihandler2.ReceiveMessage(msg2000);
ihandler2.ReceiveMessage(msg3000);
ihandler2.ReceiveMessage(msg4000);
ihandler2.ReceiveMessage(msg5000);
Console.WriteLine("press any key to exit");
Console.ReadLine();
}
}
}
The output actually is:
Testing method overloads
Handler1:
Handler1.ReceiveMessage(MessageBase:MessageBase)
Handler1.ReceiveMessage(Message1000:Message1000)
Handler1.ReceiveMessage(Message2000:Message2000)
Handler1.ReceiveMessage(Message3000:Message3000)
Handler1.ReceiveMessage(Message4000:Message4000)
Handler1.ReceiveMessage(MessageBase:Message5000)
iHandler1:
Handler1.ReceiveMessage(MessageBase:MessageBase)
Handler1.ReceiveMessage(MessageBase:Message1000)
Handler1.ReceiveMessage(MessageBase:Message2000)
Handler1.ReceiveMessage(MessageBase:Message3000)
Handler1.ReceiveMessage(MessageBase:Message4000)
Handler1.ReceiveMessage(MessageBase:Message5000)
Handler2:
Handler2.ReceiveMessage(MessageBase:MessageBase) > Handler2.HandleMessage(MessageBase:MessageBase)
Handler2.ReceiveMessage(MessageBase:Message1000) > Handler2.HandleMessage(MessageBase:Message1000)
Handler2.ReceiveMessage(MessageBase:Message2000) > Handler2.HandleMessage(MessageBase:Message2000)
Handler2.ReceiveMessage(MessageBase:Message3000) > Handler2.HandleMessage(MessageBase:Message3000)
Handler2.ReceiveMessage(MessageBase:Message4000) > Handler2.HandleMessage(MessageBase:Message4000)
Handler2.ReceiveMessage(MessageBase:Message5000) > Handler2.HandleMessage(MessageBase:Message5000)
iHandler2:
Handler2.ReceiveMessage(MessageBase:MessageBase) > Handler2.HandleMessage(MessageBase:MessageBase)
Handler2.ReceiveMessage(MessageBase:Message1000) > Handler2.HandleMessage(MessageBase:Message1000)
Handler2.ReceiveMessage(MessageBase:Message2000) > Handler2.HandleMessage(MessageBase:Message2000)
Handler2.ReceiveMessage(MessageBase:Message3000) > Handler2.HandleMessage(MessageBase:Message3000)
Handler2.ReceiveMessage(MessageBase:Message4000) > Handler2.HandleMessage(MessageBase:Message4000)
Handler2.ReceiveMessage(MessageBase:Message5000) > Handler2.HandleMessage(MessageBase:Message5000)
press any key to exit
Handler 1 actually works when called directly. Unfortunatly not out of the box when called as interface.
Though I thought at least Handler2 with the protected overloads would do the trick....
What I am actually trying to get rid of is the switch statement with castings in Handler3 (because that extra step could easily be missed and it would be great to do that magic in a base class unaccessible for the developers):
public class Handler3 : IHandler
{
public void ReceiveMessage(MessageBase msg)
{
Console.Write($"Handler3.ReceiveMessage(MessageBase:{msg.Value}) > ");
switch (msg)
{
case Message1000 msg1000: HandleMessage(msg1000); break;
case Message2000 msg2000: HandleMessage(msg2000); break;
case Message3000 msg3000: HandleMessage(msg3000); break;
case Message4000 msg4000: HandleMessage(msg4000); break;
default: Console.WriteLine("dropped because not supported: " msg.Value); break; // for the msg5000
}
}
//protected void HandleMessage(MessageBase msg) { Console.WriteLine($"Handler3.HandleMessage(MessageBase:{msg.Value})"); }
protected void HandleMessage(Message1000 msg) { Console.WriteLine($"Handler3.HandleMessage(Message1000:{msg.Value})"); }
protected void HandleMessage(Message2000 msg) { Console.WriteLine($"Handler3.HandleMessage(Message2000:{msg.Value})"); }
protected void HandleMessage(Message3000 msg) { Console.WriteLine($"Handler3.HandleMessage(Message3000:{msg.Value})"); }
protected void HandleMessage(Message4000 msg) { Console.WriteLine($"Handler3.HandleMessage(Message4000:{msg.Value})"); }
// intentionally no overload for Message5000
}
Which would actually works as shown by the output:
Handler3:
Handler3.ReceiveMessage(MessageBase:MessageBase) > dropped because not supported: MessageBase
Handler3.ReceiveMessage(MessageBase:Message1000) > Handler3.HandleMessage(Message1000:Message1000)
Handler3.ReceiveMessage(MessageBase:Message2000) > Handler3.HandleMessage(Message2000:Message2000)
Handler3.ReceiveMessage(MessageBase:Message3000) > Handler3.HandleMessage(Message3000:Message3000)
Handler3.ReceiveMessage(MessageBase:Message4000) > Handler3.HandleMessage(Message4000:Message4000)
Handler3.ReceiveMessage(MessageBase:Message5000) > dropped because not supported: Message5000
But it is a benefit that the compiler complains and prevents builds if you miss an method overload using that variant.
CodePudding user response:
Well, yes. Remember that overload resolution is done at compile-time using the compile-time types of your variables, and not at runtime. The runtime types are irrelevant.
IHandler
only has the overload void ReceiveMessage(MessageBase msg)
. So when you call IHandler.ReceiveMessage(msg)
, whatever subclass msg
happens to be, it has to call IHandler.ReceiveMessage(MessageBase msg)
, because that's the only method which IHandler
defines.
It doesn't matter that Handler1
defines other other methods which aren't in IHandler
: your Main
method is working with an instance of IHandler
, and so void ReceiveMessage(MessageBase msg)
is the only overload it can see.
In Handler2.ReceiveMessage(MessageBase msg)
, msg
has the compile-time type MessageBase
. You can see it in the method signature. So when you call HandleMessage(msg)
, msg
is a MessageBase
, so the compiler has to pick the HandleMessage(MessageBase msg)
overload.
One possible way to achieve what you're after is to use the visitor pattern. This lets you take a variable with a compile-time type of MessageBase
, and find out what its run-time type is by asking it to call a specific method on you. Something like:
public interface IMessageVisitor
{
void Accept(Message1000 msg);
void Accept(Message2000 msg);
}
// for the example: we are sending messages
// which have a common base class
public abstract class MessageBase
{
public readonly string Value;
public MessageBase() { Value = GetType().Name; }
public abstract void Visit(IMessageVisitor visitor);
}
// and there are a couple of concrete instances
public class Message1000 : MessageBase
{
public override void Visit(IMessageVisitor visitor) => visitor.Accept(this);
}
public class Message2000 : MessageBase
{
public override void Visit(IMessageVisitor visitor) => visitor.Accept(this);
}
public interface IHandler
{
void ReceiveMessage(MessageBase msg);
}
public class Handler1 : IHandler, IMessageVisitor
{
public void ReceiveMessage(MessageBase msg) => msg.Visit(this);
public void Accept(Message1000 msg) => Console.WriteLine("Message1000");
public void Accept(Message2000 msg) => Console.WriteLine("Message2000");
}
See it on dotnetfiddle.net.
CodePudding user response:
Methods are resolved based on type of the object, not the type of the parameter. I.e. you will call Handler1.ReceiveMessage
or Handler2.ReceiveMessage
based on the type of the handler object, not the message object. This is technically known as "single dispatch"
What you want is "multiple dispatch", i.e. you want the method to be resolved based on two different objects.
One way to do this would be to change your interface to an abstract base class and use pattern matching to map the type to the correct method
public abstract class HandlerBase
{
protected abstract void ReceiveMessage(Message1000 msg);
protected abstract void ReceiveMessage(Message2000 msg);
// etc
void ReceiveMessage(MessageBase msg){
switch(msg){
case Message1000 msg1000:
ReceiveMessage(msg1000);
break;
case Message2000 msg2000:
ReceiveMessage(msg2000);
break;
// etc
}
}
}
Another alternative is the visitor pattern:
public abstract class MessageBase
{
public readonly string Value;
public MessageBase() { Value = GetType().Name; }
public abstract void Visit(IVisitor visitor);
}
public class Message1000
{
public override void Visit(IVisitor visitor) => visitor.ReceiveMessage(this);
}
public interface IVisitor{
void ReceiveMessage(Message1000 msg);
void ReceiveMessage(Message2000 msg);
// etc...
}
The visitor pattern will force you to implement all the needed methods, a new message type must have an Accept method, and to implement this you need a new ReceiveMessage overload, and that has to be implemented by all the visitors/handlers. If this is a benefit or not is up to you.
A third alternative is to use "dynamic", but I would not recommend it since it will disable all type checking.