Flutter 语法进阶抽象类和接口本质区别详解

1. 接口存在的意义?

在 Dart 中 接口 定义并没有对应的关键字。可能有些人觉得 Dart 中弱化了 接口 的概念,其实不然。我们一般对接口的理解是:接口是更高级别的抽象,接口中的方法都是 抽象方法 ,没有方法体。通过接口的定义,我们可以通过定义接口来声明功能,通过实现接口来确保某类拥有这些功能。

不过你有没有仔细想过,为什么接口会存在,引入接口的概念是为了解决什么问题?可能有人会说,通过接口,可以规范一类事物的功能,可以面向接口进行操作,从而可以更加灵活地进行拓展。其实这只是接口的作用,而且这些功能 抽象类 也可以支持。所以接口一定存在什么特殊的功能,是抽象类无法做到的。

都是抽象方法的抽象类,和接口有什么本质的区别呢?在我的初入编程时,这个问题就伴随着我,但渐渐地,这个问题好像对编程没有什么影响,也就被遗忘了。网上很多文章介绍 抽象类 和 接口 的区别,只是在说些无关痛痒的形式区别,并不能让我觉得接口存在有什么必要性。

思考一件事物存在的本质意义,可以从没有这个事物会产生什么后果来分析。现在想一下,如果没有接口,一切的抽象行为仅靠 抽象类 完成会有什么局限性 或说 弊端。没有接口,就没有 实现 (implements) 的概念,其实这就等价于在问 implements 消失了,对编程有什么影响。没有实现,类之间就只能通过 继承 (extends) 来维护 is-a 的关系。所以就等价于在问 extends 有什么局限性 或说 弊端。答案呼之欲出:多继承的二义性 。

那问题来了,为什么类不能支持 多继承 ,而接口可以支持 多实现 ,继承 和 实现 有什么本质的区别呢?为什么 实现 不会带来 二义性 的问题,这是理解接口存在关键。

2. 继承 VS 实现

下面我们来探讨一下 继承 和 实现 的本质区别。如下 A 和 B 类,有一个相同的成员变量和成员方法:

class A{
 String name;
 A(this.name);
 void run(){ print("B"); }
}
class B{
 String name;
 B(this.name);
 void run(){ print("B"); }
}

对于继承而言 派生类 会拥有基类的成员变量与成员方法,如果支持多继承,就会出现两个问题:

  • 问题一 : 基类中有同名 成员变量 ,无法确定成员的归属类
  • 问题二: 基类中有同名 成员方法 ,且子类未覆写。在调用时,无法确定执行哪个。
class C extends A , B {
 C(String name) : super(name); // 如果多继承,该为哪个基类的 name 成员赋值 ??
}
void main(){
 C c = C("hello")
 c.run(); // 如果多继承,该执行哪个基类的 run 方法 ??
}

其实仔细思考一下,一般意义上的接口之所以能够 多实现 ,就是通过限制,对这两个问题进行解决。比如 Java 中:

  • 不允许在接口中定义普通的 成员变量 ,解决问题一。
  • 在接口中只定义抽象成员方法,不进行实现。而是强制派生类进行实现,解决问题二。
abstract class A{
 void run();
}
abstract class B{
 void run();
}
class C implements A,B{
 @override
 void run() {
 print("C");
 }
}

到这里,我们就认识到了为什么接口不存在 多实现 的二义性问题。这就是 继承 和 实现 最本质的区别,也是 抽象类 和 接口 最重要的差异。从这里可以看出,接口就是为了解决多继承二义性的问题,而引入的概念,这就是它存在的意义。

3. Dart 中接口与实现的特殊性

Dart 中并不像 Java 那样,有明确的关键字作为 接口类 的标识。因为 Dart 中的接口概念不再是 传统意义 上的狭义接口。而是 Dart 中的任何类都可以作为接口,包括普通的类,这也是为什么 Dart 不提供关键字来表示接口的原因。

既然普通类可以作为接口,那多实现中的 二义性问题 是必须要解决的,Dart 中是如何处理的呢? 如下是 A 、B 两个普通类,其中有两个同名 run 方法:

class A{
 void run(){
 print("run in a");
 }
}
class B{
 void run(){
 print("run in a");
 }
 void log(){
 print("log in a");
 }
}

当 C 类实现 A 、B 接口,必须强制覆写 所有 成员方法 ,这点解决了二义性的 问题二 :

那 问题一 中的 成员变量 的歧义如何解决呢?如下,在 A 、B 中添加同名的成员变量:

class A{
 final String name;
 A(this.name);
 // 略同...
}
class B{
 final String name;
 B(this.name);
 // 略同...
}

当 C 类实现 A 、B 接口,必须强制覆为 所有 成员变量提供 get 方法 ,这点解决了二义性的 问题一 :

这样,C 就可以实现两个普通类,而避免了二义性问题:

class C implements A, B {
 @override
 String get name => "C";
 @override
 void log() {}
 @override
 void run() {}
}

其实,这是 Dart 对 implements 关键字的功能加强,迫使派生类必须提供 所有 成员变量的 get 方法,必须覆写 所有 成员方法。这样就可以让 类 和 接口 成为两个独立的概念,一个 class 既可以是类,也可以是接口,具有双重身份。

其区别在于,在 extend 关键字后,表示继承,是作为类来对待;

在 implements 关键字之后,表示实现,是作为接口来对待。

4.Dart 中抽象类作为接口的小细节

我们知道,抽象类中允许定义 普通成员变量/方法 。下面举个小例子说明一下 继承 extend 和 实现 implements 的区别。对于继承来说,派生类只需要实现抽象方法即可,抽象基类 中的普通成员方法可以不覆写:

而前面说过,implements 关键字要求派生类必须覆写 接口 中的 所有 方法 。也就表示下面的 C implements A 时,也必须覆写 log 方法。从这个例子中,可以很清楚地看出 继承 和 实现 的差异性。

抽象类 和 接口 的区别,就是 继承 和 实现 的区别,在代码上的体现是 extend 和 implements 关键字功能的区别。只有理解 继承 的局限性,才能认清 接口 存在的必要性。

作者:张风捷特烈

%s 个评论

要回复文章请先登录注册