The Dart type system

The Dart type system

Dart 语言是类型安全的:它结合了静态类型检查和运行时检查,以确保变量的值始终与其静态类型相匹配,这有时被称为类型健全性(sound typing)。尽管类型是强制性的,但由于有类型推断,类型注解是可选的。

静态类型检查的一个好处是能够使用 Dart 的静态分析器在编译时发现错误。

您可以通过为泛型类添加类型注解来修复大多数静态分析错误。最常见的泛型类是集合类型 List<T>Map<K,V>

例如,在以下代码中,printInts() 函数打印一个整数列表,而 main() 函数创建一个列表并将其传递给 printInts()

1
2
3
4
5
6
7
8
9
✗ 静态分析:失败
void printInts(List<int> a) => print(a);

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

上述代码在调用 printInts(list) 时,会对 list(如上高亮所示)产生一个类型错误:

1
2
error - The argument type 'List<dynamic>' can't be assigned to the parameter type 'List<int>'. - argument_type_not_assignable
错误 - 参数类型 'List<dynamic>' 不能赋值给参数类型 'List<int>'。 - argument_type_not_assignable

这个错误指出了一个从 List<dynamic>List<int> 的不健全的隐式转换。list 变量的静态类型是 List<dynamic>。这是因为初始化声明 var list = [] 没有为分析器提供足够的信息来推断出比 dynamic 更具体的类型参数。printInts() 函数期望一个 List<int> 类型的参数,从而导致了类型不匹配。

当在创建列表时添加类型注解(<int>)(如下高亮所示),分析器会抱怨一个字符串参数不能赋值给一个 int 参数。移除 list.add('2') 中的引号后,代码将通过静态分析,并且运行时没有任何错误或警告。

1
2
3
4
5
6
7
8
9
✔ 静态分析:成功
void printInts(List<int> a) => print(a);

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

在 DartPad 中尝试一下。

什么是健全性?

健全性(Soundness)是为了确保您的程序不会进入某些无效状态。一个健全的类型系统意味着您永远不会进入这样一种状态:一个表达式的计算结果值与其静态类型不匹配。例如,如果一个表达式的静态类型是 String,那么在运行时,当您对它求值时,可以保证只会得到一个字符串。

Dart 的类型系统,就像 Java 和 C# 中的类型系统一样,是健全的。它通过结合静态检查(编译时错误)和运行时检查来强制实现这种健全性。例如,将 String 赋值给 int 是一个编译时错误。如果一个对象不是 String,使用 as String 将其转换为 String 会在运行时失败并抛出错误。

健全性的好处

一个健全的类型系统有几个好处:

  • 在编译时揭示与类型相关的错误。
    一个健全的类型系统强制代码对其类型 unambiguous(清晰明确),因此那些在运行时可能难以发现的与类型相关的错误会在编译时被揭示出来。

  • 代码更具可读性。
    代码更容易阅读,因为您可以信赖一个值确实具有其指定的类型。在健全的 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 方法:

1
2
3
4
5
6
class Animal {
void chase(Animal a) {
...
}
Animal get parent => ...
}

parent getter 方法返回一个 Animal。在 HoneyBadger 子类中,您可以将 getter 的返回类型替换为 HoneyBadger(或 Animal 的任何其他子类型),但不允许使用不相关的类型。

1
2
3
4
5
6
7
8
9
10
✔ 静态分析:成功
class HoneyBadger extends Animal {
@override
void chase(Animal a) {
...
}

@override
HoneyBadger get parent => ...
}
1
2
3
4
5
6
7
8
9
10
11
12
✗ 静态分析:失败
class Root {}

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

@override
Root get parent => ... // 错误:返回类型 'Root' 不是 'Animal' 的子类型
}

重写方法时使用健全的参数类型

被重写方法的参数必须具有与其超类中相应参数相同的类型或超类型。不要通过将类型替换为原始参数的子类型来“收紧”(tighten)参数类型。

注意
如果您有正当理由使用子类型,您可以使用 covariant 关键字。

考虑 Animal 类的 chase(Animal) 方法:

1
2
3
4
5
6
class Animal {
void chase(Animal a) {
...
}
Animal get parent => ...
}

chase() 方法接受一个 AnimalHoneyBadger(蜜獾)会追逐任何东西。因此可以重写 chase() 方法使其接受任何东西(Object)。

1
2
3
4
5
6
7
8
9
10
✔ 静态分析:成功
class HoneyBadger extends Animal {
@override
void chase(Object a) {
...
}

@override
Animal get parent => ...
}

以下代码将 chase() 方法的参数从 Animal 收紧为 MouseAnimal 的一个子类)。

1
2
3
4
5
6
7
8
9
10
11
✗ 静态分析:失败
class Mouse extends Animal {
...
}

class Cat extends Animal {
@override
void chase(Mouse a) { // 错误:参数类型 'Mouse' 不是 'Animal' 的超类型
...
}
}

这段代码不是类型安全的,因为这样一来,就可以定义一只猫,然后派它去追一只鳄鱼:

1
2
Animal a = Cat();
a.chase(Alligator()); // 既不类型安全,也不对猫安全。

不要将 dynamic 列表用作类型化列表

当您希望一个列表中包含不同类型的东西时,dynamic 列表是很好的选择。但是,您不能将 dynamic 列表用作类型化列表。

这条规则也适用于泛型类型的实例。

以下代码创建了一个 Dogdynamic 列表,并将其赋值给一个类型为 Cat 的列表,这会在静态分析期间产生错误。

1
2
3
4
5
6
7
8
✗ 静态分析:失败
class Dog {}
class Cat {}

void main() {
List<Cat> foo = <dynamic>[Dog()]; // 错误
List<dynamic> bar = <dynamic>[Dog(), Cat()]; // OK
}

运行时检查

运行时检查处理那些在编译时无法检测到的类型安全问题。

例如,以下代码在运行时会抛出异常,因为将一个 Dog 的列表转换为 Cat 的列表是错误的:

1
2
3
4
5
6
7
8
9
✗ 运行时:失败
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}

void main() {
List<Animal> animals = <Dog>[Dog()];
List<Cat> cats = animals as List<Cat>; // 抛出运行时异常
}

dynamic 进行的隐式向下转型

静态类型为 dynamic 的表达式可以被隐式转换为更具体的类型。如果实际类型不匹配,该转换会在运行时抛出错误。考虑下面的 assumeString 方法:

1
2
3
4
5
✔ 静态分析:成功
int assumeString(dynamic object) {
String string = object; // 在运行时检查 `object` 是否为 `String`。
return string.length;
}

在这个例子中,如果 object 是一个 String,转换会成功。如果它不是 String 的子类型,比如 int,则会抛出 TypeError

1
2
✗ 运行时:失败
final length = assumeString(1); // 抛出 TypeError

提示
为了防止从 dynamic 进行隐式向下转型并避免此问题,可以考虑启用分析器的严格转型模式。

1
2
3
4
# analysis_options.yaml
analyzer:
language:
strict-casts: true

要了解更多关于自定义分析器行为的信息,请查看 自定义静态分析

类型推断

分析器可以为字段、方法、局部变量和大多数泛型类型参数推断类型。当分析器没有足够的信息来推断一个特定类型时,它会使用 dynamic 类型。

以下是类型推断如何与泛型协同工作的一个例子。在这个例子中,一个名为 arguments 的变量持有一个 Map,该 Map 将字符串键与各种类型的值配对。

如果您显式地为变量指定类型,您可能会这样写:

1
Map<String, Object?> arguments = {'argA': 'hello', 'argB': 42};

或者,您可以使用 varfinal 让 Dart 推断类型:

1
var arguments = {'argA': 'hello', 'argB': 42}; // 推断为 Map<String, Object>

Map 字面量会从其条目中推断出自己的类型,然后变量再从 Map 字面量的类型中推断出自己的类型。在这个 Map 中,键都是字符串,但值的类型不同(Stringint,它们的共同上界是 Object)。因此,Map 字面量的类型是 Map<String, Object>arguments 变量的类型也是如此。

字段和方法的类型推断

  • 一个没有指定类型并且重写了超类中字段或方法的方法或字段,会继承超类方法或字段的类型。

  • 一个没有声明或继承类型但用初始值声明的字段,会根据初始值获得一个推断的类型。

静态字段的类型推断

静态字段和变量的类型是从它们的初始化器中推断出来的。请注意,如果遇到循环(即,推断一个变量的类型依赖于知道该变量的类型),推断会失败。

局部变量的类型推断

局部变量的类型是从它们的初始化器(如果有的话)中推断出来的。后续的赋值不会被考虑在内。这可能意味着推断出的类型可能过于精确。如果是这样,您可以添加类型注解。

1
2
3
✗ 静态分析:失败
var x = 3; // x 被推断为 int。
x = 4.0; // 错误:double 类型的值不能赋给 int 类型的变量。
1
2
3
✔ 静态分析:成功
num y = 3; // num 可以是 double 或 int。
y = 4.0;

类型参数的类型推断

构造函数调用和泛型方法调用的类型参数是根据上下文的向下信息和构造函数或泛型方法参数的向上信息的组合来推断的。如果推断的结果不符合您的期望,您可以随时显式指定类型参数。

1
2
3
4
5
6
7
8
9
✔ 静态分析:成功
// 推断为 <int>[]
List<int> listOfInt = [];

// 推断为 <double>[3.0]
var listOfDouble = [3.0];

// 推断为 Iterable<int>
var ints = listOfDouble.map((x) => x.toInt());

在最后一个例子中,x 使用向下信息被推断为 double。闭包的返回类型使用向上信息被推断为 int。Dart 使用这个返回类型作为向上信息来推断 map() 方法的类型参数:<int>

基于类型约束的推断

版本说明
基于类型约束的推断需要语言版本至少为 3.7.0。

通过基于类型约束的推断功能,Dart 的类型推断算法通过将现有约束与声明的类型边界相结合来生成约束,而不仅仅是尽力而为的近似值。

这对于 F-bounded 类型尤其重要,在这种情况下,基于类型约束的推断可以正确推断出,在下面的例子中,X 可以被约束为 B。如果没有这个功能,类型参数必须显式指定:f<B>(C())

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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. 如果不使用类型约束,依赖于尽力而为近似值的推断
// 会在检测到 `C` 不是 `A<C>` 的子类型后失败。
f(C());

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

这里有一个更现实的例子,使用了 Dart 中的日常类型,如 intnum

1
2
3
4
5
6
X max<X extends Comparable<X>>(X x1, X x2) => x1.compareTo(x2) > 0 ? x1 : x2;

void main() {
// 有了该功能,推断为 `max<num>(3, 7)`,没有则会失败。
max(3, 7);
}

通过基于类型约束的推断,Dart 可以解构类型参数,从泛型类型参数的边界中提取类型信息。这使得像下面例子中的 f 这样的函数能够同时保留具体的可迭代类型(ListSet)和元素类型。在基于类型约束的推断出现之前,这在不丢失类型安全或特定类型信息的情况下是不可能实现的。

1
2
3
4
5
6
7
8
9
(X, Y) f<X extends Iterable<Y>, Y>(X x) => (x, x.first);

void main() {
var (myList, myInt) = f([1]);
myInt.whatever; // 编译时错误,`myInt` 的类型是 `int`。

var (mySet, myString) = f({'Hello!'});
mySet.union({}); // 可行,`mySet` 的类型是 `Set<String>`。
}

如果没有基于类型约束的推断,myInt 的类型将是 dynamic。之前的推断算法不会在编译时捕获到不正确的表达式 myInt.whatever,而是在运行时抛出错误。相反,如果没有基于类型约束的推断,mySet.union({}) 将是一个编译时错误,因为之前的算法无法保留 mySet 是一个 Set 的信息。

要了解更多关于基于类型约束的推断算法的信息,请阅读设计文档

类型替换

当您重写一个方法时,您正在用可能具有新类型的东西(在新方法中)替换具有某种类型的东西(在旧方法中)。同样,当您将一个参数传递给一个函数时,您正在用具有另一种类型的东西(实际参数)替换具有一种类型的东西(声明了类型的形参)。那么,什么时候可以用一个子类型或超类型来替换具有某种类型的东西呢?

在替换类型时,从消费者(consumers)和生产者(producers)的角度思考会有所帮助。消费者吸收一种类型,而生产者生成一种类型。

你可以用一个超类型替换消费者的类型,用一个子类型替换生产者的类型。

让我们看看简单类型赋值和泛型类型赋值的例子。

简单类型赋值

当将对象赋值给对象时,什么时候可以用一个不同的类型替换另一个类型?答案取决于对象是消费者还是生产者。

考虑以下类型层次结构:

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() 是一个生产者:

1
Cat c = Cat();

在一个消费位置,用一个能消费任何东西(Animal)的东西来替换一个消费特定类型(Cat)的东西是安全的,所以用 Animal c 替换 Cat c 是允许的,因为 AnimalCat 的超类型。

1
2
✔ 静态分析:成功
Animal c = Cat();

但是用 MaineCoon c 替换 Cat c 会破坏类型安全,因为超类可能会提供一个具有不同行为的 Cat 类型,比如 Lion

1
2
✗ 静态分析:失败
MaineCoon c = Cat(); // 错误

在一个生产位置,用一个更具体的类型(MaineCoon)替换一个生产某种类型(Cat)的东西是安全的。所以,以下是允许的:

1
2
✔ 静态分析:成功
Cat c = MaineCoon();

泛型类型赋值

对于泛型类型,规则是否相同?是的。考虑动物列表的层次结构——Cat 的列表是 Animal 列表的子类型,是 MaineCoon 列表的超类型:

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

在下面的例子中,您可以将一个 MaineCoon 列表赋值给 myCats,因为 List<MaineCoon>List<Cat> 的子类型:

1
2
3
✔ 静态分析:成功
List<MaineCoon> myMaineCoons = ...
List<Cat> myCats = myMaineCoons;

那么反过来呢?您可以将一个 Animal 列表赋值给一个 List<Cat> 吗?

1
2
3
✗ 静态分析:失败
List<Animal> myAnimals = ...
List<Cat> myCats = myAnimals; // 错误

这个赋值无法通过静态分析,因为它创建了一个隐式向下转型,而从非 dynamic 类型(如 Animal)进行隐式向下转型是不允许的。

要使这类代码通过静态分析,您可以使用显式转型。

1
2
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
✔ 静态分析:成功
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 的更多信息:

作者

wuhunyu

发布于

2025-09-04

更新于

2025-09-11

许可协议