目錄

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 是編譯時錯誤。如果物件不是 String,則使用 as 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 子類別中,您可以使用 HoneyBadger(或 Animal 的任何其他子類型)來取代 getter 的傳回類型,但不允許使用無關的類型。

✔ 靜態分析:成功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());

在最後一個範例中,使用向下資訊推斷 xdouble。使用向上資訊推斷閉包的回傳型別為 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();

在消費位置,用可以消費任何東西 (Animal) 的東西替換消費特定型別 (Cat) 的東西是安全的,因此允許用 Animal c 替換 Cat c,因為 AnimalCat 的超型別。

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

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

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

在生產位置,用更具體的型別 (MaineCoon) 替換產生型別 (Cat) 的東西是安全的。因此,允許以下操作

✔ 靜態分析:成功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 的更多資訊