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 語法抽象類接口的資料請關註WalkonNet其它相關文章!

推薦閱讀: