【问题标题】:c# generic method overload not consistent with abstract Visitor patternc#泛型方法重载与抽象访问者模式不一致
【发布时间】:2010-01-29 17:48:45
【问题描述】:

在使用访问者模式和泛型方法进行实验时,我发现了 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

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

【问题讨论】:

    标签: c# generics methods overloading operator-precedence


    【解决方案1】:

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

    编辑

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

    编辑 - 更多细节

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

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

    【讨论】:

    • @kvb:不,他看到的行为是出乎意料的。在编译时,非泛型方法应该优先于泛型。 connect.microsoft.com/VisualStudio/feedback/details/522202/…
    • 是的,我知道 C#.NET 不允许模板特化,但是这里关于 .NET 更喜欢显式重载而不是泛​​型方法的说法是不正确的。您能否指出 CLI 标准或 C# 标准的部分,这将解释该行为?否则我认为这是一个错误。
    • @Joel - 阅读关于连接问题的评论 - 这是基于对规则的误解,不是错误。
    • 这不是错误。当Visitor.Visit&lt;T&gt; 被编译时,编译器没有任何关于T 的信息,甚至不知道它是引用类型还是值类型。所以编译器别无选择,只能调用VisitImpl&lt;T&gt;。请记住,编译器不仅不会为泛型方法的实例化生成不同的代码,而且实际上在编译时也没有实例化;只有一个方法Visit&lt;T&gt;
    • 有一种乱七八糟的方法可以让重载动态发生。您将访问者和可访问对象都转换为动态。这就像在编译器面前扔烟,因此它无法在编译时解析类型,迫使运行时引擎解析它们......并为您提供所需的紧密类型。杂乱。不是很好。但这是我发现唯一可行的 DRY 解决方案。我这里有更多解释:stackoverflow.com/questions/58309692/…
    【解决方案2】:

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

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

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

    【讨论】:

    • 感谢您的解释,以及您在 kvb 帖子中提供的链接,我了解它是如何处理的。但这真的不好看!!!
    • 请注意,这里编译器的行为与装箱/拆箱无关。装箱将值类型存储在对象中,但在这种情况下,涉及的所有类型都是引用类型。
    • @kvb:我使用这个词很宽松,因为它是描述它的最佳方式。它在语义上不正确,但链接背后的想法是重要的部分。编译器无法保持超出原始通用约束的链接。因此,为什么我将编辑放在那里以希望澄清我已经知道的描述是混乱的。但感谢您指出显而易见的事情。
    【解决方案3】:

    您似乎混淆了重载和覆盖。

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

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

    覆盖是指您提供多个实现相同(虚拟)方法

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

    C# 执行单次调度

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

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

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

    【讨论】:

      【解决方案4】:

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

      【讨论】:

      • 但是你的论点不适合这里。当调用 VisitorBase.Visit(...) 我假设编译器知道 T 是什么。还是会进行类型擦除? IMO 调用将调用转发到泛型方法的泛型方法应保留所有类型信息。正如我在示例中看到的那样,它会丢失。为什么?例如,在 C++ 中,不允许使用虚函数模板。为什么这些在 C# 中被允许并且处理错误?
      • 在 C# 中,要对泛型类型参数做出任何假设(并且您需要在选择重载时做出假设),泛型参数必须具有泛型类型约束。代码中的 Visit 没有任何类型约束,因此编译器可以但不允许解决重载问题。
      • @ovanes:您将变量静态输入到 A 中。没有信息丢失,它仍然是同一个对象,但是您假设您正在处理类型 A,而不是类型 B。如果您希望编译器正确解析,那么在声明变量时,请使用它想要的类型。编译器将尝试选择最具体的签名,然后退回到下一个最不具体的签名,直到找到方法。
      猜你喜欢
      • 2019-10-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-08-06
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多