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 关键字功能的区别。只有理解 继承 的局限性,才能认清 接口 存在的必要性。
以上就是Flutter 语法进阶抽象类和接口本质区别详解的详细内容,更多关于Flutter 语法抽象类接口的资料请关注脚本之家其它相关文章!