跳到主要內容

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 編譯在沒有類型的情況下也是可能的,但產生的程式碼效率會低得多。

通過靜態分析的提示

#

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

  • 覆寫方法時使用健全的傳回類型。
  • 覆寫方法時使用健全的參數類型。
  • 不要將 dynamic 列表用作類型列表。

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

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 列表會很好用。但是,您不能將 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 的隱含向下轉型

#

靜態類型為 dynamic 的表達式可以隱含地轉換為更特定的類型。如果實際類型不符,則轉換會在執行階段擲回錯誤。請考慮以下 assumeString 方法

✔ 靜態分析:成功dart
int assumeString(dynamic object) {
  String string = object; // Check at run time that `object` is a `String`.
  return string.length;
}

在此範例中,如果 objectString,則轉換會成功。如果它不是 String 的子類型,例如 int,則會擲回 TypeError

✗ 執行階段:失敗dart
final length = assumeString(1);

類型推論

#

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

以下是如何使用泛型進行類型推論的範例。在此範例中,名為 arguments 的變數保存將字串鍵與各種類型的值配對的地圖。

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

dart
Map<String, Object?> 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>

使用界限進行推論

#

透過使用界限進行推論功能,Dart 的類型推論演算法會透過將現有約束與宣告的類型界限結合來產生約束,而不僅僅是盡力而為的近似值。

這對於 F 綁定類型尤其重要,其中使用界限進行推論可以正確推論出在以下範例中,X 可以綁定到 B。如果沒有此功能,則必須明確指定類型引數:f<B>(C())

dart
class A<X extends A<X>> {}

class B extends A<B> {}

class C extends B {}

void f<X extends A<X>>(X x) {}

void main() {
  f(B()); // OK.

  // OK. Without using bounds, inference relying on best-effort approximations
  // would fail after detecting that `C` is not a subtype of `A<C>`.
  f(C());

  f<B>(C()); // OK.
}

以下是使用 Dart 中常見類型 (例如 intnum) 的更實際範例

dart
X max<X extends Comparable<X>>(X x1, X x2) => x1.compareTo(x2) > 0 ? x1 : x2;

void main() {
  // Inferred as `max<num>(3, 7)` with the feature, fails without it.
  max(3, 7);
}

透過使用界限進行推論,Dart 可以解構類型引數,從泛型類型參數的界限中擷取類型資訊。這允許以下範例中的 f 等函數保留特定的可迭代類型 (ListSet) 元素類型。在使用界限進行推論之前,如果不損失類型安全或特定類型資訊,這是不可能的。

dart
(X, Y) f<X extends Iterable<Y>, Y>(X x) => (x, x.first);

void main() {
  var (myList, myInt) = f([1]);
  myInt.whatever; // Compile-time error, `myInt` has type `int`.

  var (mySet, myString) = f({'Hello!'});
  mySet.union({}); // Works, `mySet` has type `Set<String>`.
}

如果不使用界限進行推論,myInt 將具有 dynamic 類型。先前的推論演算法不會在編譯時期捕獲不正確的表達式 myInt.whatever,而會在執行階段擲回錯誤。相反地,如果不使用界限進行推論,mySet.union({}) 將會是編譯時期錯誤,因為先前的演算法無法保留 mySetSet 的資訊。

如需有關使用界限進行推論演算法的詳細資訊,請參閱設計文件

替換類型

#

當您覆寫方法時,您會將一種類型 (在舊方法中) 的事物替換為可能具有新類型 (在新方法中) 的事物。同樣地,當您將引數傳遞給函數時,您會將具有一種類型 (具有宣告類型的參數) 的事物替換為具有另一種類型 (實際引數) 的事物。何時可以將具有一種類型的事物替換為具有子類型或超類型的事物?

在替換類型時,以消費者生產者的角度思考會有所幫助。消費者吸收類型,而生產者產生類型。

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

讓我們看看簡單類型指派和使用泛型類型指派的範例。

簡單類型指派

#

將物件指派給物件時,何時可以將類型替換為不同的類型?答案取決於物件是消費者還是生產者。

請考慮以下類型階層

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 方法),您可以將傳回類型替換為子類型。

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

協變參數

#

某些 (很少使用的) 程式碼模式依賴於透過使用子類型覆寫參數的類型來收緊類型,這是無效的。在這種情況下,您可以使用 covariant 關鍵字來告知分析器您是有意這樣做的。這會移除靜態錯誤,而是在執行階段檢查無效的引數類型。

以下顯示您可能如何使用 covariant

✔ 靜態分析:成功dart
class Animal {
  void chase(Animal x) {
     ...
  }
}

class Mouse extends Animal {
   ...
}

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

雖然此範例顯示在子類型中使用 covariant,但 covariant 關鍵字可以放在超類別或子類別方法中。通常超類別方法是放置它的最佳位置。covariant 關鍵字適用於單一參數,並且也支援 setter 和欄位。

其他資源

#

以下資源提供有關健全 Dart 的更多資訊