Flutter 语法进阶 | 抽象类和接口本质的区别

2022-09-20 10:26:02 浏览数 (1)

1. 接口存在的意义?

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

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

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

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

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


2. 继承 VS 实现

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

代码语言:javascript复制
class A{
  String name;
  
  A(this.name);

  void run(){  print("B"); }
}

class B{
  String name;

  B(this.name);

  void run(){ print("B"); }
}

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

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

void main(){
  C c = C("hello")
  c.run(); // 如果多继承,该执行哪个基类的 run 方法 ??
}

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

  • 不允许在接口中定义普通的 成员变量 ,解决问题一。
  • 在接口中只定义抽象成员方法,不进行实现。而是强制派生类进行实现,解决问题二。
代码语言:javascript复制
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 中是如何处理的呢? 如下是 AB 两个普通类,其中有两个同名 run 方法:

代码语言:javascript复制
class A{
  void run(){
    print("run in a");
  }
}

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

  void log(){
    print("log in a");
  }
}

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


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

代码语言:javascript复制
class A{
  final String name;
  A(this.name);
  // 略同...
}

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

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

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

代码语言:javascript复制
class C implements A, B {
  @override
  String get name => "C";

  @override
  void log() {}

  @override
  void run() {}
}

其实,这是 Dartimplements 关键字的功能加强,迫使派生类必须提供 所有 成员变量的 get 方法,必须覆写 所有 成员方法。这样就可以让 接口 成为两个独立的概念,一个 class 既可以是类,也可以是接口,具有双重身份。其区别在于,在 extend 关键字后,表示继承,是作为类来对待;在 implements 关键字之后,表示实现,是作为接口来对待。


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

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


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

抽象类接口 的区别,就是 继承实现 的区别,在代码上的体现是 extendimplements 关键字功能的区别。只有理解 继承 的局限性,才能认清 接口 存在的必要性。那本文就到这了,谢谢观看 ~

0 人点赞