C#泛型方法重载与抽象访问者模式不一致

发布于 2024-08-19 23:54:08 字数 2399 浏览 3 评论 0原文

在尝试访问者模式和泛型方法时,我发现 C#.NET 中存在某种差异。据我所知,C# 编译器更喜欢显式重载而不是泛​​型方法,因此以下代码:

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);
    }
}

生成的输出是(如预期):

visiting B
visiting C
visiting generic type: D

但是,此访问者模式实现不允许交换访问者类。引入抽象类 VisitorBase 并将调用转发到重载会产生一些效果。对我来说出乎意料......

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);
    }
}

现在的输出是:

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

泛型方法仅更喜欢泛型方法吗?为什么没有调用显式重载?

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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

欲拥i 2024-08-26 23:54:08

重载是静态完成的,因此当您调用 VisitImpl(t) 时,编译器必须选择此调用代表的单个最佳重载方法(如果有)。由于类型参数 T 可以是任何内容,唯一兼容的方法是泛型方法,因此来自 Visit(T t) 的所有调用都会调用 <代码>VisitImpl(T t)。

编辑

看起来您可能有 C++ 背景,因此也许值得注意的是 C++ 模板与 C# 泛型有很大不同;特别是,C# 中不存在专门化这样的东西,这可能就是您看到的行为出乎意料的原因。 C# 编译器不会为调用泛型方法的不同类型发出不同的代码(即,当您调用 Visit(1)< 时,C# 编译器会调用相同的泛型方法/code> 和 Visit("hello"),它不会生成类型为 intstring 的方法的特化。在运行时,CLR 创建类型特定的方法,但这发生在编译之后,并且不会影响重载解析。

编辑 - 更详细

当非泛型方法静态已知适用时,C# 确实更喜欢非泛型方法而不是泛型方法。

C# 编译器将选择一个方法在任何给定的调用站点进行调用。完全忘记重载,并为每个方法指定不同的名称;可以在相关调用站点调用哪些重命名的方法?只有通用的那种。因此,即使三个名称发生冲突并且重载决议启动,这也是该站点适用的唯一重载,并且是所选的方法。

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.

情徒 2024-08-26 23:54:08

据我了解,我可能是非常错误的,在编译时,泛型函数访问实际上执行了一种原始类型的拆箱。虽然我们从逻辑上可以看出类型应该在编译时运行,但 C# 编译器无法在保存类型的同时将 Visit 函数传递到 VisitImpl 函数,因此原始 b.visit(v) 在编译时被视为未装箱。鉴于此,它必须通过泛型来路由调用 Visit 方法时匹配的所有类型。

编辑:为了澄清我的意思,因为我刚刚读了自己的废话:

编译器将 b.Visit 的链接保存为通用调用。它适合并被标记为通用。
编译器根据需要将 Visit->VisitImpl 的单独链接保留为类型化和/或泛型方法。
编译器无法保存来自 b.Visit 的链接(作为通用)->按键入的方式访问Impl。由于来自 b.Visit() -> 的路径VisitImpl 必须通过泛型,它将其作为泛型类型保存,因此泛型 VisitImpl 是首选。

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.

农村范ル 2024-08-26 23:54:08

看来您混淆了重载和覆盖。

重载是指您提供具有相同名称多个方法,但参数类型不同:

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

重写是指您提供同一(虚拟)方法的多个实现

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

C# 执行单次调度

  • 调用方法的重载在编译时确定-time。

  • 重写方法的实现是在运行时确定的。

访问者模式通过将方法调用分派到 Visit 方法的正确实现来利用后者。在具有多重分派的语言中,不需要访问者模式,因为在运行时选择了正确的重载。

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.

旧梦荧光笔 2024-08-26 23:54:08

泛型是编译器功能,因此仅使用编译时可用的信息来确定应调用哪个方法。您所做的需要在运行时确定变量的实际类型是什么。编译器只知道变量 b 是 A 类型,c 是 A 类型,d 是 A 类型。它选择最好的重载,即泛型重载,因为没有方法采用 A。

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文