Вопрос

У меня возникли некоторые проблемы с наследованием, поскольку у меня есть группа взаимосвязанных абстрактных классов, которые необходимо переопределить вместе, чтобы создать клиентскую реализацию.В идеале я хотел бы сделать что-то вроде следующего:

abstract class Animal
{
  public Leg GetLeg() {...}
}

abstract class Leg { }

class Dog : Animal
{
  public override DogLeg Leg() {...}
}

class DogLeg : Leg { }

Это позволит любому, кто использует класс Dog, автоматически получать DogLegs, а любому, кто использует класс Animal, — получать Legs.Проблема в том, что переопределенная функция должна иметь тот же тип, что и базовый класс, чтобы она не скомпилировалась.Я не понимаю, почему этого не должно быть, поскольку DogLeg неявно присоединяется к Leg.Я знаю, что есть много способов обойти это, но мне больше любопытно, почему это невозможно/не реализовано в C#.

РЕДАКТИРОВАТЬ:Я несколько изменил это, поскольку на самом деле я использую в своем коде свойства вместо функций.

РЕДАКТИРОВАТЬ:Я снова изменил его на функции, потому что ответ применим только к этой ситуации (ковариация параметра значения функции set свойства). не должен работа).Извините за колебания!Я понимаю, что из-за этого многие ответы кажутся неуместными.

Это было полезно?

Решение

Короткий ответ: GetLeg инвариантен в отношении типа возвращаемого значения.Подробный ответ можно найти здесь: Ковариантность и контравариантность

Я хотел бы добавить, что хотя наследование обычно является первым инструментом абстракции, который большинство разработчиков извлекают из своего набора инструментов, вместо него почти всегда можно использовать композицию.Композиция требует немного больше работы для разработчика API, но делает API более полезным для потребителей.

Другие советы

Очевидно, что вам понадобится актерский состав, если вы работаете на сломанной собаке.

Dog должен возвращать Leg, а не DogLeg в качестве типа возвращаемого значения.Фактическим классом может быть DogLeg, но цель состоит в том, чтобы отделить его, чтобы пользователю Dog не нужно было знать о DogLegs, ему нужно знать только о Legs.

Изменять:

class Dog : Animal
{
  public override DogLeg GetLeg() {...}
}

к:

class Dog : Animal
{
  public override Leg GetLeg() {...}
}

Не делайте этого:

 if(a instanceof Dog){
       DogLeg dl = (DogLeg)a.GetLeg();

это противоречит цели программирования абстрактного типа.

Причина скрытия DogLeg заключается в том, что функция GetLeg в абстрактном классе возвращает абстрактную ногу.Если вы переопределяете GetLeg, вы должны вернуть Leg.В этом смысл наличия метода в абстрактном классе.Чтобы распространить этот метод на его дочерние элементы.Если вы хотите, чтобы пользователи Dog знали о DogLegs, создайте метод GetDogLeg и верните DogLeg.

Если бы вы МОЖЛИ поступить так, как хочет задавший вопрос, тогда каждый пользователь Animal должен был бы знать обо ВСЕХ животных.

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

Но C# еще не поддерживает «ковариантные типы возврата» в переопределенных методах (в отличие от C++ [1998] и Java [2004]).

В обозримом будущем вам придется поработать и обойтись, как заявил Эрик Липперт в его блог[19 июня 2008 г.]:

Такая дисперсия называется «ковариацией возвращаемого типа».

у нас нет планов реализовать такую ​​вариацию в C#.

abstract class Animal
{
  public virtual Leg GetLeg ()
}

abstract class Leg { }

class Dog : Animal
{
  public override Leg GetLeg () { return new DogLeg(); }
}

class DogLeg : Leg { void Hump(); }

Сделайте это следующим образом, и тогда вы сможете использовать абстракцию в своем клиенте:

Leg myleg = myDog.GetLeg();

Затем, если вам нужно, вы можете его применить:

if (myleg is DogLeg) { ((DogLeg)myLeg).Hump()); }

Совершенно надуманно, но суть в том, чтобы вы могли сделать это:

foreach (Animal a in animals)
{
   a.GetLeg().SomeMethodThatIsOnAllLegs();
}

При этом сохраняя возможность использовать специальный метод «Горб» на «Доглегах».

Вы можете использовать дженерики и интерфейсы для реализации этого на C#:

abstract class Leg { }

interface IAnimal { Leg GetLeg(); }

abstract class Animal<TLeg> : IAnimal where TLeg : Leg
 { public abstract TLeg GetLeg();
   Leg IAnimal.GetLeg() { return this.GetLeg(); }
 }

class Dog : Animal<Dog.DogLeg>
 { public class DogLeg : Leg { }
   public override DogLeg GetLeg() { return new DogLeg();}
 } 

GetLeg() должен возвращать Leg, чтобы быть переопределением.Однако ваш класс Dog по-прежнему может возвращать объекты DogLeg, поскольку они являются дочерними классами Leg.клиенты могут затем использовать их и работать с ними как с изогнутыми ногами.

public class ClientObj{
    public void doStuff(){
    Animal a=getAnimal();
    if(a is Dog){
       DogLeg dl = (DogLeg)a.GetLeg();
    }
  }
}

Концепция, вызывающая у вас проблемы, описана на странице http://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)

Не то чтобы от этого было много пользы, но, возможно, интересно отметить, что Java поддерживает ковариантные возвраты, и поэтому это будет работать именно так, как вы надеялись.За исключением того, что у Java нет свойств;)

Возможно, проще увидеть проблему на примере:

Animal dog = new Dog();
dog.SetLeg(new CatLeg());

Теперь это должно скомпилироваться, если вы скомпилировали Dog, но нам, вероятно, не нужен такой мутант.

Связанный с этим вопрос: должен ли Dog[] быть Animal[] или IList<Dog> IList<Animal>?

В C# есть явные реализации интерфейса для решения именно этой проблемы:

abstract class Leg { }
class DogLeg : Leg { }

interface IAnimal
{
    Leg GetLeg();
}

class Dog : IAnimal
{
    public override DogLeg GetLeg() { /* */ }

    Leg IAnimal.GetLeg() { return GetLeg(); }
}

Если у вас есть Dog по ссылке типа Dog, то вызов GetLeg() вернет DogLeg.Если у вас есть тот же объект, но ссылка имеет тип IAnimal, то она вернет Leg.

Да, я понимаю, что могу просто кастовать, но это значит, что клиент должен знать, что у Собак есть DogLegs.Мне интересно, есть ли технические причины, по которым это невозможно, учитывая, что существует неявное преобразование.

@Brian Leahy Очевидно, если вы работаете только на ней как на ногу, нет необходимости или причины для броска.Но если есть какое-то специфическое поведение DogLeg или Dog, иногда бывают причины, по которым приведение необходимо.

Вы также можете вернуть интерфейс ILeg, который реализуют Leg и/или DogLeg.

Важно помнить, что вы можете использовать производный тип везде, где вы используете базовый тип (вы можете передать Dog любому методу/свойству/полю/переменной, которая ожидает Animal)

Возьмем эту функцию:

public void AddLeg(Animal a)
{
   a.Leg = new Leg();
}

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

AddLeg(new Dog());

Если свойство Dog.Leg не имеет типа Leg, функция AddLeg внезапно содержит ошибку и не может быть скомпилирована.

@Люк

Я думаю, что вы, возможно, неправильно понимаете наследование.Dog.GetLeg() вернет объект DogLeg.

public class Dog{
    public Leg GetLeg(){
         DogLeg dl = new DogLeg(super.GetLeg());
         //set dogleg specific properties
    }
}


    Animal a = getDog();
    Leg l = a.GetLeg();
    l.kick();

фактический вызываемый метод будет Dog.GetLeg();и DogLeg.Kick() (я предполагаю, что метод Leg.kick() существует), поэтому объявленный тип возвращаемого значения DogLeg не является необходимым, поскольку именно это возвращается, даже если тип возвращаемого значения для Dog.GetLeg() равен Нога.

Вы можете добиться того, чего хотите, используя обобщенный шаблон с соответствующим ограничением, например:

abstract class Animal<LegType> where LegType : Leg
{
    public abstract LegType GetLeg();
}

abstract class Leg { }

class Dog : Animal<DogLeg>
{
    public override DogLeg GetLeg()
    {
        return new DogLeg();
    }
}

class DogLeg : Leg { }
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top