面向对象编程/继承
在许多书籍中,继承和 OOP 被认为是同义词,因此我们如此迟才讨论这个话题可能看起来很奇怪。这反映了继承随着时间的推移而发挥的作用越来越小。事实上,经典 OOP 和现代 OOP 之间的主要区别之一就在于继承的使用方式。正如老话所说,如果你只有一把锤子,那么所有东西看起来都像钉子。因此,继承往往是当时 OOP 程序员唯一的可用工具,因此所有概念都被塞进了继承之中。这种缺乏概念完整性和关注点分离导致了过于亲密的依赖关系和许多困难。在一些语言中,程序员的技术发展使得使用相同的有限语言功能使概念更清晰,而其他语言则明确开发了功能来解决这些问题。由于你在 OOP 学习中的某个阶段几乎肯定会接触到一些这些误导性的建议,因此我们将尝试解释一些问题。然而,大部分讨论将在它们适得其所的部分进行!
首先,什么是继承?嗯,就像你奶奶去世了你继承了她那件难看透顶的灯一样,OOP 中的继承会赋予子类(或派生类)所有父类(或超类)的属性。例如(在 C++ 中)
class Parent
{
public:
int f() {return 10;}
};
class Child : public Parent
{
// see, nothing here! But wait, we inherited the function f()!
};
Child c;
int result = c.f(); // huh? Oh, f() was inherited from the parent!
你会看到关于继承的最常用也是最无价值的讨论围绕着“Is-A 与 Has-A”展开。例如,汽车是车辆,但它有一个方向盘。这些作者想要表达的意思是,你的汽车类应该继承你的车辆类,并且有一个方向盘作为成员。你可能还会遇到形状的例子,其中矩形是形状。关键是,再次强调,抽象。目标是识别仅对车辆或形状的抽象概念进行操作的操作,然后只编写一次代码。然后,通过继承的魔力,你可以将汽车或矩形或任何东西传递给通用代码,它将会起作用,因为派生类是父类的一切,“再加上更多”。
这里的问题是,继承将几个东西混杂在一起:你同时继承了“类型性”、“接口”和“实现”。然而,所有这些例子都关注接口,而谈论的是“类型性”。抽象代码并不关心汽车“是”车辆,只关心对象是否响应一组特定的函数,即接口。事实上,如果你想给你的椅子类赋予 accelerate()、brake()、turn_left() 和 turn_right() 方法,抽象代码是否应该能够处理椅子?当然,但那并不能使椅子成为车辆。
因此,在这些“is-a”讨论中提出的解决方案,大多已被所谓的接口编程和模板编程所取代。由于模板编程提供了最松散的耦合,因此它将注意力集中到语法语义混淆上。仅仅因为你拥有具有正确名称的函数,是否意味着它们按照你的预期工作?如果椅子类拥有 accelerate()、brake() 和其他车辆类型的函数,那么让通用车辆代码处理这个椅子是否合理?这导致了对通用代码假设的更多规范:例如,brake(INFINITY) ==> STOPPED。这意味着 brake(x) != accelerate(-x)。因此,椅子可能比企业号宇宙飞船更适合成为车辆!
你将在后面关于消息传递的部分看到更多关于委托的内容,但我们要提到,在没有提供委托功能的语言中,继承通常解决了许多由委托解决的问题。举个例子比解释更容易,因此简而言之,以下是委托的示例
interface I
{
int f();
}
class A implements I
{
// A member variable that actually services all requests for
// calls to the I interface.
delegate I private_i;
A(I target)
{
private_i = target;
}
}
class B implements I
{
int f() { return 1; }
}
B b; // create a B object to be the target of A's delegation
A a(b); // Create an A object, passing in b
int foo = a.f(); // This call to f() is delegated to b.f(), causing 1 to be returned.
继承完成了一个类似的壮举
class B
{
int f() { return 1; }
}
class A inherits B
{
}
A a;
int foo = a.f(); // returns 1 just like in the delegation example.
但是,委托可以随着时间推移而改变,而基类则不能。因此,委托有时被称为“动态继承”。
多重继承是指一个对象从多个不同的类继承其属性。例如,房子既是“建筑物”,又是“睡觉的地方”。
在多重继承中,一个对象从多个对象(其父类或基类)继承其属性。因此,我们需要设置一些类,我们将使用 C++ 来完成
class Building
{
int size;
int purpose;
int price;
};
class PlaceToSleepIn
{
int type; // tent, house, dorm, etc.
bool pleasant_to_live_in; // true or false
};
在 C++ 中,要继承所有类型,使用 : 运算符,例如
class House : public Building, public PlaceToSleepIn
{
// all of the Building and PlaceToSleepIn variables are here!
int rooms; // the number of rooms
};
继承的一个问题是,预定义类的用户可能不知道抽象数据的存储位置。因此,他们可能难以获取它。大多数继承问题是由于设计脆弱性或编程语言实现方式引起的。
在处理大型程序中的继承时,一个常见的问题被称为脆弱的基类。当基类的子类对基类提供的契约之外的属性(属性的定义)做出假设时,就会出现这种情况。
一个例子可能是,一个属性在基类中声明为字节,但后来发现需要字的范围。将其更改为字将至少需要重新编译所有子类,但这可能还不够,因为子类可能依赖于该属性的类型,因此也需要更改源代码。
这种关于给定实现的实现者固有知识,通常是通用编程中的问题,但 OOP/OOD 试图通过提供更多机会来实现更好的编程实践来解决这个问题。但与代码中的任何其他事物一样,它取决于程序员是否充分利用它,使其尽可能透明,以便其他人能够充分利用其代码,从而利用继承的特性。