Почему наследование не работает так, как я думаю, должно работать?

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

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. Я знаю, что есть много способов обойти это, но мне больше любопытно, почему это невозможно/не реализовано на С#.

EDIT: я несколько изменил это, так как фактически использую в своем коде свойства вместо функций.

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


person Luke    schedule 05.09.2008    source источник


Ответы (17)


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

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

person Apocalisp    schedule 05.09.2008
comment
Ваше заявление о дисперсии не имеет смысла. В подтипе: Ковариация = связанный тип подтипируется и Контравариантность = связанный тип получает супертип. Этот вопрос представляет ковариантный (подтип, используемый в подтипе) возвращаемый тип, тогда как C# допускает только инвариантный (тот же самый) возвращаемый тип. - person Ivan Hamilton; 07.09.2008
comment
Те статьи, на которые вы ссылаетесь, посвящены вариации общего типа. Вопрос касается ковариации возвращаемого типа. В этих статьях я прямо заявляю, что не говорю о ковариантности возвращаемого типа. - person Eric Lippert; 17.07.2010
comment
Эрик Липперт: Но эти вещи неразрывно связаны. См. здесь: apocalisp.wordpress.com/2009/08/27/ враждебное отношение к созданию подтипов - person Apocalisp; 18.07.2010

Очевидно, вам понадобится гипс, если вы оперируете сломанную DogLeg.

person vextasy    schedule 05.09.2008

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

Изменять:

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

to:

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 должен был бы знать обо ВСЕХ животных.

person Brian Leahy    schedule 05.09.2008
comment
Не имеющий отношения. Возникает вопрос, почему подтип не может переопределить метод с помощью метода, возвращаемое значение которого является подтипом возвращаемого значения переопределяемого. - person Ivan Hamilton; 07.09.2008
comment
Я могу понять аргумент о том, что пользователь класса Animal должен знать только об объектах Leg, но я менее убежден в том, что это следует скрывать от пользователей класса Dog. - person Luke; 08.09.2008

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

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

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

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

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

person Ivan Hamilton    schedule 06.09.2008

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();
}

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

person FlySwat    schedule 05.09.2008

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

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();}
 } 
person Mark Cidade    schedule 06.09.2008
comment
Имейте в виду, что это элегантно выглядящее решение может привести к сложностям из-за отсутствия в C# универсальной поддержки дисперсии. - person Ivan Hamilton; 07.09.2008
comment
какие сложности? Это решение не требует никакой дисперсии. - person Mark Cidade; 12.09.2008
comment
Вы можете либо добавить параметр типа для каждой части тела (например, Животное‹TLeg, TTtail, TTeeth›), либо поместить все части тела в объект и делегировать свойства объекту: Animal‹TParts› где TParts : PartsBase, new( ) и Собака : Animal‹DogParts› - person Mark Cidade; 05.11.2008

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

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

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

person ben    schedule 05.09.2008

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

person serg10    schedule 05.09.2008

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

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

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

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

person Tom Hawtin - tackline    schedule 05.09.2008

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.

person munificent    schedule 16.09.2008

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

person Luke    schedule 05.09.2008

@ Брайан Лихи Очевидно, что если вы работаете с ним только как с ногой, нет необходимости или причины использовать его. Но если есть какое-то специфическое поведение DogLeg или Dog, иногда есть причины, по которым бросок необходим.

person shsteimer    schedule 05.09.2008

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

person Keith Sirmons    schedule 05.09.2008

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

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

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

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

AddLeg(new Dog());

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

person Nir    schedule 05.09.2008

@Люк

Я думаю, что вы, возможно, неправильно понимаете наследование. 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() Нога.

person shsteimer    schedule 05.09.2008

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

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 { }
person Community    schedule 16.09.2008