为什么inheritance不按我认为应该工作的方式工作?

我有一些inheritance问题,因为我有一组相互关联的抽象类需要全部重写以创建客户端实现。 理想情况下,我想做类似以下的事情:

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函数的value参数的协方差不应该起作用)。 对不起波动! 我意识到这使得许多答案看起来无关紧要。

简短的回答是GetLeg在其返回类型中是不变的。 可以在这里找到答案很长的答案: 协方差和逆变

我想补充一点,虽然inheritance通常是大多数开发人员从他们的工具箱中取出的第一个抽象工具,但几乎总是可以使用组合。 对于API开发人员来说,组合稍微有些工作,但是对于其消费者来说API更有用。

显然,如果你在破损的DogLeg上操作,你需要一个演员阵容。

狗应该返回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函数返回一个抽象的Leg。 如果您要覆盖GetLeg,则必须返回Leg。 这就是在抽象类中使用方法的重点。 将该方法传播给它的孩子们。 如果您希望Dog的用户了解DogLegs,请创建一个名为GetDogLeg的方法并返回DogLeg。

如果您可以像提问者那样做,那么Animal的每个用户都需要了解所有动物。

完全有效的愿望是让签名的重写方法具有返回类型,该类型是重写方法( phew )中返回类型的子类型。 毕竟,它们与运行时类型兼容。

但是C#在重写方法中还不支持“协变返回类型”(与C ++ [1998]和Java [2004]不同)。

正如Eric Lippert在他的博客 [2008年6月19日]中所说的那样,你需要在可预见的未来努力工作并做好准备:

这种方差称为“返回类型协方差”。

我们没有计划在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(); } 

虽然仍然保留了在Doglegs上使用特殊驼峰方法的能力。

您可以使用generics和接口在C#中实现它:

 abstract class Leg { } interface IAnimal { Leg GetLeg(); } abstract class Animal : IAnimal where TLeg : Leg { public abstract TLeg GetLeg(); Leg IAnimal.GetLeg() { return this.GetLeg(); } } class Dog : Animal { public class DogLeg : Leg { } public override DogLeg GetLeg() { return new DogLeg();} } 

GetLeg()必须返回Leg作为覆盖。 但是,您的Dog类仍然可以返回DogLeg对象,因为它们是Leg的子类。 然后客户可以像doglegs一样投射和操作它们。

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

并不是说它有多大用处,但注意Java确实支持协变返回可能很有意思,所以这将完全符合您的预期。 显然Java没有属性;)

也许通过示例更容易看到问题:

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

如果你编译了Dog,那应该编译,但我们可能不想要这样的突变体。

一个相关的问题是Dog []应该是Animal [],还是IList IList

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。

是的,我知道我可以投,但这意味着客户必须知道Dogs有DogLegs。 我想知道的是,如果存在技术原因,为什么这是不可能的,因为存在隐式转换。

@Brian Leahy显然如果你只是作为一个腿进行操作,就没有必要或没有理由去施展。 但是如果有一些DogLeg或Dog特定的行为,有时候有理由认为演员是必要的。

您还可以返回Leg和/或DogLeg都实现的接口ILeg。

要记住的重要一点是,您可以在每个使用基类型的地方使用派生类型(您可以将Dog传递给任何期望Animal的方法/属性/字段/变量)

我们来看看这个function:

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

一个完全有效的函数,现在让我们调用这样的函数:

 AddLeg(new Dog()); 

如果属性Dog.Leg不是Leg类型,则AddLeg函数突然包含错误,无法编译。

@Luke

我想你或许是对遗产的误解。 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()的返回类型是腿。

您可以通过使用具有适当约束的generics来实现您想要的效果,如下所示:

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