繁体   English   中英

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

[英]c# generic method overload not consistent with abstract Visitor pattern

在尝试访问者模式和通用方法时,我发现了 C#.NET 中的一种差异。 AFAIK 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 并将调用转发到重载会产生 smth。 出乎我的意料......

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

泛型方法只喜欢泛型方法吗? 为什么不调用显式重载?

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

编辑

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

编辑 - 更详细

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

C# 编译器将选择一个方法在任何给定的调用站点进行调用。 完全忘记重载,给你的方法一个不同的名字; 哪些重命名的方法可以在有问题的调用站点上调用? 只有通用的。 因此,即使这三个名称发生冲突并且重载解析开始,这也是该站点唯一适用的重载,并且是所选择的方法。

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

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

编译器将 b.Visit 的链接保存为通用调用。 它适合并被标记为通用。 编译器根据需要将 Visit->VisitImpl 的单独链接作为类型化和/或泛型方法保存。 编译器不能保存从 b.Visit(作为通用)-> VisitImpl 作为类型的链接。 由于从 b.Visit() -> VisitImpl 的路径必须经过一个泛型,它把它作为一个泛型类型,所以泛型 VisitImpl 是首选。

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

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

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# 执行单分派

  • 被调用方法的重载是在编译时确定的。

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

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

泛型是编译器的一个特性,所以只有在编译时可用的信息才能确定应该调用什么方法。 您正在做的事情需要在运行时确定变量的实际类型是什么。 编译器只知道变量 b 是 A 类型,c 是 A 类型,d 是 A 类型。它选择了最好的重载,也就是泛型重载,因为没有方法接受 A。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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