Различие между базовым и производным классом при передаче в качестве параметров перегруженным методам

В настоящее время я реализую шаблон посетителя на С# для лекции в университете. На данный момент он работает нормально, но у меня есть вопрос по задаче, которую мы должны были выполнить. На данный момент у меня есть что-то вроде этого:

public class TreeStructure<T>
{
  protected TreeStructure<T> _left;
  protected TreeStructure<T> _right;
  public T myValue;

  public TreeStructure(TreeStructure<T> left, T value ,TreeStructure<T> right)
  {
    this._left = left;
    this._right = right; 
    this.myValue = value;
  }
  public virtual void InOrder(MyVisitor<T> visitor)
  {
    if(!IsEmpty(this._left))
    {
      _left.InOrder(visitor);
    }
    visitor.Visit(this);
    if(!IsEmpty(this._right))
    {
      _right.InOrder(visitor);
    }
  }
  protected bool IsEmpty(TreeStructure<T> node)
  {
    return node == null;
  }
}

 public class SpecialTree<T> : TreeStructure<T>
  {
    public SpecialTree(TreeStructure<T> left, T value, TreeStructure<T> right)
    {
      this._left = left;
      this._right = right;
      this.myValue = value;
    }

    public override void InOrder(MyVisitor<T> visitor)
    {
      if(!IsEmpty(this._left))
      {
        _left.InOrder(visitor);
      }
      visitor.Visit(this);
      Console.WriteLine("Hallo");
      if(!IsEmpty(this._right))
      {
        _right.InOrder(visitor);
      }
    }
  }

А теперь нам надо реализовать несколько посетителей. Это не было проблемой, пока мы не должны различать в одном посетителе тип дерева. Итак, теперь у меня есть один посетитель, который выглядит так:

  public interface MyVisitor<T>
  {
    void Visit(TreeStructure<T> tree);
  }

  public class CountVisitor<T> : MyVisitor<T>
  {
    public int count { get; set;}
    public CountVisitor()
    {
      count = 0;
    }

    public void Visit(TreeStructure<T> tree)
    {
      count++;
    }

    public void Visit(SpecialTree<T> specialTree)
    {
      count+=2;
    }
  }

Итак, вопрос или проблема у меня в том, что это, по-видимому, работает на Java, но не на С#. Метод Visit() для SpecialTree никогда не вызывается. Итак, я знаю, что могу проверить конкретно тип объекта, как здесь: производные-классы-базового-класса">Как лучше всего отличить производные классы от базового класса? Но может ли кто-нибудь объяснить мне, почему это не работает с перегруженными методами? Или у меня тут ошибка?

РЕДАКТИРОВАТЬ: Вот одна тестовая инициализация дерева:

TreeStructure<int> tree1 = new TreeStructure<int>(new TreeStructure<int>  (null,3,null),1,new SpecialTree<int>(null,2,null));
      CountVisitor<int> visitor2 = new CountVisitor<int>();
      tree1.InOrder(visitor2);
      Console.WriteLine("Nodecount: " + visitor2.count.ToString());

person Roland M    schedule 31.10.2014    source источник
comment
Что такое MyVisitor<T>?   -  person Dirk    schedule 31.10.2014
comment
Короткий, но полный пример, демонстрирующий проблему (и в идеале ничего больше — вообще не нужно использовать деревья или посетителей), действительно помог бы. Я сильно подозреваю, что эквивалентный код на Java тоже не будет работать, но нам действительно нужно увидеть полный пример, чтобы сказать...   -  person Jon Skeet    schedule 31.10.2014
comment
Ой, извините, я забыл, это просто самодельный интерфейс. Добавлю в стартовый пост.   -  person Roland M    schedule 31.10.2014
comment
@RolandM Примечание: по соглашению в C# интерфейсы должны начинаться с I, так что это будет IMyVistor<T> :)   -  person dav_i    schedule 31.10.2014
comment
Ok. Я не хотел называть его IVisitor из-за возможных конфликтов с уже реализованными интерфейсами, но ладно, запомню на будущее.   -  person Roland M    schedule 31.10.2014


Ответы (4)


Это сильно зависит от реализации MyVisitor<T>. В своем комментарии вы говорите, что это интерфейс. Вы уверены, что это весь пример? Код, который вы показываете в своем вопросе, не компилируется, потому что SpecialTree<T> не вызывает свой родительский конструктор.

Если MyVisitor<T> не объявляет Visit(SpecialTree<T> specialTree), диспетчеризация интерфейса, выполненная InOrder, никогда не достигнет случая SpecialTree.

Обновить

Учитывая ваш обновленный код инициализации и предполагая, что интерфейс объявлен следующим образом:

interface MyVisitor<T>
{
    void Visit(TreeStructure<T> treeStructure);
    void Visit(SpecialTree<T> tree);
}

Ваш CountVisitor<T> дает мне результат 4, как и ожидалось.

Почему это не сработало?

Я попытаюсь объяснить, почему ваш пример не сработал. Метод InOrder в классе SpecialTree имеет параметр типа MyVisitor. Когда метод Visit вызывается для MyVisitor, нет перегрузки, которая занимает SpecialTree, однако есть перегрузка, которая занимает TreeStructure. C# не учитывает реализацию интерфейса при поиске правильного метода для вызова. Таким образом, вместо метода, принимающего параметр SpecialTree, вызывается метод с параметром TreeStructure.

Что, если мне все равно, какая это древовидная структура?

В ответ на ваш комментарий вы можете просто создать метод, который будет вызываться всеми реализациями дерева:

class DoNotCareVisitor<T> : MyVisitor<T> {
    void GenericVisit(TreeStructure<T> tree) {  
        //.. do whatever 
    }
    public void Visit(TreeStructure<T> tree) {
        GenericVisit(tree);
    }
    public void Visit(SpecialTree<T> tree) {
        GenericVisit(treeStructure);
    }
}
person Bas    schedule 31.10.2014
comment
ОП обновил свой код объявлением интерфейса, и вы правы, что у него есть только одна перегрузка, поэтому обновление его до вашего примера будет работать (я думаю). - person Chris; 31.10.2014
comment
Но тогда у меня есть проблема с посетителями, которым не нужно различать базовый и производный класс!? - person Roland M; 31.10.2014
comment
@RolandM Обновленный ответ - person Bas; 31.10.2014
comment
Хорошо, теперь я понимаю проблему. Но у меня есть несколько посетителей, и не всем из них нужно различать базовый и производный классы. Теперь, когда я меняю интерфейс, мне нужно реализовать эти методы и для всех посетителей, верно? - person Roland M; 31.10.2014
comment
Хм, хорошо. Спасибо за объяснение. - person Roland M; 31.10.2014
comment
@RolandM - Нет, это неправильно. Суть класса посетителя в том, что его не нужно менять. Вы переопределяете метод InOrder для любых производных классов, которые должны по-разному относиться к посетителю. - person Enigmativity; 31.10.2014
comment
@RolandM - Из Википедии шаблон дизайна посетителя - это способ отделения алгоритма от структуры объекта, с которой он работает. Если вам нужно изменить посетителя на основе изменений в структуре вашего объекта, вы не используете шаблон посетителя. - person Enigmativity; 31.10.2014
comment
Но тогда мне пришлось бы различать в посещаемом объекте, какого посетителя я вызываю. Как в этом примере: когда у меня есть посетитель, который увеличивает значение узла, я не хочу вызывать его дважды. - person Roland M; 31.10.2014

Ваш метод Visit(SpecialTree<T> specialTree) не является частью вашего интерфейса MyVisitor<T> (кстати, рекомендуется ставить перед интерфейсами заглавную букву I, поэтому IMyVisitor<T> улучшит читаемость), поэтому нет причин, по которым он будет вызываться.

Другими словами, когда вы вызываете visitor.Visit(this), ваша переменная посетителя имеет тип MyVisitor<T>, поэтому в этот момент компилятор не знает, что вы собираетесь использовать там определенный класс, который, кстати, может иметь лучшую перегрузку вашей функции.

Самый простой способ — предполагая, что вы не пытаетесь целенаправленно написать это так (и чтобы заставить его работать, вам нужно использовать проверку типов во время выполнения) — это добавить в ваш интерфейс второй метод со следующей сигнатурой: void Visit(SpecialTree<T> specialTree).

person decPL    schedule 31.10.2014

Все, что вам нужно сделать, это проверить различие между деревьями в public void Visit(TreeStructure<T> tree), например:

public class CountVisitor<T> : MyVisitor<T>
{
    public int count { get; set;}
    public CountVisitor()
    {
      count = 0;
    }

    public void Visit(TreeStructure<T> tree)
    {
      if (tree is SpecialTree<T>)
          count += 2;
      else
          count++;
    }
}
person Yogesh    schedule 31.10.2014

Вам не нужно различать два типа деревьев. Цель посетителя — обойти структуру и собрать какую-то метрику в общем виде.

Посетитель должен выглядеть так:

public class CountVisitor<T> : MyVisitor<T>
{
    public int count { get; set;}

    public CountVisitor()
    {
        count = 0;
    }

    public void Visit(TreeStructure<T> tree)
    {
        count++;
    }
}

Вот и все.

Вот что вам нужно сделать в классе SpecialTree<T>:

public override void InOrder(MyVisitor<T> visitor)
{
    if(!IsEmpty(this._left))
    {
        _left.InOrder(visitor);
    }
    visitor.Visit(this);
    visitor.Visit(this);
    if(!IsEmpty(this._right))
    {
        _right.InOrder(visitor);
    }
}

Таким образом, цель переопределения в классе SpecialTree<T> состоит в том, чтобы дважды вызвать метод .Visit.

Теперь, когда я запускаю ваш тестовый код, я получаю:

Nodecount: 4
person Enigmativity    schedule 31.10.2014
comment
Разве это не так, что посещаемый объект не заботится о своем собственном типе и что посетитель должен проверить? Или это так, что посетителю все равно? - person Roland M; 31.10.2014
comment
@RolandM - Смысл использования посетителя в том, что он отделен от структур объектов, которые он пересекает. Таким образом, единственной целью класса CountVisitor является подсчет узлов. Ему не нужно знать, что SpecialTree имеет специальное свойство, состоящее в том, что каждый узел имеет число два. Если завтра вы решите, что каждый узел в SpecialTree соответствует трем, вам не нужно менять CountVisitor — вы просто добавляете дополнительный вызов посетителю в InOrder. Вот где это принадлежит. - person Enigmativity; 31.10.2014