继承(面向对象的编程)
在面向对象的编程中,继承是基于对象或类基于另一个对象(基于原型的继承)或类(基于类的继承)的机制,以保留类似的实现。也定义为从现有的类别或基类等现有类别的新类(子类)中,然后将它们形成为类的层次结构。在大多数基于班级的面向对象的语言(例如C ++)中,通过继承创建的对象,“子对象”,都会获取“父对象”的所有属性和行为,除了:构造函数,构造函数,破坏者,超载操作员和朋友基类的功能。继承允许程序员创建建立在现有类上的类,指定新实现的同时维护相同的行为(实现接口),重复使用代码并通过公共类和接口独立扩展原始软件。通过继承的对像或类的关系产生了定向的无环图。
继承的类称为其父级或超级类的子类。术语“继承”既适用于基于类的基于类”和基于原型的编程,但是在狭义的使用中,该术语保留用于基于类的编程(一个类来自另一个类),基于原型的编程中的相应技术是而是称为委托(一个对象委托给另一个对象)。可以根据简单的网络接口参数预先定义类调整的继承模式,从而保留语言间兼容性。
继承不应与亚型混淆。在某些语言中,继承和亚型同意,而在另一些语言中,它们有所不同。通常,子类型建立了IS-A关系,而继承仅重新实现实施并建立句法关系,而不一定是语义关系(继承不能确保行为性亚型)。为了区分这些概念,有时将子类型称为接口继承(不承认类型变量的专业化也引起了亚型关系),而此处定义的继承称为实现继承或代码继承。尽管如此,继承是建立亚型关系的常用机制。
继承与对象组成对比,其中一个对象包含另一个对象(或一个类的对象包含另一类的对象);请参阅继承的组成。与亚型的IS-A关系相反,组成实现了Has-A关系。
历史
1966年,托尼·霍尔(Tony Hoare)在记录上发表了一些评论,特别是介绍了记录子类,具有共同属性的记录类型的想法,但被变体标签歧视,并在变体中私有字段。在此影响的情况下,1967年Ole-Johan Dahl和Kristen Nygaard提出了一种设计,允许指定属于不同类但具有共同特性的对象。共同的特性是在超级类中收集的,每个超类本身可能具有超类。因此,子类的值是复合对象,由属于各种超级类的一些前缀部分以及属于子类的主要部分组成。这些部分都被连接在一起。通过点表示法可以访问复合对象的属性。这个想法首先在Simula 67编程语言中采用。然后,这个想法传播到SmallTalk , C ++ , Java , Python和许多其他语言。
类型
基于范式和特定语言,有多种类型的继承。
- 单个继承
- 子类继承一个超类的功能。一个类获取另一类的属性。
- 多元继承
- 一个类可以拥有一个以上的超级类并继承所有父类的功能。
-
“多重继承 ...广泛认为很难有效地实施。例如,在C ++的摘要中,布拉德·考克斯(Brad Cox)实际上声称,在C ++上添加多个继承是不可能的。因此,多重继承似乎更像是一个挑战。由于我早在1982年就考虑过多种继承,并在1984年发现了一种简单有效的实施技术,所以我无法抗拒挑战。我怀疑这是唯一影响事件顺序的情况。”
- 多级继承
- 其中一个子类从另一个子类继承。并不少见的是,类是从另一个派生类派生的,如图“多级继承”所示。
- A类充当派生B类的基类,这又用作派生类C的基类。 B类被称为中间基类,因为它为A和C之间的继承提供了链接。链条ABC被称为继承路径。
- 具有多级继承的派生类如下:
// C++ language implementation class A { ... }; // Base class class B : public A { ... }; // B derived from A class C : public B { ... }; // C derived from B
- 此过程可以扩展到任意数量的级别。
- 分层继承
- 在这里,一个班级作为一个以上的子类充当超类(基类)。例如,父类A可以具有两个子类B和C。B和C的父类都是A,但是B和C是两个单独的子类。
- 混合继承
- 混合继承是在发生上述两种类型的继承的混合物时。一个例子是当A类具有一个子类B,该子类B具有两个子类C和D。这是多级继承和层次继承的混合物。
子类和超类
子类,派生类,继承人类或子类是模块化衍生类类,它们从一个或多个其他类(称为超类,基础类或父级)继承一个或多个语言实体。班级继承的语义因语言而异,但通常该子类自动继承其超类的实例变量和成员函数。
定义派生类的一般形式是:
class SubClass: visibility SuperClass
{
// subclass members
};
- 结肠表明子类从超级类继承。可见性是可选的,如果存在,则可以是私人的或公共的。默认可见性是私人的。可见性指定基类的功能是私人派生还是公开派生。
某些语言还支持其他结构的继承。例如,在艾菲尔(Eiffel)中,定义班级规范的合同也由继承人继承。超类建立了一个常见的接口和基础功能,专门的子类可以继承,修改和补充。子类继承的软件被认为是在子类中重复使用的。对类的实例的引用实际上可能是指其子类之一。在编译时,无法预测所引用的对象的实际类。统一接口用于调用许多不同类别的对象的成员函数。子类可以用全新的功能替代超级类功能,这些功能必须共享相同的方法签名。
不可分类的类
在某些语言中,可以通过将某些类修饰符添加到类声明中,将类声明为不可分类。示例包括Java和C ++ 11中的final
关键字或C#中的sealed
关键字。在class
关键字和类标识符声明之前,将这些修饰符添加到类声明中。这种不可分类的类限制了可重复使用性,尤其是当开发人员只能访问预编译二进制文件而不访问源代码时。
一个不可分类的类没有子类别,因此可以在编译时间很容易地推导出它的参考或指示该类的对象实际上是参考该类的实例,而不是子类的实例(它们不存在)或超类的实例(向上引用类型违反了类型系统)。由于要引用的对象的确切类型是在执行之前已知的,因此可以使用早期绑定(也称为静态调度)代替晚期绑定(也称为Dynamic Dispatch ),该绑定需要一个或多个虚拟方法表查找,取决于多个继承是否取决于多个继承或者仅使用正在使用的编程语言支持单个继承。
不可剥夺的方法
正如类可能是不可分类的一样,方法声明可能包含方法修饰符,以防止该方法被覆盖(即用相同的名称和类型签名在子类中替换为新功能)。一种私有方法是无法克服的,仅仅是因为它是通过类以外的类访问的,它是成员的函数(尽管对于C ++是不正确的)。 Java中的final
方法,C#中的sealed
方法或Eiffel中的frozen
功能不能被覆盖。
虚拟方法
如果超类方法是一种虚拟方法,则将动态派遣超类方法的调用。某些语言要求该方法被专门称为虚拟(例如C ++),在其他方法中,所有方法都是虚拟的(例如Java)。非虚拟方法的调用将始终在静态上派遣(即,在编译时确定函数调用的地址)。静态调度比动态调度更快,并且允许进行优化,例如内联膨胀。
继承成员的可见性
下表显示了使用C ++建立的术语来取决于衍生类时给定的可见性的哪些变量和函数的继承。
基类可见性 | 派生的类可见性 | ||
---|---|---|---|
私人推导 | 受保护的推导 | 公共推导 | |
|
|
|
|
申请
继承用于彼此共同汇总两个或多个类。
覆盖
许多面向对象的编程语言允许类或对象替换其继承的方面的实现(通常是一种行为)。此过程称为覆盖。覆盖介绍了一个复杂的问题:哪个版本的行为是继承类使用的实例(是其自身类别的一部分),还是来自父(基本)类的类别?答案在编程语言之间有所不同,某些语言提供了表明不覆盖特定行为的能力,并且应按照基类的定义。例如,在C#中,只有在子类中标记了虚拟,抽像或覆盖修饰符的基本方法或属性,而在诸如Java之类的编程语言中,可以调用不同的方法来覆盖其他方法。覆盖的替代方法是隐藏继承的代码。
代码重复使用
实现继承是基类中的子类重新使用代码的机制。默认情况下,子类保留了基类的所有操作,但是子类可以覆盖某些或所有操作,从而用自己的基础级实现来代替基础类实现。
在以下python示例中,子类 SquareSumComputer和CubesumComputer覆盖基类SumComputer的转换方法。基类包括操作,以计算两个整数之间的平方和。除了将数字转换为正方形的操作外,子类重复了基类的所有功能,但分别将数字转换为正方形和立方体的操作将其替换为正方形。因此,子类计算两个整数之间的正方形/立方体的总和。
以下是Python的示例。
class SumComputer:
def __init__(self, a, b):
self.a = a
self.b = b
def transform(self, x):
raise NotImplementedError
def inputs(self):
return range(self.a, self.b)
def compute(self):
return sum(self.transform(value) for value in self.inputs())
class SquareSumComputer(SumComputer):
def transform(self, x):
return x * x
class CubeSumComputer(SumComputer):
def transform(self, x):
return x * x * x
在大多数季度中,出于代码重复使用的唯一目的的班级继承已失败。主要问题是,实施继承不能提供多态性替代性的任何保证,而重复使用类的实例不一定被替换为继承类的实例。替代技术,明确的授权需要更多的编程工作,但避免了替代性问题。在C ++中,私有继承可以用作实施继承的一种形式,而无需替代性。公共继承代表“ IS-A”关系,而授权代表“ has-a”关系,而私人(和受保护的)继承可以被视为“一种”关系。
继承的另一个频繁使用是确保类保持某种共同的界面;也就是说,他们实现了相同的方法。母类可以是在子类中实施的实施操作和操作的组合。通常,Supertype和子类型之间没有接口变化 - 孩子实现所描述的行为而不是其父级。
继承与亚型
继承与亚型相似,但与众不同。亚型使给定类型可以代替另一种类型或抽象,并且据说可以根据语言支持建立隐式或明确的某些现有抽象之间的IS-A关系。该关系可以通过在支持继承作为亚型机制的语言中明确表示。例如,以下C ++代码建立了B类之间的显式继承关系,其中B既是A的子类,又是A的子类型,并且可以用作指定B的任何位置(通过参考,指针或指针或指针,或对象本身)。
class A {
public:
void DoSomethingALike() const {}
};
class B : public A {
public:
void DoSomethingBLike() const {}
};
void UseAnA(const A& a) {
a.DoSomethingALike();
}
void SomeFunc() {
B b;
UseAnA(b); // b can be substituted for an A.
}
与类型之间的关系相比,在不支持继承作为亚型机制的编程语言中,基类和派生类之间的关系只是实现(代码重用的机制)之间的关系。继承,即使在支持继承作为亚型机制的编程语言中,也不一定需要行为亚型。在预期父类的上下文中使用时对象的行为将不正确地得出一个类,该类完全有可能得出一个类。请参阅Liskov替代原则。 (比较内涵/表示。)在某些OOP语言中,代码重用和亚型的概念重合,因为声明子类型的唯一方法是定义一个继承另一个实现另一个的新类。
设计约束
在设计程序时广泛使用继承会施加某些限制。
例如,考虑一个包含一个人的姓名,出生日期,地址和电话号码的人。我们可以定义一个名为“学生”的子类,其中包含该人的平均成绩和上课,还有另一个名为雇员的子类,其中包含该人的工作标题,雇主和薪水。
在定义这种继承层次结构时,我们已经定义了某些限制,并非所有限制都是可取的:
- 单身
- 使用单个继承,一个子类只能从一个超类继承。继续上面给出的示例,一个人对象可以是学生或雇员,但不是两者兼而有之。使用多个继承可以部分解决此问题,因为然后可以定义从学生和员工继承的学生雇员类。但是,在大多数实施中,它仍然只能从每个超级阶级继承一次,因此不支持学生有两个工作或参加两个机构的案件。 EIFFEL中可用的继承模型通过支持重复继承使这成为可能。
- 静止的
- 当对象的类型被选中并且不会随时间变化时,对象的继承层次结构已固定在实例上。例如,继承图不允许学生对像在保留人的超级阶级状态的同时成为员工对象。 (但是,可以通过装饰图案实现这种行为。)有些人批评继承,认为它将开发人员置于其原始设计标准中。
- 能见度
- 每当客户端代码访问对象时,通常都可以访问所有对象的超类数据。即使尚未公开宣布超级阶级,客户仍然可以将对象投入其超类型类型。例如,没有办法将功能作为学生的平均成绩和成绩单的指针,而无需使该功能访问学生的所有个人数据中存储的所有个人数据。许多现代语言,包括C ++和Java,都提供了“受保护的”访问修饰符,允许子类访问数据,而无需允许继承链之外的任何代码访问它。
复合重用原理是继承的替代方法。该技术通过将行为与小学等级层次结构分开,并根据任何业务领域类要求包括特定的行为类别来支持多态性和代码重复使用。这种方法通过允许在运行时进行行为修改并允许一个类实现自助餐风格的行为,而不是仅限于其祖先类的行为,从而避免了类层次结构的静态性质。
问题和替代方案
从至少1990年代起,实施继承是有争议的,以对象为导向编程的理论家。其中包括设计模式的作者,他们提倡界面继承,而偏爱构图而不是继承。例如,已经提出了装饰物图案(如上所述)来克服阶级之间继承的静态性质。作为解决同一问题的更基本的解决方案,面向角色的编程引入了独特的关系,将继承和构图的属性结合到一个新概念中。
根据艾伦·霍鲁布(Allen Holub)的说法,实现继承的主要问题是,它以“脆弱的基层问题”的形式引入了不必要的耦合:基础类实现的修改可能会导致子类的无意义行为变化。使用接口会避免此问题,因为没有共享实现,而仅共享API。说明这一点的另一种方式是“继承破坏封装”。问题表面在开放的对象系统(例如框架)中,预计客户端代码将从系统提供的类中继承,然后在其算法中替换为系统类。
据报导,Java发明家詹姆斯·高斯林(James Gosling)表示反对实施继承,并指出,如果他要重新设计Java,他将不包括它。从1990年才出现了将继承与子类型(接口继承)相结合的语言设计;一个现代的例子是Go编程语言。
复杂的继承或在不足的成熟设计中使用的继承可能导致溜溜球问题。当继承在1990年代后期用作构建程序的主要方法时,随着系统功能的增长,开发人员倾向于将代码分解为更多的继承层。如果一个开发团队将继承的多层继承与单个责任原则结合在一起,则导致许多非常薄的代码层,许多层仅由1或2行实际代码组成。太多的层使调试成为一个重大挑战,因为很难确定需要调试哪个层。
继承的另一个问题是必须在代码中定义子类,这意味着程序用户不能在运行时添加新的子类。其他设计模式(例如实体 - 组件 - 系统)允许程序用户在运行时定义实体的变化。