关于c#:为什么继承不按我认为应该起作用的方式工作?

关于c#:为什么继承不按我认为应该起作用的方式工作?

Why doesn't inheritance work the way I think it should work?

我遇到了一些继承问题,因为我有一组相互关联的抽象类,需要将它们全部一起重写以创建客户端实现。理想情况下,我想执行以下操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
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的返回类型不变。长答案可以在这里找到:协方差和协方差

我想补充一点,虽然继承通常是大多数开发人员从其工具箱中提取的第一个抽象工具,但几乎总是可以使用合成代替。对于API开发人员而言,组合工作要稍微多一些,但会使API对它的使用者更有用。


显然,如果您是
在破碎的DogLeg上操作。


Dog应该返回腿而不是DogLeg作为返回类型。实际的类可能是DogLeg,但是关键是要解耦,因此Dog的用户不必了解DogLegs,他们只需要了解Legs。

更改:

1
2
3
4
class Dog : Animal
{
  public override DogLeg GetLeg() {...}
}

至:

1
2
3
4
class Dog : Animal
{
  public override Leg GetLeg() {...}
}

不要这样做:

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

它违反了编程为抽象类型的目的。

隐藏DogLeg的原因是因为抽象类中的GetLeg函数返回了抽象腿。如果要覆盖GetLeg,则必须返回Leg。那就是在抽象类中具有方法的意义。将该方法传播给孩子。如果您希望Dog的用户了解DogLegs,请创建一个名为GetDogLeg的方法并返回DogLeg。

如果您可以按问问者的意愿进行操作,那么Animal的每个用户都需要了解所有动物。


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

但是C#尚不支持重写方法中的"协变量返回类型"(与C ++ [1998]和Java [2004]不同)。

正如埃里克·利珀特(Eric Lippert)在他的博客中所说,您需要努力并为可预见的未来做好准备。
[2008年6月19日]:

That kind of variance is called"return type covariance".

we have no plans to implement that kind of variance in C#.


1
2
3
4
5
6
7
8
9
10
11
12
13
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(); }

这样做,然后可以在客户端中利用抽象:

1
Leg myleg = myDog.GetLeg();

然后,如果需要,可以将其转换为:

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

完全是人为的,但重点是您可以这样做:

1
2
3
4
foreach (Animal a in animals)
{
   a.GetLeg().SomeMethodThatIsOnAllLegs();
}

同时仍然保留对Doglegs使用特殊驼峰方法的能力。


您可以使用泛型和接口在C#中实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
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();}
 }

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


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


GetLeg()必须返回Leg作为重写。但是,由于Dog类是Leg的子类,因此您的Dog类仍然可以返回DogLeg对象。然后,客户可以像狗狗一样投下并对其进行操作。

1
2
3
4
5
6
7
8
public class ClientObj{
    public void doStuff(){
    Animal a=getAnimal();
    if(a is Dog){
       DogLeg dl = (DogLeg)a.GetLeg();
    }
  }
}

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

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

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

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


C#具有显式的接口实现来解决此问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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。


@卢克

我认为您也许对继承有误解。 Dog.GetLeg()将返回DogLeg对象。

1
2
3
4
5
6
7
8
9
10
11
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()的返回类型为腿。


@布莱恩·莱希
显然,如果您仅以支腿的形式进行操作,则没有必要或理由进行铸造。但是,如果存在某些DogLeg或Dog特定行为,则有时出于某些原因,必须进行强制转换。


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

让我们使用以下功能:

1
2
3
4
public void AddLeg(Animal a)
{
   a.Leg = new Leg();
}

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

1
AddLeg(new Dog());

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


是的,我知道我可以施放,但这意味着客户必须知道Dogs有DogLegs。我想知道的是,鉴于存在隐式转换,是否有技术原因无法做到这一点。


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 { }

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


推荐阅读

    linux命令测试客户端?

    linux命令测试客户端?,地址,系统,网络,工具,工作,分析,环境,命令,下行,资料,l

    go操作linux命令?

    go操作linux命令?,系统,工具,环境,软件,标准,网上,最新,信息,设备,电脑,Linux

    linux命令操作vim?

    linux命令操作vim?,系统,地址,工作,命令,模式,标准,信息,入口,连续,时间,Linu

    linux操作不了命令?

    linux操作不了命令?,系统,密码,不了,命令,位置,信息,情况,网络,地址,定期,lin

    linux运维操作命令?

    linux运维操作命令?,系统,网络,基础,标准,工具,信息,工作,命令,软件,数据,lin

    linux命令行常见操作?

    linux命令行常见操作?,系统,工作,地址,管理,信息,命令,目录,一致,基础,标准,l

    linux清空操作命令?

    linux清空操作命令?,系统,数据,命令,名称,不了,文件夹,文件,环境,回收站,目

    linux安装操作命令?

    linux安装操作命令?,系统,工作,地址,工具,服务,情况,信息,命令,最新,管理,lin

    linux操作系mv命令?

    linux操作系mv命令?,名字,系统,软件,文件,命令,信息,地址,目录,文件夹,源文

    linux操作命令大全图?

    linux操作命令大全图?,工作,地址,系统,信息,命令,目录,控制台,功能,操作,内

    linux操作命令论文?

    linux操作命令论文?,管理,系统,设计,网络,企业,基础,机电设备,技术,发展,机

    linux操作命令cp?

    linux操作命令cp?,系统,文件,命令,源文件,基本知识,时间,目录,文件夹,选项,

    linux操作命令及用法?

    linux操作命令及用法?,工作,地址,系统,管理,信息,命令,目录,网络,文件,操作,L

    linux基础命令和操作?

    linux基础命令和操作?,工作,地址,基础,命令,管理,标准,系统,目录,单位,信息,l

    linux中查询操作命令?

    linux中查询操作命令?,信息,系统,名称,命令,地址,数字,指令,用户,历史,文件,

    linux线程操作命令?

    linux线程操作命令?,系统,工作,时间,代码,线程,信息,第一,命令,函数,操作,lin

    查看linux操作的命令?

    查看linux操作的命令?,系统,工作,信息,情况,地址,命令,软件,电脑,第一,名称,l

    linux压缩操作命令?

    linux压缩操作命令?,系统,命令,文件,名称,数据,软件,平台,目录,选项,下面,lin

    linux系统用命令操作?

    linux系统用命令操作?,系统,情况,工作,发行,位置,管理,地址,信息,密码,命令,l

    linux历史操作命令?

    linux历史操作命令?,系统,信息,地址,工作,名称,命令,管理,用户,操作,历史,如