Search code examples
c#inheritancedynamicdispatchvisitor-pattern

Double dispatch inside a single inheritance tree


I have an inheritance tree of different Line-classes, starting with the abstract Line-class. I want to be able to intersect each line with each other line, and sometimes, I do not know neither of the runtime types, e.g. I'm calling Line.Intersect(Line) (so I need double dispatch). This will always call the most abstract overload of the overriden Intersect-methods, e.g. Circle.Intersect(Line) instead of Circle.Intersect(actualType). Here's some example code:

class Program
{
  static void Main(string[] args)
  {
    Line straightLine = new StraightLine();
    Line circle = new Circle();

    // Will print: "Circle intersecting a line."
    // But should print: "Circle intersecting a straight line."
    circle.Intersect(straightLine);

    Console.ReadLine();
  }
}


abstract class Line
{
  public abstract void Intersect(Line line);

  public abstract void Intersect(StraightLine straightLine);

  public abstract void Intersect(Circle circle);
}


class StraightLine : Line
{
  public override void Intersect(Line line)
  {
    Console.WriteLine("Straigth line intersecting a line.");
  }

  public override void Intersect(StraightLine straightLine)
  {
    Console.WriteLine("Straight line intersecting a straight line.");
  }

  public override void Intersect(Circle circle)
  {
    Console.WriteLine("Straight line intersecting a circle.");
  }
}


class Circle : Line
{
  public override void Intersect(Line line)
  {
    Console.WriteLine("Circle intersecting a line.");
  }

  public override void Intersect(Circle circle)
  {
    Console.WriteLine("Circle intersecting a circle.");
  }

  public override void Intersect(StraightLine straightLine)
  {
    Console.WriteLine("Circle intersecting a straight line.");
  }
}

One possible workaround is to use dynamic, which I currently do. However, I want to migrate to a .NET Standard-library, where dynamic is not allowed.

Are there other ways to make this work? I'd be willing to switch the abstract class for one or multiple interfaces, if that helps. Maybe the visitor-pattern is applicable, although I've only seen this used for different inheritance trees (and find it quite ugly).


Solution

  • It is possible to emulate double dispatch by using reflection. Targeting .NET Standard 1.1 and using the Nuget-Package System.Reflection, the Intersect(Line line)-method does not need be abstract or virtual, but only has to be implemented once.

    This is the whole example code for the .NET Standard library (I now return a string instead of using Console.WriteLine(), since the latter is not available in .NET Standard):

    using System.Reflection;
    
    namespace IntersectLibrary
    {
      public abstract class Line
      {
        public string Intersect(Line line)
        {
          var method = this.GetType().GetRuntimeMethod(nameof(Intersect), new[] { line.GetType() });
          return (string)method.Invoke(this, new[] { line });
        }
    
        public abstract string Intersect(StraightLine straightLine);
    
        public abstract string Intersect(Circle circle);
      }
    
    
      public class StraightLine : Circle
      {
        public override string Intersect(StraightLine straightLine)
        {
          return "Straight line intersecting a straight line.";
        }
    
        public override string Intersect(Circle circle)
        {
          return "Straight line intersecting a circle.";
        }
      }
    
    
      public class Circle : Line
      {
        public override string Intersect(Circle circle)
        {
          return "Circle intersecting a circle.";
        }
    
        public override string Intersect(StraightLine straightLine)
        {
          return "Circle intersecting a straight line.";
        }
      }
    }
    

    Please note that when targeting .NET Framework, System.Reflection offers different methods, and the code will need to be modified.

    In a console application, the following will happen:

    using System;
    using IntersectLibrary;
    
    namespace ConsoleApplication
    {
      class Program
      {
        static void Main(string[] args)
        {
          Line straightLine = new StraightLine();
          Line circle = new Circle();
          Circle circle2 = new Circle();
    
          // Calls "Line.Intersect(Line)", and correctly
          // prints "Circle intersecting a straight line.".
          Console.WriteLine(circle.Intersect(straightLine));
    
          // Also calls "Line.Intersect(Line)",
          // since the argument's compile-time type is "Line".
          Console.WriteLine(circle2.Intersect(straightLine));
    
          // Calls "Line.Intersect(Circle)",
          // since the argument's compile-time type is "Circle".
          // At runtime, the call will be resolved to
          // "StraightLine.Intersect(Circle)" via single dispatch.
          Console.WriteLine(straightLine.Intersect(circle2));
    
          Console.ReadLine();
        }
      }
    }
    

    If you now have one object of compile-time type Line and one of a concrete type (e.g. Circle), it's better to call Line.Intersect(Circle), since this won't need the (slower) reflection to resolve the method call. However, Circle.Intersect(Line) will also work, and most importantly, calling Line.Intersect(Line) is now always possible.