內容

Dart 型別系統

Dart 語言是類型安全的:它結合使用靜態類型檢查和 執行時期檢查,以確保變數的值始終與變數的靜態類型相符,有時稱為健全類型。儘管類型是強制性的,但由於 類型推論,類型註解是可選的。

靜態類型檢查的一個好處是能夠在編譯時使用 Dart 的 靜態分析器 找出錯誤。

您可以透過為泛型類別新增類型註解來修正大部分靜態分析錯誤。最常見的泛型類別是集合類型 List<T>Map<K,V>

例如,在以下程式碼中,printInts() 函式會列印整數清單,而 main() 會建立一個清單並將其傳遞給 printInts()

✗ 靜態分析:失敗dart
void printInts(List<int> a) => print(a);

void main() {
  final list = [];
  list.add(1);
  list.add('2');
  printInts(list);
}

前述程式碼會在 printInts(list) 的呼叫中產生 list 的類型錯誤(如上所述)

error - The argument type 'List<dynamic>' can't be assigned to the parameter type 'List<int>'. - argument_type_not_assignable

錯誤會強調從 List<dynamic>List<int> 的不健全隱式轉換。list 變數的靜態類型為 List<dynamic>。這是因為初始化宣告 var list = [] 沒有提供分析器足夠的資訊,讓它推論出比 dynamic 更具體的類型引數。printInts() 函式預期一個類型為 List<int> 的參數,導致類型不符。

在建立清單時新增類型註解 (<int>)(如下所述)時,分析器會抱怨無法將字串引數指定給 int 參數。移除 list.add('2') 中的引號會產生通過靜態分析且執行時沒有錯誤或警告的程式碼。

✔ 靜態分析:成功dart
void printInts(List<int> a) => print(a);

void main() {
  final list = <int>[];
  list.add(1);
  list.add(2);
  printInts(list);
}

在 DartPad 中嘗試.

什麼是健全性?

#

健全性是確保程式不會進入某些無效狀態。健全的類型系統表示你永遠不會進入表達式評估為與表達式的靜態類型不匹配的值的狀態。例如,如果表達式的靜態類型為 String,在執行階段,保證你在評估它時只會取得字串。

Dart 的類型系統,就像 Java 和 C# 中的類型系統,是健全的。它使用靜態檢查(編譯時期錯誤)和執行階段檢查的組合來強制執行健全性。例如,將 String 指定給 int 是編譯時期錯誤。使用 as String 將物件轉型為 String 會失敗,並產生執行階段錯誤(如果物件不是 String)。

健全性的優點

#

健全的類型系統有幾個好處

  • 在編譯時期揭露與類型相關的錯誤。
    健全的類型系統強制程式對其類型明確,因此在執行階段可能難以找到的與類型相關的錯誤會在編譯時期揭露。

  • 更易於閱讀的程式碼。
    程式碼更容易閱讀,因為你可以依賴值實際具有指定的類型。在健全的 Dart 中,類型不會說謊。

  • 更易於維護的程式碼。
    使用健全的類型系統,當你變更一段程式碼時,類型系統可以警告你剛中斷的其他程式碼片段。

  • 更好的預先編譯 (AOT)。
    雖然沒有類型也可以進行 AOT 編譯,但產生的程式碼效率低得多。

通過靜態分析的提示

#

大多數靜態類型的規則都很容易理解。以下是一些不太明顯的規則

  • 覆寫方法時,使用健全的傳回類型。
  • 覆寫方法時,使用健全的參數類型。
  • 不要將動態清單用作類型化清單。

讓我們詳細了解這些規則,並使用以下類型階層的範例

a hierarchy of animals where the supertype is Animal and the subtypes are Alligator, Cat, and HoneyBadger. Cat has the subtypes of Lion and MaineCoon

覆寫方法時使用健全的回傳類型

#

子類別中方法的傳回類型必須與超類別中方法的傳回類型相同或為其子類型。考慮 Animal 類別中的 getter 方法

dart
class Animal {
  void chase(Animal a) { ... }
  Animal get parent => ...
}

parent getter 方法傳回 Animal。在 HoneyBadger 子類別中,你可以將 getter 的傳回類型替換為 HoneyBadger(或 Animal 的任何其他子類型),但不允許不相關的類型。

✔ 靜態分析:成功dart
class HoneyBadger extends Animal {
  @override
  void chase(Animal a) { ... }

  @override
  HoneyBadger get parent => ...
}
✗ 靜態分析:失敗dart
class HoneyBadger extends Animal {
  @override
  void chase(Animal a) { ... }

  @override
  Root get parent => ...
}

覆寫方法時使用健全的參數類型

#

覆寫方法的參數必須與超類別中對應參數具有相同的類型或超類型。不要透過將類型替換為原始參數的子類型來「收緊」參數類型。

考慮 Animal 類別的 chase(Animal) 方法

dart
class Animal {
  void chase(Animal a) { ... }
  Animal get parent => ...
}

chase() 方法採用 AnimalHoneyBadger 追逐任何東西。覆寫 chase() 方法以採用任何東西 (Object) 是可以的。

✔ 靜態分析:成功dart
class HoneyBadger extends Animal {
  @override
  void chase(Object a) { ... }

  @override
  Animal get parent => ...
}

下列程式碼將 chase() 方法的參數從 Animal 縮小到 Mouse,這是 Animal 的子類別。

✗ 靜態分析:失敗dart
class Mouse extends Animal { ... }

class Cat extends Animal {
  @override
  void chase(Mouse a) { ... }
}

這段程式碼不是類型安全的,因為這樣就可以定義一隻貓並讓牠追逐一隻鱷魚

dart
Animal a = Cat();
a.chase(Alligator()); // Not type safe or feline safe.

不要將動態清單用作類型化清單

#

當您想要一個清單中包含不同種類的項目時,dynamic 清單很好用。但是,您不能將 dynamic 清單用作類型化清單。

此規則也適用於泛型類型的實例。

下列程式碼建立一個 Dogdynamic 清單,並將其指定給類型為 Cat 的清單,這會在靜態分析期間產生錯誤。

✗ 靜態分析:失敗dart
void main() {
  List<Cat> foo = <dynamic>[Dog()]; // Error
  List<dynamic> bar = <dynamic>[Dog(), Cat()]; // OK
}

執行時期檢查

#

執行時期檢查處理編譯時無法偵測的類型安全問題。

例如,下列程式碼在執行時期擲回例外,因為將狗清單轉換為貓清單是錯誤的

✗ 執行時期:失敗dart
void main() {
  List<Animal> animals = <Dog>[Dog()];
  List<Cat> cats = animals as List<Cat>;
}

類型推論

#

分析器可以推論欄位、方法、局部變數和大多數泛型類型引數的類型。當分析器沒有足夠的資訊來推論特定類型時,它會使用 dynamic 類型。

以下是類型推論如何與泛型一起運作的範例。在此範例中,名為 arguments 的變數包含一個將字串金鑰與各種類型的值配對的地圖。

如果您明確輸入變數,您可能會這樣寫

dart
Map<String, dynamic> arguments = {'argA': 'hello', 'argB': 42};

或者,您可以使用 varfinal 並讓 Dart 推論類型

dart
var arguments = {'argA': 'hello', 'argB': 42}; // Map<String, Object>

地圖文字從其條目中推論其類型,然後變數從地圖文字的類型中推論其類型。在此地圖中,金鑰都是字串,但值有不同的類型 (Stringint,它們的上限為 Object)。因此,地圖文字的類型為 Map<String, Object>arguments 變數也是如此。

欄位和方法推論

#

沒有指定類型且覆寫超類別欄位或方法的欄位或方法,會繼承超類別方法或欄位的類型。

沒有宣告或繼承類型,但宣告有初始值的欄位,會根據初始值取得推論的類型。

靜態欄位推論

#

靜態欄位和變數會從其初始化器推論其類型。請注意,如果遇到循環(也就是說,推論變數的類型取決於知道該變數的類型),推論會失敗。

局部變數推論

#

如果有的話,局部變數類型會從其初始化器推論。後續的指定不會考慮在內。這表示可能會推論出太精確的類型。如果是這樣,您可以新增類型註解。

✗ 靜態分析:失敗dart
var x = 3; // x is inferred as an int.
x = 4.0;
✔ 靜態分析:成功dart
num y = 3; // A num can be double or int.
y = 4.0;

類型引數推論

#

建構式呼叫和泛型方法呼叫的類型引數會根據發生環境的向下資訊和建構式或泛型方法引數的向上資訊組合推論。如果推論沒有依照您的希望或預期進行,您隨時都可以明確指定類型引數。

✔ 靜態分析:成功dart
// Inferred as if you wrote <int>[].
List<int> listOfInt = [];

// Inferred as if you wrote <double>[3.0].
var listOfDouble = [3.0];

// Inferred as Iterable<int>.
var ints = listOfDouble.map((x) => x.toInt());

在最後一個範例中,x會使用向下資訊推論為double。封閉的回傳類型會使用向上資訊推論為int。Dart在推論map()方法的類型引數時,會將此回傳類型用作向上資訊:<int>

替換類型

#

當您覆寫方法時,您會將一種類型(在舊方法中)的某個項目替換為可能具有新類型(在新方法中)的某個項目。類似地,當您將引數傳遞給函式時,您會將具有某種類型(具有宣告類型之參數)的某個項目替換為具有另一種類型(實際引數)的某個項目。什麼時候您可以將具有某種類型之某個項目替換為具有子類型或超類型的某個項目?

在替換類型時,有助於思考消費者生產者。消費者會吸收類型,而生產者會產生類型。

您可以將消費者的類型替換為超類型,將生產者的類型替換為子類型。

我們來看簡單類型指定和具有泛型類型的指定的範例。

簡單類型指定

#

在將物件指定給物件時,什麼時候您可以將類型替換為不同的類型?答案取決於物件是消費者還是生產者。

考量下列類型階層

a hierarchy of animals where the supertype is Animal and the subtypes are Alligator, Cat, and HoneyBadger. Cat has the subtypes of Lion and MaineCoon

考量下列簡單指定,其中Cat c消費者,而Cat()生產者

dart
Cat c = Cat();

在消費位置中,用消耗特定類型 (Cat) 的東西取代消耗任何東西 (Animal) 的東西是安全的,因此將 Cat c 替換為 Animal c 是允許的,因為 AnimalCat 的超類型。

✔ 靜態分析:成功dart
Animal c = Cat();

但將 Cat c 替換為 MaineCoon c 會破壞類型安全性,因為超類別可能會提供具有不同行為的 Cat 類型,例如 Lion

✗ 靜態分析:失敗dart
MaineCoon c = Cat();

在產生位置中,用產生類型 (Cat) 的東西取代更具體的類型 (MaineCoon) 是安全的。因此,下列情況是允許的

✔ 靜態分析:成功dart
Cat c = MaineCoon();

泛型類型指定

#

通用類型規則相同嗎?是的。考慮動物清單的層級—CatListAnimalList 的子類型,也是 MaineCoonList 的超類型

List<Animal> -> List<Cat> -> List<MaineCoon>

在下列範例中,您可以將 MaineCoon 清單指定給 myCats,因為 List<MaineCoon>List<Cat> 的子類型

✔ 靜態分析:成功dart
List<MaineCoon> myMaineCoons = ...
List<Cat> myCats = myMaineCoons;

反之亦然呢?您可以將 Animal 清單指定給 List<Cat> 嗎?

✗ 靜態分析:失敗dart
List<Animal> myAnimals = ...
List<Cat> myCats = myAnimals;

此指定不會通過靜態分析,因為它會建立一個隱式向下轉型,而這在非 dynamic 類型(例如 Animal)中是不允許的。

若要讓此類型的程式碼通過靜態分析,您可以使用明確轉型。

dart
List<Animal> myAnimals = ...
List<Cat> myCats = myAnimals as List<Cat>;

不過,明確轉型仍可能在執行階段失敗,具體取決於所轉型的清單 (myAnimals) 的實際類型。

方法

#

覆寫方法時,產生者和消費者規則仍然適用。例如

Animal class showing the chase method as the consumer and the parent getter as the producer

對於消費者(例如 chase(Animal) 方法),您可以將參數類型替換為超類型。對於產生者(例如 parent getter 方法),您可以將回傳類型替換為子類型。

如需詳細資訊,請參閱 覆寫方法時使用合理的回傳類型覆寫方法時使用合理的參數類型

其他資源

#

下列資源提供了有關合理 Dart 的更多資訊