简体   繁体   中英

c# generic method overload not consistent with abstract Visitor pattern

experimenting with Visitor pattern and generic method I found a kind of discrepancy in C#.NET. AFAIK C# compiler prefers an explicit overload to a generic method, therefore the following code:

public abstract class A
{
    public abstract void Accept(Visitor v);
}

public class B : A
{
    public override void Accept(Visitor v)
    { v.Visit(this); }
}

public class C : A
{
    public override void Accept(Visitor v)
    { v.Visit(this); }
}

public class D : A
{
    public override void Accept(Visitor v)
    { v.Visit(this); }
}

public class Visitor
{
    public void Visit(B b)
    { Console.WriteLine("visiting B"); }

    public void Visit(C c)
    { Console.WriteLine("visiting C"); }

    public void Visit<T>(T t)
    { Console.WriteLine("visiting generic type: " + typeof(T).Name); }
}

class Program
{

    static void Main()
    {
        A b = new B();
        A c = new C();
        A d = new D();

        Visitor v = new Visitor();

        b.Accept(v);
        c.Accept(v);
        d.Accept(v);
    }
}

The output produced is (as expected):

visiting B
visiting C
visiting generic type: D

However this Visitor pattern implementation does not allow to exchange the Visitor class. Introducing an abstract class VisitorBase and forwarding the call to the overloads produces smth. unexpected for me....

public abstract class A
{
    public abstract void Accept(VisitorBase v);
}

public class B : A
{
    public override void Accept(VisitorBase v)
    { v.Visit(this); }
}

public class C : A
{
    public override void Accept(VisitorBase v)
    { v.Visit(this); }
}

public class D : A
{
    public override void Accept(VisitorBase v)
    { v.Visit(this); }
}

public abstract class VisitorBase
{
    public abstract void Visit<T>(T t);
}

public class Visitor : VisitorBase
{
    protected void VisitImpl(B b)
    { Console.WriteLine("visiting B"); }

    protected void VisitImpl(C c)
    { Console.WriteLine("visiting C"); }

    protected void VisitImpl<T>(T t)
    { Console.WriteLine("visiting generic type: " + typeof(T).Name); }

    public override void Visit<T>(T t)
    {
        VisitImpl(t); //forward the call to VisitorImpl<T> or its overloads
    }
}

class Program
{

    static void Main()
    {
        A b = new B();
        A c = new C();
        A d = new D();

        VisitorBase v = new Visitor();

        b.Accept(v);
        c.Accept(v);
        d.Accept(v);
    }
}

Now the output is:

visiting generic type: B
visiting generic type: C
visiting generic type: D

Do generic methods only prefer generic methods? Why are no explicit overloads called?

Overloading is done statically, so when you call VisitImpl(t) , the compiler must pick the single best overloaded method that this call represents (if there is one). Since the type parameter T could be anything, the only method which is compatible is the generic method, and therefore all calls from Visit<T>(T t) call into VisitImpl<T>(T t) .

EDIT

It looks like you may be coming from a C++ background, so perhaps it's worth noting that C++ templates are very different from C# generics; in particular, there's no such thing as specialization in C#, which may be why the behavior you see is unexpected. The C# compiler does not emit different code for the different types at which a generic method may be called (that is, the C# compiler calls the same generic method when you call Visit(1) and Visit("hello") , it does not generate specializations of the method at types int and string ). At runtime, the CLR creates type specific methods, but this happens after compilation and cannot affect overload resolution.

EDIT - even more elaboration

C# does prefer non-generic methods to generic methods when the non-generic method is statically known to be applicable .

The C# compiler will pick a single method to call at any given call-site. Forget about overloading entirely, and give your methods each a different name; which of those renamed methods can be called at the call-site in question? Only the generic one. Therefore, even when the three names collide and overload resolution kicks in, that is the only overload which is applicable at that site, and is the method chosen.

As I understand it, and I could be very wrong, at compile time the generic function visit actually performs a sort of unboxing of the original type. While we can logically see that the types should run through at compile time, the C# compiler can't make it through the Visit function to the VisitImpl function while holding the types, so the original b.visit(v) is considered unboxed at compile. Given this, it must route through the generic for all types that match when the Visit method is called.

EDIT: To clarify what I mean because I just read my own crap:

The compiler holds the link for b.Visit as a generic call. It fits and is labeled generic. The compiler holds separate links for Visit->VisitImpl as typed and/or generic methods as necessary. The compiler can not hold a link from b.Visit (as generic) -> VisitImpl as typed. Since the path from b.Visit() -> VisitImpl must go through a generic, it holds it as a generic type and so the generic VisitImpl is preferred.

It seems you're confusing overloading and overriding.

Overloading is when you provide multiple methods with the same name , that differ in parameter types:

class Foo
   |
   +- void Qux(A arg)
   +- void Qux(B arg)
   +- void Qux(C arg)

Overriding is when you provide multiple implementations of the same (virtual) method :

class Foo                  class Bar : Foo             class Baz : Foo
   |                          |                           |
   +- virtual void Quux()     +- override void Quux()     +- override void Quux()

C# performs single dispatch :

  • The overload of an invoked method is determined at compile-time.

  • The implementation of an overridden method is determined at run-time.

The visitor pattern exploits the latter by dispatching the method call to the right implementation of the Visit method. In languages with multiple dispatch, the visitor pattern is not needed because the right overload is chosen at run-time.

Generics are a compiler feature, so only information available at compile time is used to determine what method should be called. What you are doing would require at runtime to determine what the actual type of the variable is. The compiler only knows that variable b is of type A, c is of type A, and d is of type A. It's picking the best overload, which is the generic one, as there is no method that takes A.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM