抽象类和接口

为了探索抽象类和接口,我们回到端到端绘图程序示例。为了简单起见,它的 API 包含一个虚方法 draw。到目前为止,它一直是空的,但与此同时,即使是这样的空实现也是一个具体的实现。但是,Shape 类的对象无法绘制,它们的形状尚未定义。因此,将 draw 方法设为抽象方法(或者说纯虚方法)是合理的。

为此,应移除带有空实现的代码块,并在方法头中添加 "= 0":

class Shape
{
public:
   virtual void draw() = 0;
   ...

至少有一个抽象方法的类也会变成抽象的,因为它的对象无法创建:没有实现。具体来说,我们的 Shape 构造函数可供派生类使用(这要归功于 protected 修饰符),并且它们的开发人员理论上可以创建一个 Shape 对象。但这是在声明抽象方法之前,声明抽象方法之后我们都停止了这种行为,因为我们(绘图接口的作者)禁止这样做。编译器会抛出错误:

'Shape' -cannot instantiate abstract class
      'void Shape::draw()' is abstract

要描述接口,最佳方法是为接口创建一个抽象类,其中仅包含抽象方法。在我们的例子中,draw 方法应该移到新类 Drawable 中,并且 Shape 类应该从其继承 (Shapes.mq5)。

class Drawable
{
public:
   virtual void draw() = 0;
};
 
class Shape : public Drawable
{
public:
   ...
   // virtual void draw() = 0; // moved to base class
   ...
};

当然,接口方法必须位于 public 段中。

MQL5 提供了另一种便捷的接口描述方式,即使用 interface 关键字。接口中的所有方法虽已声明但并未实现,被视为公共和虚拟。与上述类等效的 Drawable 接口的说明如下:

interface Drawable
{
   void draw();
};

在本例中,如果抽象类中没有任何字段(这将违反抽象原则),则其后代类无需进行任何更改。

现在需要扩展接口,并将 setColormoveXmoveY 这三个方法也添加到接口中。

interface Drawable
{
   void draw();
   Drawable *setColor(const color c);
   Drawable *moveX(const int x);
   Drawable *moveY(const int y);
};

请注意,这些方法返回一个 Drawable 对象,因为我对 Shape 一无所知。在 Shape 类中,我们已经有了适合重写这些方法的实现,因为 Shape 继承自 DrawableShape 在某种程度上也算是一种 Drawable 对象)。

现在,第三方开发者可以将其他类型的 Drawable 类添加到绘图程序中,具体来说,不仅包括形状,还包括文本、位图,甚至还能包括其他 Drawable 的集合(惊喜吧!),这样您便可以将对象嵌套在一起,实现复杂的组合。只需从接口中集成并实现其方法即可。

class Text : public Drawable
{
public:
   Text(const string label)
   {
      ...
   }
   
   void draw()
   {
      ...
   }
   
   Text *setColor(const color c)
   {
      ...
      return &this;
   }
   ...
};

如果形状类以二进制 ex5 库(不包含源代码)的形式分发,我们将为其提供一个头文件,其中仅包含接口的说明,不包含任何内部数据结构的提示。

由于虚函数在程序执行期间会动态(稍后)绑定到对象,因此可能会出现“纯虚函数调用”致命错误:程序终止。如果程序员无意中“忘记”提供实现,就会发生这种情况。编译器不一定总能在编译时检测到此类遗漏。