跳到主要內容

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 子類中,您可以將 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 收緊為 MouseMouseAnimal 的子類。

✗ 靜態分析:失敗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 列表用作型別化列表。

此規則也適用於泛型型別的例項。

以下程式碼建立一個 dynamicDog 列表,並將其賦值給一個型別為 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>

Map 字面量從其條目推斷其型別,然後變數從 Map 字面量的型別推斷其型別。在此 Map 中,鍵都是字串,但值具有不同的型別(Stringint,它們的上界是 Object)。因此,Map 字面量的型別是 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-bounded 型別尤為重要,使用邊界進行推斷在此類情況下能正確推斷出,在下面的示例中,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();

在消費位置,用消費任何型別 (Animal) 的東西替換消費特定型別 (Cat) 的東西是安全的,因此允許將 Cat c 替換為 Animal 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 方法),可以將返回型別替換為子型別。

更多資訊請參閱重寫方法時使用健全的返回型別重寫方法時使用健全的引數型別

協變引數

#

一些(很少使用的)編碼模式依賴於透過將引數型別重寫為子型別來收緊型別,這是無效的。在這種情況下,您可以使用 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 的更多資訊