7

尝试访问者模式和通用方法,我在 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

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

4

4 回答 4

6

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

编辑

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

编辑 - 更详细

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

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

于 2010-01-29T17:56:43.937 回答
1

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

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

编译器将 b.Visit 的链接保存为通用调用。它适合并被标记为通用。编译器将 Visit->VisitImpl 的单独链接保存为必要的类型化和/或泛型方法。编译器不能保存来自 b.Visit (as generic) -> VisitImpl as typed 的链接。由于从 b.Visit() -> VisitImpl 的路径必须经过泛型,因此它将其作为泛型类型,因此首选泛型 VisitImpl。

于 2010-01-29T18:06:04.680 回答
1

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

重载是当您提供多个具有相同名称的方法时,它们的参数类型不同:

Foo 类
   |
   +- void Qux(A arg)
   +- 无效 Qux(B arg)
   +- 无效 Qux(C arg)

覆盖是当您提供相同(虚拟)方法的多个实现时:

类 Foo 类 Bar : Foo 类 Baz : Foo
   | | |
   +- 虚拟 void Quux() +- 覆盖 void Quux() +- 覆盖 void Quux()

C# 执行单一调度

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

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

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

于 2010-01-29T18:13:15.053 回答
0

泛型是一个编译器特性,因此只有在编译时可用的信息用于确定应该调用什么方法。您正在做的事情需要在运行时确定变量的实际类型是什么。编译器只知道变量 b 属于 A 类型,c 属于 A 类型,而 d 属于 A 类型。它会选择最好的重载,即泛型重载,因为没有采用 A 的方法。

于 2010-01-29T17:55:31.900 回答