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

簡述:javascript

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

其實,dart類型系統不夠嚴格,這一點不只僅體如今可選類型上和尚未劃分可空與非空類型上,甚至還體現dart中的泛型類型安全上,這一點我會經過對比Kotlin和Dart中泛型實現。你會發現Dart和Kotlin泛型安全徹底走不是一個路子,並且dart泛型安全是不可靠的,可是也會發現dart2.0以後對這塊作很大的改進。git

1、可選類型

在Dart中的類型其實是可選的,也就是在Dart中函數類型,參數類型,變量類型是能夠直接省略的。github

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推出了命名構造器的概念。那可選類型語法特性爲何會和函數重載特性衝突呢?app

咱們可使用反證法,假設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爲例:ide

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爲何不支持函數重載呢? 其實,不是沒有能力支持,而是沒有必要的。其實在不少的現代語言好比GO,Rust中的都是沒有函數重載。Kotlin中也推薦使用默認值參數替代函數重載,感興趣的能夠查看我以前的一篇文章juejin.im/post/5ac0da…。然而在dart中函數也是支持默認值參數的,其實函數重載更容易讓人困惑,就好比Java中的Thread類中7,8個構造函數重載放在一塊兒,讓人就感到困惑。具體參考這個討論: groups.google.com/a/dartlang.…

2、接口類型

在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方法
}
複製代碼

3、泛型

一、泛型的基本介紹

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};
}
複製代碼

二、泛型的使用

  • 類泛型的使用

    //定義類的泛型很簡單,只須要在類名後加: <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) {
     //...
     }
    }
    複製代碼

三、子類、子類型和子類型化關係

  • 泛型類與非泛型類

咱們能夠把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類型的超類型。能夠明顯看出子類型的規則會比子類規則更爲寬鬆。那麼咱們能夠一塊兒分析下面幾個例子:

注意: 某個類型也是它本身自己的子類型,很明顯String類型的值任意出現地方,String確定都是能夠替換的。屬於子類關係的通常也是子類型關係。像double類型值確定不能替代int類型值出現的地方,因此它們不存在子類型關係.

  • 子類型化關係:

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

四、協變(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>子類型
}
複製代碼

五、協變在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 自己就是其子類型
}
複製代碼

六、爲何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)
}
複製代碼

4、類型具體化

一、類型檢測

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

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

二、強制類型轉化

強制類型轉換在Dart中通常使用 as 關鍵字,這一點也和Kotlin中是一致的。強制類型轉換是對一個表達式的值轉化目標類型,若是轉化失敗就會拋出CastError異常。

Object o = [1, 2, 3];
o as List;
o as Map;//拋出異常
複製代碼

5、總結

到這裏咱們就把Dart中的類型系統和泛型介紹完畢了,相信這篇文章將會使你對Dart中的類型系統有一個更全面的認識。其實經過Dart中泛型,就會發現Dart2.x真的優化不少東西,好比泛型安全的問題,雖然靜態檢查能經過可是運行沒法經過,換作Dart1.x運行也是能夠經過的。Dart2.x將會愈來愈嚴謹愈來愈完善,說明Dart在改變這是一件好事,一塊兒期待它的更多特性。下一篇咱們將進入Dart中更爲核心的部分異步編程系列,感謝關注~~~.

個人公衆號

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

Dart系列文章,歡迎查看:

相關文章
相關標籤/搜索