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
关键字功能的区别。只有理解 继承
的局限性,才能认清 接口
存在的必要性。那本文就到这了,谢谢观看 ~
作者:张风捷特烈
链接:https://juejin.cn/post/7131880904154644516
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。