Search code examples
c#.netoverloadingdynamictype

Overloading methods based on unordered parameter sets


I have the following Shape hierarchy:

public abstract class Shape
{ ... }

public class Rectangle : Shape
{ ... }

public class Circle : Shape
{ ... }

public class Triangle : Shape
{ ... }

I have implemented the following functionality to determine if two shapes are intersecting. I use the following IsOverlapping extension method, which uses dynamic to call the appropriate overloaded IsOverlappingSpecialisation method at runtime. I believe this is called double dispatching.

static class ShapeActions
{
    public static bool IsOverlapping(this Shape shape1, Shape shape2)
    {
        return IsOverlappingSpecialisation(shape1 as dynamic, shape2 as dynamic);
    }

    private static bool IsOverlappingSpecialisation(Rectangle rect, Circle circle)
    {
        // Do specialised geometry
        return true;
    }

    private static bool IsOverlappingSpecialisation(Rectangle rect, Triangle triangle)
    {
        // Do specialised geometry
        return true;
    }

This means I can do the following:

Shape rect = new Rectangle();
Shape circle = new Circle();

bool isOverlap = rect.IsOverlapping(circle);

The problem I face now, is that I will have to also implement the following in ShapeActions for circle.IsOverlapping(rect) to work:

private static bool IsOverlappingSpecialisation(Circle circle, Rectangle rect)
{
    // The same geometry maths is used here
    return IsOverlappingSpecialisation(rect, circle); 
}

This is redundant (as I will need to do this for every new shape created). Is there a way I could possibly get around this? I thought of passing in a Tuple parameter into IsOverlapping, but I still have problems. Essentially I want overloading to occur based on unique unordered parameter sets (I know this is not possible, so looking for a workaround).


Solution

  • I may be overcomplicating things here, but it works...

    public static class OverlapCalculator
    {
        private static readonly Dictionary<Tuple<Type, Type>, Delegate> Calculations = new Dictionary<Tuple<Type, Type>, Delegate>();
    
        public static bool IsOverlapping<TShape, TOtherShape>(this TShape shape, TOtherShape otherShape)
            where TShape : Shape
            where TOtherShape : Shape
        {
            var calculation = GetCalculationDelegate<TShape, TOtherShape>();
            if (calculation != null)
            {
                return calculation(shape, otherShape);
            }
    
            throw new InvalidOperationException(string.Format("Could not find calculation for {0} and {1}", typeof(TShape).Name, typeof(TOtherShape).Name));
        }
    
        public static void AddCalculation<TShape, TOtherShape>(Func<TShape, TOtherShape, bool> calculation)
            where TShape : Shape
            where TOtherShape : Shape
        {
            var key = new Tuple<Type, Type>(typeof(TShape), typeof(TOtherShape));
            Calculations[key] = calculation;
    
            var reverseKey = new Tuple<Type, Type>(typeof(TOtherShape), typeof(TShape));
            var reverseCalculation = new Func<TOtherShape, TShape, bool>((otherShape, shape) => calculation(shape, otherShape));
            Calculations[reverseKey] = reverseCalculation;
        }
    
        private static Func<TShape, TOtherShape, bool> GetCalculationDelegate<TShape, TOtherShape>()
        {
            var key = new Tuple<Type, Type>(typeof(TShape), typeof(TOtherShape));
    
            Delegate calculationDelegate;
            if (Calculations.TryGetValue(key, out calculationDelegate))
            {
                return (Func<TShape, TOtherShape, bool>) calculationDelegate;
            }
    
            return null;
        }
    }
    

    This just stores delegates in a Dictionary and tries to get a matching one when you call IsOverlapping on a Shape.

    You use it like this:

    public class Program
    {
        public static void Main()
        {
            // Add the calculation algorithm defined below.
            OverlapCalculator.AddCalculation<Rectangle, Triangle>(IsOverlapping);
    
            var rect = new Rectangle();
            var triangle = new Triangle();
            var circle = new Circle();
    
            // These will work since we have a two way calculation for Rectangle and Triangle
            rect.IsOverlapping(triangle);
            triangle.IsOverlapping(rect);
    
            // This will throw since we have no calculation between Circle and Triangle.
            circle.IsOverlapping(triangle);
        }
    
        private static bool IsOverlapping(Rectangle rectangle, Triangle triangle)
        {
            // Do specialised geometry
            return true;
        }
    }
    

    This should be a neat and fast (no reflection) solution to your problem.

    One drawback with this solution is that you have to "declare" the calculation methods using the AddCalculation method.