Перегрузка универсального метода c # не соответствует абстрактному шаблону Visitor - PullRequest
6 голосов
/ 29 января 2010

экспериментируя с шаблоном Visitor и универсальным методом, я обнаружил некое расхождение в 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 и переадресация вызова на перегрузки приводит к чему-то. неожиданно для меня ....

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 ]

5 голосов
/ 29 января 2010

Перегрузка выполняется статически, поэтому, когда вы вызываете VisitImpl(t), компилятор должен выбрать единственный перегруженный метод, который представляет этот вызов (если он есть). Поскольку параметр типа T может быть любым, единственный совместимый метод - это универсальный метод, и поэтому все вызовы из Visit<T>(T t) вызывают в VisitImpl<T>(T t).

EDIT

Похоже, вы пришли из C ++, поэтому, возможно, стоит отметить, что шаблоны C ++ сильно отличаются от шаблонов C #; в частности, в C # нет такой вещи, как специализация, из-за которой поведение, которое вы видите, является неожиданным. Компилятор C # не генерирует разный код для разных типов, при которых может быть вызван универсальный метод (то есть компилятор C # вызывает тот же универсальный метод при вызове Visit(1) и Visit("hello"), это не генерирует специализации метода для типов int и string). Во время выполнения CLR создает специфичные для типа методы, но это происходит после компиляции и не может повлиять на разрешение перегрузки.

РЕДАКТИРОВАТЬ - еще более сложный

C # предпочитает неуниверсальные методы универсальным методам , когда статически известно, что неуниверсальный метод применим .

Компилятор C # выберет один метод для вызова на любом данном сайте вызова. Забудьте о полной перегрузке и присвойте своим методам разные имена; Какой из этих переименованных методов может быть вызван на данном сайте? Только общий. Поэтому, даже когда три имени сталкиваются и разрешается перегрузка, это единственная перегрузка, которая применима на этом сайте и является выбранным методом.

1 голос
/ 29 января 2010

Кажется, вы путаете перегрузку и переопределение.

Перегрузка - это когда вы предоставляете несколько методов с тем же именем , которые отличаются типами параметров:

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. В языках с множественной диспетчеризацией шаблон посетителя не требуется, поскольку во время выполнения выбрана правильная перегрузка.

1 голос
/ 29 января 2010

Насколько я понимаю, и я могу ошибаться, во время компиляции универсальная функция посещение фактически выполняет своего рода распаковку исходного типа. Хотя мы можем логически видеть, что типы должны проходить во время компиляции, компилятор C # не может сделать это с помощью функции Visit для функции VisitImpl, удерживая типы, поэтому оригинальный b.visit (v) считается не упакованным при компиляции , Учитывая это, он должен маршрутизировать через универсальный для всех типов, которые совпадают, когда вызывается метод Visit.

РЕДАКТИРОВАТЬ: Чтобы уточнить, что я имею в виду, потому что я только что прочитал мою собственную чушь:

Компилятор содержит ссылку на b.Visit как общий вызов. Он подходит и обозначен как универсальный. Компилятор содержит отдельные ссылки для Visit-> VisitImpl как типизированные и / или универсальные методы по мере необходимости. Компилятор не может содержать ссылку из b.Visit (как универсальную) -> VisitImpl в типизированном виде. Поскольку путь из b.Visit () -> VisitImpl должен проходить через универсальный тип, он содержит его как универсальный тип и поэтому предпочтительным является универсальный тип VisitImpl.

0 голосов
/ 29 января 2010

Обобщение - это функция компилятора, поэтому только информация, доступная во время компиляции, используется для определения того, какой метод должен быть вызван. То, что вы делаете, потребует во время выполнения, чтобы определить, каков реальный тип переменной. Компилятор знает только, что переменная b имеет тип A, c имеет тип A, а d имеет тип A. Он выбирает наилучшую перегрузку, которая является общей, поскольку нет метода, который принимает A.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...