Dart語法篇之型別系統與泛型(七)

mikyou發表於2019-11-25

簡述:

下面開始Dart語法篇的第七篇型別系統和泛型,上一篇我們用了一篇Dart中可空和非空型別譯文做了鋪墊。實際上,Dart中的型別系統是不夠嚴格,這當然和它的歷史原因有關。在dart最開始誕生之初,它的定位是一門像javascript一樣的動態語言,動態語言的型別系統是比較鬆散的,所以在Dart型別也是可選的。然後動態語言型別系統鬆散對開發者並不是一件好事,程式邏輯一旦複雜,鬆散的型別可能就變得混亂,分析起來非常痛苦,但是有靜態型別檢查可以在編譯的時候就快速定位問題所在。

其實,dart型別系統不夠嚴格,這一點不僅僅體現在可選型別上和還沒有劃分可空與非空型別上,甚至還體現dart中的泛型型別安全上,這一點我會通過對比Kotlin和Dart中泛型實現。你會發現Dart和Kotlin泛型安全完全走不是一個路子,而且dart泛型安全是不可靠的,但是也會發現dart2.0之後對這塊做很大的改進。

一、可選型別

在Dart中的型別實際上是可選的,也就是在Dart中函式型別,引數型別,變數型別是可以直接省略的。

sum(a, b, c, d) {//函式引數型別和返回值型別可以省略
  return a + b + c + d;
}

main() {
  print('${sum(10, 12, 14, 12)}');//正常執行
}
複製程式碼

上述的sum函式既沒有返回值型別也沒有引數型別,可能有的人會疑惑如果sum函式最後一個形參傳入一個String型別會是怎麼樣。

答案是: 靜態型別檢查分析正常但是編譯執行異常。

sum(a, b, c, d) {
  return a + b + c + d;
}

main() {
  print('${sum(10, 12, 14, "12312")}');//靜態檢查型別檢查正常,執行異常
}

//執行結果
Unhandled exception:
type 'String' is not a subtype of type 'num' of 'other' //請先記住這個子型別不匹配異常問題,因為在後面會詳細分析子型別的含義,而且Dart、Flutter開發中會經常看到這個異常。

Process finished with exit code 255
複製程式碼

雖然,可選型別從一方面使得整個程式碼變得簡潔以及具有動態性,但是從另一方面它會使得靜態檢查型別難以分析。但是這也使得dart中失去了基於型別函式過載特性。我們都知道函式過載是靜態語言中比較常見的語法特性,可是在dart中是不支援的。比如在其他語言我們一般使用構造器過載解決多種方式構造物件的場景,但是dart不支援構造器過載,所以為了解決這個問題,Dart推出了命名構造器的概念。那可選型別語法特性為什麼會和函式過載特性衝突呢?

我們可以使用反證法,假設dart支援函式過載,那麼可能就會有以下這段程式碼:

class IllegalCode {
  overloaded(num data) {

  }
  overloaded(List data){//假設支援函式過載,實際上這是非法的

  }
}

main() {
    var data1 = 100; 
    var data2 = ["100"];
    //由於dart中的型別是可選的,以下函式呼叫,根本就無法分辨下面程式碼實際上呼叫哪個overloaded函式。
    overloaded(data1);
    overloaded(data2);
}
複製程式碼

個人一些想法,如果僅從可選型別角度去考慮的話,實際上dart現在是可以支援基於型別的函式過載的,因為Dart有型別推導功能。如果dart能夠推匯出上述data1和data2型別,那麼就可以根據推匯出的型別去匹配過載的函式。Kotlin就是這樣做的,以Kotlin為例:

fun overloaded(data: Int) {
    //....
}

fun overloaded(data: List<String>) {
   //....
}

fun main(args: Array<String>) {
    val data1 = 100 //這裡Kotlin也是採用型別推導為Int
    val data2 = listOf("100")//這裡Kotlin也是採用型別推導為List<String>
    //所以以下過載函式的呼叫在Kotlin中是合理的
    overloaded(data1)
    overloaded(data2)
}
複製程式碼

實際上,Dart官方在Github提到過Dart遷移到新的型別系統中,Dart是有能力支援函式過載的 。具體可以參考這個dartlang的issue: github.com/dart-lang/s…

Dart語法篇之型別系統與泛型(七)

但是,dart為什麼不支援函式過載呢? 其實,不是沒有能力支援,而是沒有必要的。其實在很多的現代語言比如GO,Rust中的都是沒有函式過載。Kotlin中也推薦使用預設值引數替代函式過載,感興趣的可以檢視我之前的一篇文章juejin.im/post/5ac0da…。然而在dart中函式也是支援預設值引數的,其實函式過載更容易讓人困惑,就比如Java中的Thread類中7,8個建構函式過載放在一起,讓人就感到困惑。具體參考這個討論: groups.google.com/a/dartlang.…

Dart語法篇之型別系統與泛型(七)

二、介面型別

在Dart中沒有直接顯示宣告介面的方法,沒有類似interface的關鍵字來宣告介面,而是隱性地通過類宣告引入。所以每個類都存在一個對應名稱隱性的介面,dart中的型別也就是介面型別。

//定義一個抽象類Person,同時它也是一個隱性的Person介面
abstract class Person {
  final String name;
  final int age;

  Person(this.name, this.age);

  get description => "My name is $name, age is $age";
}

//定義一個Student類,使用implements關鍵字實現Person介面
class Student implements Person {
  @override
  // TODO: implement age
  int get age => null;//重寫age getter函式,由於在Person介面中是final修飾,所以它只有getter訪問器函式,作為介面實現就是需要重寫它所有的函式,包括它的getter或setter方法。

  @override
  // TODO: implement description
  get description => null;//重寫定義description方法

  @override
  // TODO: implement name
  String get name => null;//重寫name getter函式,由於在Person介面中是final修飾,所以它只有getter訪問器函式,作為介面實現就是需要重寫它所有的函式,包括它的getter或setter方法。
}

//定義一個Student2類,使用extends關鍵字繼承Person抽象類
class Student2 extends Person {
  Student2(String name, int age) : super(name, age);//呼叫父類中的建構函式

  @override
  get description => "Student: ${super.description}";//重寫父類中的description方法
}
複製程式碼

三、泛型

1、泛型的基本介紹

Dart中的泛型和其他語言差不多,但是Dart中的型別是可選的,使用泛型可以限定型別;使用泛型可以減少很多模板程式碼。

一起來看個例子:

//這是一個列印int型別msg的PrintMsg
class PrintMsg {
  int _msg;

  set msg(int msg) {
    this._msg = msg;
  }

  void printMsg() {
    print(_msg);
  }
}

//現在又需要支援String,double甚至其他自定義類的Msg,我們可能這麼加
class Msg {
  @override
  String toString() {
    return "This is Msg";
  }
}

class PrintMsg {
  int _intMsg;
  String _stringMsg;
  double _doubleMsg;
  Msg _msg;

  set intMsg(int msg) {
    this._intMsg = msg;
  }

  set stringMsg(String msg) {
    this._stringMsg = msg;
  }

  set doubleMsg(double msg) {
    this._doubleMsg = msg;
  }

  set msg(Msg msg) {
    this._msg = msg;
  }

  void printIntMsg() {
    print(_intMsg);
  }

  void printStringMsg() {
    print(_stringMsg);
  }

  void printDoubleMsg() {
    print(_doubleMsg);
  }

  void printMsg() {
    print(_msg);
  }
}

//但是有了泛型以後,我們可以把上述程式碼簡化很多:
class PrintMsg<T> {
  T _msg;

  set msg(T msg) {
    this._msg = msg;
  }
  
  void printMsg() {
    print(_msg);
  }
}
複製程式碼

補充一點Dart中可以指定實際的泛型引數型別,也可以省略。省略實際上就相當於指定了泛型引數型別為dynamic型別。

class Test {
  List<int> nums = [1, 2, 3, 4];
  Map<String, int> maps = {'a': 1, 'b': 2, 'c': 3, 'd': 4};

//上述定義可簡寫成如下形式,但是不太建議使用這種形式,僅在必要且適當的時候使用
  List nums = [1, 2, 3, 4];
  Map maps = {'a': 1, 'b': 2, 'c': 3, 'd': 4};

//上述定義相當於如下形式
  List<dynamic> nums = [1, 2, 3, 4];
  Map<dynamic, dynamic> maps = {'a': 1, 'b': 2, 'c': 3, 'd': 4};
}
複製程式碼

2、泛型的使用

  • 類泛型的使用

    //定義類的泛型很簡單,只需要在類名後加: <T>;如果需要多個泛型型別引數,可以在尖括號中追加,用逗號分隔
    class List<T> {
      T element;
    
      void add(T element) {
        //...
      }
    }
    複製程式碼
  • 函式泛型的使用

    //定義函式的泛型
    void add(T elememt) {//函式引數型別為泛型型別
        //...
    }
    
    T elementAt(int index) {//函式引數返回值型別為泛型型別
        //...
    }
    
    E transform(R data) {//函式引數型別和函式引數返回值型別均為泛型型別
       //... 
    }
    複製程式碼
  • 集合泛型的使用

    var list = <int> [1, 2, 3];
    //相當於如下形式
    List<int> list = [1, 2, 3];
    
    var map = <String, int>{'a':1, 'b':2, 'c':3};
    //相當於如下形式
    Map<String, int> map = {'a':1, 'b':2, 'c':3};
    複製程式碼
  • 泛型的上界限定

    //和Java一樣泛型上界限定可以使用extends關鍵字來實現
    class List<T extends num> {
     T element;
     void add(T element) {
     //...
     }
    }
    複製程式碼

3、子類、子型別和子型別化關係

  • 泛型類與非泛型類

我們可以把Dart中的類可分為兩大類: 泛型類非泛型類

先說非泛型類也就是開發中接觸最多的一般類,一般的類去定義一個變數的時候,它的實際就是這個變數的型別. 例如定義一個Student類,我們會得到一個Student型別

泛型類比非泛型類要更加複雜,實際上一個泛型類可以對應無限種型別。為什麼這麼說,其實很容易理解。我們從前面文章知道,在定義泛型類的時候會定義泛型形參,要想拿到一個合法的泛型型別就需要在外部使用地方傳入具體的型別實參替換定義中的型別形參。我們知道在Dart中List是一個類,它不是一個型別。由它可以衍生成無限種泛型型別。例如List<String>、List<int>、List<List<num>>、List<Map<String,int>>

  • 何為子型別

我們可能會經常在Flutter開發中遇到subtype子型別的錯誤: type 'String' is not a subtype of type 'num' of 'other'. 到底啥是子型別呢? 它和子類是一個概念嗎?

首先給出一個數學歸納公式:

如果G是一個有n個型別引數的泛型類,而A[i]是B[i]的子型別且屬於 1..n的範圍,那麼可表示為G<A[1],...,A[n]> * G<B[1],...,B[n]>的子型別,其中 A * B 可表示A是B的子型別。

上述是不是很抽象,其實Dart中的子型別概念和Kotlin中子型別概念極其相似。

我們一般說子類就是派生類,該類一般會繼承它的父類(也叫基類)。例如: class Student extends Person{//...},這裡的Student一般稱為Person的子類

子型別則不一樣,我們從上面就知道一個類可以有很多型別,那麼子型別不僅僅是想子類那樣繼承關係那麼嚴格。子型別定義的規則一般是這樣的: 任何時候如果需要的是A型別值的任何地方,都可以使用B型別的值來替換的,那麼就可以說B型別是A型別的子型別或者稱A型別是B型別的超型別。可以明顯看出子型別的規則會比子類規則更為寬鬆。那麼我們可以一起分析下面幾個例子:

Dart語法篇之型別系統與泛型(七)

注意: 某個型別也是它自己本身的子型別,很明顯String型別的值任意出現地方,String肯定都是可以替換的。屬於子類關係的一般也是子型別關係。像double型別值肯定不能替代int型別值出現的地方,所以它們不存在子型別關係.

  • 子型別化關係:

如果A型別的值在任何時候任何地方出現都能被B型別的值替換,B型別就是A型別的子型別,那麼B型別到A型別之間這種對映替換關係就是子型別化關係

4、協變(covariant)

一提到協變,可能我們還會對應另外一個詞那就是逆變,實際上在Dart1.x的版本中是既支援協變又支援逆變的,但是在Dart2.x版本僅僅支援協變的。有了子型別化關係的概念,那麼協變就更好理解了,協變實際上就是保留子型別化關係,首先,我們需要去明確一下這裡所說的保留子型別化關係是針對誰而言的呢?

比如說intnum的子型別,因為在Dart中所有泛型類都預設是協變的,所以List<int>就是List<num>的子型別,這就是保留了子型別化關係,保留的是泛型引數(intnum)型別的子型別化關係.

一起看個例子:

class Fruit {
  final String color;

  Fruit(this.color);
}

class Apple extends Fruit {
  Apple() : super("red");
}

class Orange extends Fruit {
  Orange() : super("orange");
}

void printColors(List<Fruit> fruits) {
  for (var fruit in fruits) {
    print('${fruit.color}');
  }
}

main() {
  List<Apple> apples = <Apple>[];
  apples.add(Apple());
  printColors(apples);//Apple是Fruit的子型別,所以List<Apple>是List<Fruit>子型別。
  // 所以printColors函式接收一個List<Fruit>型別,可以使用List<Apple>型別替代
  List<Orange> oranges = <Orange>[];
  oranges.add(Orange());
  printColors(oranges);//同理

  List<Fruit> fruits = <Fruit>[];
  fruits.add(Fruit('purple'));
  printColors(fruits);//Fruit本身也是Fruit的子型別,所以List<Fruit>肯定是List<Fruit>子型別
}
複製程式碼

5、協變在Dart中的應用

實際上,在Dart中協變預設用於泛型型別實際上還有用於另一種場景協變方法引數型別. 可能對專業術語有點懵逼,先通過一個例子來看下:

//定義動物基類
class Animal {
  final String color;

  Animal(this.color);
}

//定義Cat類繼承Animal
class Cat extends Animal {
  Cat() : super('black cat');
}

//定義Dog類繼承Animal
class Dog extends Animal {
  Dog() : super('white dog');
}

//定義一個裝動物的籠子類
class AnimalCage {
  void putAnimal(Animal animal) {
    print('putAnimal: ${animal.color}');
  }
}

//定義一個貓籠類
class CatCage extends AnimalCage {
  @override
  void putAnimal(Animal animal) {//注意: 這裡方法引數是Animal型別
    super.putAnimal(animal);
  }
}

//定義一個狗籠類
class DogCage extends AnimalCage {
    @override
    void putAnimal(Animal animal) {//注意: 這裡方法引數是Animal型別
      super.putAnimal(animal);
    }
}
複製程式碼

我們需要去重寫putAnimal方法,由於是繼承自AnimalCage類,所以方法引數型別是Animal.這會造成什麼問題呢? 一起來看下:

main() {
  //建立一個貓籠物件
  var catCage = CatCage();
  //然後卻可以把一條狗放進去,如果按照設計原理應該貓籠子只能put貓。
  catCage.putAnimal(Dog());//這行靜態檢查以及執行都可以通過。
  
  //建立一個狗籠物件
  var dogCage = DogCage();
  //然後卻可以把一條貓放進去,如果按照設計原理應該狗籠子只能put狗。
  dogCage.putAnimal(Cat());//這行靜態檢查以及執行都可以通過。
}
複製程式碼

其實對於上述的出現問題,我們更希望putAnimal的引數更具體些,為了解決上述問題你需要使用 covariant 協變關鍵字。

//定義一個貓籠類
class CatCage extends AnimalCage {
  @override
  void putAnimal(covariant Cat animal) {//注意: 這裡使用covariant協變關鍵字 表示CatCage物件中的putAnimal方法只接收Cat物件
    super.putAnimal(animal);
  }
}

//定義一個狗籠類
class DogCage extends AnimalCage {
    @override
    void putAnimal(covariant Dog animal) {//注意: 這裡使用covariant協變關鍵字 表示DogCage物件中的putAnimal方法只接收Dog物件
      super.putAnimal(animal);
    }
}
//呼叫
main() {
  //建立一個貓籠物件
  var catCage = CatCage();
  catCage.putAnimal(Dog());//這時候這樣呼叫就會報錯, 報錯資訊: Error: The argument type 'Dog' can't be assigned to the parameter type 'Cat'.
}
複製程式碼

為了進一步驗證結論,可以看下這個例子:

typedef void PutAnimal(Animal animal);

class TestFunction {
  void putCat(covariant Cat animal) {}//使用covariant協變關鍵字

  void putDog(Dog animal) {}

  void putAnimal(Animal animal) {}
}

main() {
  var function = TestFunction()
  print(function.putCat is PutAnimal);//true 因為使用協變關鍵字
  print(function.putDog is PutAnimal);//false
  print(function.putAnimal is PutAnimal);//true 本身就是其子型別
}
複製程式碼

6、為什麼Kotlin比Dart的泛型型變更安全

實際上Dart和Java一樣,泛型型變都存在安全問題。以及List集合為例,List在Dart中既是可變的,又是協變的,這樣就會存在安全問題。然而Kotlin卻不一樣,在Kotlin把集合分為可變集合MutableList<E>和只讀集合List<E>,其中List<E>在Kotlin中就是不可變的,協變的,這樣就不會存在安全問題。下面這個例子將對比Dart和Kotlin的實現:

  • Dart中的實現
class Fruit {
  final String color;

  Fruit(this.color);
}

class Apple extends Fruit {
  Apple() : super("red");
}

class Orange extends Fruit {
  Orange() : super("orange");
}

void printColors(List<Fruit> fruits) {//實際上這裡List是不安全的。
  for (var fruit in fruits) {
    print('${fruit.color}');
  }
}

main() {
  List<Apple> apples = <Apple>[];
  apples.add(Apple());
  printColors(apples);//printColors傳入是一個List<Apple>,因為是協變的
}
複製程式碼

為什麼說printColors函式中的List<Fruit>是不安全的呢,外部main函式中傳入的是一個List<Apple>.所以printColors函式中的fruits實際上是一個List<Apple>.可是printColors這樣改動呢?

void printColors(List<Fruit> fruits) {//實際上這裡List是不安全的。
  fruits.add(Orange());//靜態檢查都是通過的,Dart1.x版本中執行也是可以通過的,但是好在Dart2.x版本進行了優化,
  // 在2.x版本中執行是會報錯的:type 'Orange' is not a subtype of type 'Apple' of 'value'
  // 由於在Dart中List都是可變的,在fruits中新增Orange(),實際上是在List<Apple>中新增Orange物件,這裡就會出現安全問題。
  for (var fruit in fruits) {
    print('${fruit.color}');
  }
}
複製程式碼
  • Kotlin中的實現

然而在Kotlin中的不會存在上面那種問題,Kotlin對集合做了很細緻的劃分,分為可變與只讀。只讀且協變的泛型型別更具安全性。一起看下Kotlin怎麼做到的。

open class Fruit(val color: String)

class Apple : Fruit("red")

class Orange : Fruit("orange")

fun printColors(fruits: List<Fruit>) {
    fruits.add(Orange())//此處編譯不通過,因為在Kotlin中只讀集合List<E>,沒有add, remove之類修改集合的方法只有讀的方法,
    //所以它不會存在List<Apple>中還新增一個Orange的情況出現。
    for (fruit in fruits) {
        println(fruit.color)
    }
}

fun main() {
    val apples = listOf(Apple())
    printColors(apples)
}
複製程式碼

四、型別具體化

1、型別檢測

在Dart中一般使用 is 關鍵字做型別檢測,這一點和Kotlin中是一致的,如果判斷不是某個型別dart中使用is!, 而在Kotlin中正好相反則用 !is 表示。型別檢測就是對錶達式結果值的動態型別與目標型別做對比測試。

main() {
  var apples = [Apple()];
  print(apples is List<Apple>);
}
複製程式碼

2、強制型別轉化

強制型別轉換在Dart中一般使用 as 關鍵字,這一點也和Kotlin中是一致的。強制型別轉換是對一個表示式的值轉化目標型別,如果轉化失敗就會丟擲CastError異常。

Object o = [1, 2, 3];
o as List;
o as Map;//丟擲異常
複製程式碼

五、總結

到這裡我們就把Dart中的型別系統和泛型介紹完畢了,相信這篇文章將會使你對Dart中的型別系統有一個更全面的認識。其實通過Dart中泛型,就會發現Dart2.x真的優化很多東西,比如泛型安全的問題,雖然靜態檢查能通過但是執行無法通過,換做Dart1.x執行也是可以通過的。Dart2.x將會越來越嚴謹越來越完善,說明Dart在改變這是一件好事,一起期待它的更多特性。下一篇我們將進入Dart中更為核心的部分非同步程式設計系列,感謝關注~~~.

我的公眾號

這裡有最新的Dart、Flutter、Kotlin相關文章以及優秀國外文章翻譯,歡迎關注~~~

Dart語法篇之型別系統與泛型(七)

Dart系列文章,歡迎檢視:

相關文章