为什么继承不能按照我认为应该的方式工作?
-
09-06-2019 - |
题
我遇到了一些继承问题,因为我有一组相互关联的抽象类,需要一起重写它们才能创建客户端实现。理想情况下,我想做类似以下的事情:
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# 中不可能/实现。
编辑: :我对此进行了一些修改,因为我实际上在代码中使用属性而不是函数。
编辑: :我将其改回函数,因为答案仅适用于这种情况(属性设置函数的值参数的协方差 不应该 工作)。对于波动,敬请谅解!我意识到这让很多答案显得无关紧要。
解决方案
简短的回答是 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函数返回一个Abstract Leg。如果您要重写 GetLeg,则必须返回一个 Leg。这就是在抽象类中拥有方法的要点。将该方法传播给它的子方法。如果您希望 Dog 的用户了解 DogLeg,请创建一个名为 GetDogLeg 的方法并返回 DogLeg。
如果你可以按照提问者想要的那样做,那么动物的每个用户都需要了解所有动物。
让重写方法的签名具有作为被重写方法中返回类型的子类型的返回类型是完全合理的愿望(唷)。毕竟,它们是运行时类型兼容的。
但 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();
}
同时仍然保留对狗腿有特殊驼峰方法的能力。
您可以使用泛型和接口在 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();
}
}
}
并不是说它有多大用处,但值得注意的是 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。
是的,我知道我可以直接投射,但这意味着客户必须知道 Dogs 有 DogLegs。我想知道是否存在技术原因导致这不可能,因为存在隐式转换。
@Brian Leahy显然,如果您仅作为腿部操作,就不需要或理由铸造。但如果存在一些 DogLeg 或 Dog 的特定行为,有时就有必要进行强制转换。
您还可以返回 Leg 和/或 DogLeg 实现的接口 ILeg。
要记住的重要一点是,您可以在使用基类型的每个地方使用派生类型(您可以将 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 { }