8

我遇到了一些继承问题,因为我有一组相互关联的抽象类,需要一起重写以创建客户端实现。理想情况下,我想做以下事情:

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 参数的协方差不应该起作用)。波动请见谅!我意识到这让很多答案看起来无关紧要。

4

17 回答 17

15

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

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

于 2008-09-05T21:37:40.180 回答
12

显然,如果您在断掉的 DogLeg 上进行手术,您将需要一个演员表。

于 2008-09-05T22:38:45.340 回答
6

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 的用户了解 DogLegs,请创建一个名为 GetDogLeg 的方法并返回一个 DogLeg。

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

于 2008-09-05T21:35:18.330 回答
4

让签名覆盖方法具有一个返回类型是被覆盖方法(phew)中返回类型的子类型是一个完全有效的愿望。毕竟,它们是运行时类型兼容的。

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

正如 Eric Lippert 在他的博客 [2008 年 6 月 19 日]中所说,您需要在可预见的未来努力解决问题:

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

我们没有计划在 C# 中实现这种差异。

于 2008-09-06T06:57:34.173 回答
3
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 上使用特殊 Hump 方法的能力。

于 2008-09-05T21:48:30.233 回答
3

您可以使用泛型和接口在 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();}
 } 
于 2008-09-06T04:37:04.370 回答
2

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

public class ClientObj{
    public void doStuff(){
    Animal a=getAnimal();
    if(a is Dog){
       DogLeg dl = (DogLeg)a.GetLeg();
    }
  }
}
于 2008-09-05T21:33:00.707 回答
2

http://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)中描述了导致您出现问题的概念

于 2008-09-05T21:38:51.677 回答
2

并不是说它很有用,但注意到 Java 确实支持协变返回可能很有趣,因此这将完全按照您的希望工作。除了显然 Java 没有属性 ;)

于 2008-09-05T22:03:05.580 回答
1

也许用一个例子更容易看出问题:

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

现在,如果你是 Dog 编译器,它应该可以编译,但我们可能不想要这样的突变体。

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

于 2008-09-05T21:54:04.977 回答
1

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,那么它将返回一条腿。

于 2008-09-16T15:39:53.133 回答
0

是的,我知道我只能施放,但这意味着客户必须知道 Dogs 有 DogLegs。我想知道的是,鉴于存在隐式转换,是否有技术原因导致这是不可能的。

于 2008-09-05T21:36:05.333 回答
0

@Brian Leahy 显然,如果您只是将其作为腿部进行操作,则无需或没有理由进行施放。但是,如果有一些 DogLeg 或 Dog 特定的行为,则有时有原因需要强制转换。

于 2008-09-05T21:38:30.600 回答
0

@卢克

我认为您可能误解了继承。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() 的返回类型是腿。

于 2008-09-05T21:46:54.027 回答
0

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

于 2008-09-05T21:47:21.187 回答
0

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

让我们来看看这个功能:

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

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

AddLeg(new Dog());

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

于 2008-09-05T21:54:20.703 回答
0

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

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 { }
于 2008-09-16T00:46:20.377 回答