-1

I am exploring the Visitor pattern and below is the code. Please note that i already know how to fix this - using Virtual methods and expanding the Visitor interface with various overloads. I am trying to better understand type and overload resolution in c#.

In the test method, when Accept method is called, it always calls Visit method with parameter type "Element" instead of BinaryOperator. As per answers to other similar questions on SO, this happens because the overloading method & object type are resolved at compile time.

Links:

why-isnt-the-overloaded

runtime-type-vs-compile-time

non-virtual-method-resolution

When i check the generated IL, for Accept method, the instruction at the call for Visitor.Visit() is "callvirt" instead of "call". Is there something else i should be looking at in IL that indicates that the overload is set at compile time?

Also if i use reflection to inspect the object type in the Accept method, it prints MathVisitor and BinaryOperator. So the run time knows the correct types. Then why is the correct overload of Visit method not called?

Abstractions:

public enum MathOp
{
    ADD,
    SUBSTRACT,
    MULTIPLY
}

public interface IElementVisitor
{
    void Visit(Element e);
}

public abstract class Element
{
    public string ElementValue { get; set; }
    public  void Accept(IElementVisitor ev) {

        //Console.WriteLine("Type for Paramter is {0}",ev.GetType().Name);
        //Console.WriteLine("Type for 'this' is {0}", this.GetType().Name);

        ev.Visit(this);

        //(ev as dynamic).Visit(this as dynamic);
    }
    public int ToNumber
    {
        get { return int.Parse(ElementValue); }
    }
}

Concrete:

class NumberLiteral:Element
{
    public NumberLiteral(int number)
    {
        ElementValue = number.ToString();
    }
}

class BinaryOperator:Element
{
    public NumberLiteral Left { get; set; }
    public NumberLiteral Right { get; set; }

    public MathOp MathOpType { get; set; }

    public BinaryOperator(MathOp optype)
    {
        MathOpType = optype;
    }
}

class MathVisitor : IElementVisitor
{
    public int Result { get; private set; }
    public void Visit(Element e)
    {
        Console.WriteLine("---Not Implemented--for--Element");
    }

    public void Visit(NumberLiteral e)
    {
        Console.WriteLine("Num Lit - do nothing");
    }

    public void Visit(BinaryOperator b)
    {
        if (b.MathOpType.Equals(MathOp.ADD))
        {
            int v1 = b.Left.ToNumber;
            int v2 = b.Right.ToNumber;
            Result = v1 + v2;
        }
    }
}

Test:

public class TestVisitorPattern
{
    public void TestMethod()
    {
        NumberLiteral e1 = new NumberLiteral(1);
        NumberLiteral e2 = new NumberLiteral(2);

        BinaryOperator b1 = new BinaryOperator(MathOp.ADD);

        b1.Left = e1;
        b1.Right = e2;


        MathVisitor mv = new MathVisitor();
        Console.WriteLine("------------direct call---------------");
        mv.Visit(b1);
        Console.WriteLine(mv.Result);


        mv = new MathVisitor();
        Console.WriteLine("------------call through accept---------------");

        b1.Accept(mv);
        Console.WriteLine(mv.Result);

    }
}
naren
  • 1
  • 2
  • As covered in the marked duplicate, when calling a method through a variable typed as an interface, the compiler doesn't have enough context to choose any method overload except the one the object implemented for that interface. That's the only method overload visible to the compiler at that point in time. See also https://stackoverflow.com/questions/2038333/overloads-done-at-compile-time and https://stackoverflow.com/questions/5572880/c-sharp-method-overload-problem-with-class-derived-from-generic-abstract-class. – Peter Duniho Jun 11 '17 at 23:48
  • Note that another work-around, instead of extending the interface, would be for the implementer of the interface to do some type-checking to pick the correct overload. Either explicitlly with the `as` operator, or implicitly via `dynamic`. IMHO, this would be preferable to cluttering up your interface declaration with a bunch of types that implementers may never need to even know about, never mind use (something that can be important when exposing the interface as a public type, while trying to keep implementation-specific types internal). – Peter Duniho Jun 11 '17 at 23:50

1 Answers1

0

When you call Accept, it goes into this method:

    public void Accept(IElementVisitor ev)
    {

        //Console.WriteLine("Type for Paramter is {0}",ev.GetType().Name);
        //Console.WriteLine("Type for 'this' is {0}", this.GetType().Name);

        ev.Visit(this);

        //(ev as dynamic).Visit(this as dynamic);
    }

that in turn calls into:

    public void Visit(Element e)
    {
        Console.WriteLine("---Not Implemented--for--Element");
    }

That is because that is the only method that implements the IElementVisitor interface:

public interface IElementVisitor
{
    void Visit(Element e);
}

So even though the implementator has other 'better' methods available, since ev is of type IElementVisitor the only methods available to call on it are Visit(Element e) - and so that is the one it invokes.

mjwills
  • 23,389
  • 6
  • 40
  • 63