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

时间:2010-01-29 17:48:45

标签: c# generics methods overloading operator-precedence

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

但是,此访问者模式实现不允许交换Visitor类。引入抽象类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

泛型方法只是偏好通用方法吗?为什么没有调用显式重载?

非常感谢,
Ovanes

4 个答案:

答案 0 :(得分:5)

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

修改

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

编辑 - 更详细说明

当静态知道非泛型方法适用时,C#确实更喜欢非泛型方法和泛型方法

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

答案 1 :(得分:1)

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

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

编译器将b.Visit的链接保存为通用调用。它适合并标记为通用。 编译器为Visit-&gt; VisitImpl保存单独的链接,作为必要的类型和/或通用方法。 编译器无法保存来自b.Visit的链接(作为通用) - &gt; VisitImpl是打字的。由于路径来自b.Visit() - &gt; VisitImpl必须通过泛型,它将其作为泛型类型保留,因此通用的VisitImpl是首选。

答案 2 :(得分:1)

看起来你的重载和覆盖太混乱了。

重载是指多个方法 同名,但参数类型不同:

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#执行单一调度

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

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

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

答案 3 :(得分:0)

泛型是一种编译器功能,因此只有编译时可用的信息才能用于确定应该调用哪种方法。您正在执行的操作需要在运行时确定变量的实际类型。编译器只知道变量b的类型为A,c的类型为A,而d的类型为A.它选择了最佳的重载,这是一般的重载,因为没有方法需要A。