C++学习笔记----9、发现继承的技巧(四)---- 多态继承(1)

时间:2024-10-22 07:58:21

        现在理解了继承类与父类的关系,可以在最强大的场景--多态上使用继承了。以前我们讨论过多态允许用通用父类交换使用对象,在使用父类的地方使用本尊。

1、Spreadsheet的回归

        前面两章使用spreadsheet程序作为应用程序的例子引向面向对象的设计。SpreadsheetCell代表了数据的一个元素。到现在为止,该元素只是保存了一个单独的double值。接着是一个SpreadsheetCell的简单类定义。注意到单元格可以被设置为double或string_view,但是保存的依然是double。单元格的当前值,总是返回一个string。

class SpreadsheetCell
{
public:
    virtual void set(double value);
    virtual void set(std::string_view value);
    virtual std::string getString() const;
private:
    static std::string doubleToString(double value);
    static double stringToDouble(std::string_view value);
    double m_value;
};

        在真实的spreadsheet应用程序中,单元格会保存不同的数据。一个单元格可以保存一个double,但是也可以保存一段文本。可能会有更多单元格类型的要求,例如公式单元格或日期单元格。怎么支持这些呢?

2、设计多态Spreadsheet单元格

        SpreadsheetCell类要扩展到层次结构了。可行的方法是限制其SpreadsheetCell的范围到只包含string,可能在过程中会将其重命名为StringSpreadsheetCell。为了处理double,再用一个类,DoubleSpreadsheetCell,继承自StringSpreadsheetCell,提供对于自身格式的特定功能。下图展示了这种设计。

        这个方法给重用继承打了个样儿,因为DoubleSpreadsheetCell从StringSpreadsheetCell继承只是为了使用其内置的一些功能。

        如果你要实现上图所示的设计,可能会发现继承类重载了大部分,如果不是全部的话,基类的功能。因为在几乎所有的情况下double与string的处理都是不同的,其关系可能不是我们一开始理解的那样。但是,在单元格包含string与单元格包含double之间的关系是清晰的。上图展示的模型,DoubleSpreadsheetCell是一个StringSpreadsheetCell,这种模型一看就不对,更好的设计是使这些类为一个通用类SpreadsheetCell的子类。下图展示了这样的设计。

        上图的设计展示了SpreadsheetCell层次关系的多态方法。因DoubleSpreadsheetCell与StringSpreadsheetCell都继承自一个通用的父类SpreadsheetCell,在其它代码看来,它们是可交换的,在实践中,其意义如下:

  • 两个继承类都支持基类定义的同样的接口(成员函数集)。
  • 使用SpreadsheetCell对象的代码甚至可以在不知道单元格是DoubleSpreadsheetCell还是StringSpreadsheetCell的情况下就可以调用接口中的任意的成员函数。
  • 通过virtual成员函数的魔法,接口中每个成员函数的恰当的实例依据对象的类不同而被调用。
  • 其它数据结构,例如Spreadsheet类,可以包含通过指向基类的多种类型单元格的集合。

3、SpreadsheetCell基类

        因为所有的spreadsheet单元格都继承自SpreadsheetCell基类,首先写这个类可能是一个好主意。当设计一个基类的时候,需要考虑继承类与其关系 。依据这个信息,可以继承进入父类的通用性。例如,string单元格与double单元格在包含一个数据止是类似的。因为数据来自于用户,又回显给用户,该值被设置为string,作为string查询。这些行为是共享功能,组成了基类。

3.1、第一次尝试

        SpreadsheetCell基类负责定义所有SpreadsheetCell继承类支持的行为。在这个例子中,所有的单元格需要能够被设置其值为string。所有的单元格也需要能够作为string返回其当前值。基类定义声明了这些成员函数,也显式地缺省了virtual析构函数,但是注意它没有数据成员。定义在spreadsheet_cell模块中。

export module spreadsheet_cell;

import std;

export class SpreadsheetCell
{
public:
	virtual ~SpreadsheetCell() = default;

	virtual void set(std::string_view value);

	virtual std::string getString() const;
};

        当你在为该类写.cpp文件是地,马上就会碰到一个总是。考虑到spreadsheet基类的单元格包含的既不是double也不是string数据成员,怎么实现呢?说得更通用一些,怎么写一个基类声明了被继承类支持的行为,而没有实际定义这些行为的实现?

        一个可能的方法是为这些行为实现“什么也不做”的功能。例如,在SpreadsheetCell基类上调用set()成员函数没有效果,因为基类没有什么可以设置的。然而,这个方法仍然感觉不对。理想情况下,在基类的实例中不应该有这么一个对象。调用set()应该总是应该有效果的,因为它应该要么在DoubleSpreadsheetCell上,要么在StringSpreadsheetcell上调用。好的解决方案加强了这一限制。

3.2、干净的virtual成员函数与抽象基类

        干净的virtual成员函数是指在类定义中没有显式定义的成员函数。为了使成员函数成为干净的virtual,需要告诉编译器在当前类中不存在该成员函数的定义。至少有一个干净的virtual成员函数的类叫做抽象类,因为没有代码能够实例化它。编译器强化了这个事实,如果一个类包含一个或者多个干净的virtual成员函数,它不能用于构建该类型的对象。

        设计一个干净的virtual成员函数有一个特别的语法。该成员函数声明跟随=0。不需要写实现。

export module spreadsheet_cell;

import std;

export class SpreadsheetCell
{
public:
	virtual ~SpreadsheetCell() = default;

	virtual void set(std::string_view value) = 0;

	virtual std::string getString() const = 0;
};

        既然基类是一个抽象类,生成SpreadsheetCell对象是不可能的。下面的代码编译失败,返回错误,如:“’SpreadsheetCell’:无法初始化抽象类”:

SpreadsheetCell cell; // Error! Attempts creating abstract class instance.

        然而,一旦StringSpreadsheetCell类被实现,下面的代码就会编译成功,因为它实例化了一个抽象基类的继承类:

unique_ptr<SpreadsheetCell> cell { new StringSpreadsheetCell {} };

        注意:抽象类提供了防止其它代码直接实例化对象的方法,与其继承类相对。

        注意不需要实现SpreadsheetCell类,所有的成员函数都是干净的virtual,析构函数显式缺省。