Dart 类型系统
Dart 语言是类型安全的:它结合使用静态类型检查和 运行时检查 来确保变量的值始终与其静态类型匹配,这有时被称为健全类型。尽管类型是强制性的,但由于类型推断,类型注解是可选的。
静态类型检查的一个好处是能够使用 Dart 的 静态分析器 在编译时发现错误。
您可以通过向泛型类添加类型注解来修复大多数静态分析错误。最常见的泛型类是集合类型 List<T>
和 Map<K,V>
。
例如,在下面的代码中,printInts()
函数打印一个整数列表,而 main()
创建一个列表并将其传递给 printInts()
。
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')
中的引号后,代码通过静态分析,并且运行时没有错误或警告。
void printInts(List<int> a) => print(a);
void main() {
final list = <int>[];
list.add(1);
list.add(2);
printInts(list);
}
什么是健全性?
#健全性旨在确保您的程序不会进入某些无效状态。健全的类型系统意味着您永远不会遇到表达式求值结果与表达式静态类型不匹配的状态。例如,如果一个表达式的静态类型是 String
,那么在运行时,您求值时一定会得到一个字符串。
Dart 的类型系统,就像 Java 和 C# 的类型系统一样,是健全的。它通过静态检查(编译时错误)和运行时检查的组合来强制执行健全性。例如,将 String
赋值给 int
会导致编译时错误。如果对象不是 String
,使用 as String
将对象转换为 String
将导致运行时错误。
健全性的好处
#健全的类型系统有几个好处
在编译时发现与类型相关的错误。
健全的类型系统强制代码在类型上是明确的,因此在运行时可能难以发现的类型相关错误会在编译时被揭示出来。更易读的代码。
代码更易读,因为您可以相信一个值确实具有指定的类型。在健全的 Dart 中,类型不会说谎。更易维护的代码。
有了健全的类型系统,当您更改一段代码时,类型系统可以警告您刚刚被破坏的其他代码片段。更好的预先 (AOT) 编译。
虽然没有类型也可以进行 AOT 编译,但生成的代码效率会低得多。
通过静态分析的技巧
#大多数静态类型的规则都很容易理解。以下是一些不太明显的规则
- 重写方法时使用健全的返回类型。
- 重写方法时使用健全的参数类型。
- 不要将动态列表用作类型化列表。
让我们详细看看这些规则,并使用以下类型层次结构的示例

重写方法时使用健全的返回类型
#子类中方法的返回类型必须与超类中方法的返回类型相同或为其子类型。考虑 Animal
类中的 getter 方法
class Animal {
void chase(Animal a) {
...
}
Animal get parent => ...
}
parent
getter 方法返回一个 Animal
。在 HoneyBadger
子类中,您可以将 getter 的返回类型替换为 HoneyBadger
(或 Animal
的任何其他子类型),但不允许使用不相关的类型。
class HoneyBadger extends Animal {
@override
void chase(Animal a) {
...
}
@override
HoneyBadger get parent => ...
}
class HoneyBadger extends Animal {
@override
void chase(Animal a) {
...
}
@override
Root get parent => ...
}
重写方法时使用健全的参数类型
#重写方法的参数类型必须与超类中对应参数的类型相同或为其超类型。不要通过将类型替换为原始参数的子类型来“收紧”参数类型。
考虑 Animal
类的 chase(Animal)
方法
class Animal {
void chase(Animal a) {
...
}
Animal get parent => ...
}
chase()
方法接受一个 Animal
。HoneyBadger
追逐任何东西。重写 chase()
方法以接受任何类型 (Object
) 是允许的。
class HoneyBadger extends Animal {
@override
void chase(Object a) {
...
}
@override
Animal get parent => ...
}
以下代码将 chase()
方法的参数从 Animal
收紧为 Mouse
,Mouse
是 Animal
的子类。
class Mouse extends Animal {
...
}
class Cat extends Animal {
@override
void chase(Mouse a) {
...
}
}
此代码不是类型安全的,因为它允许定义一只猫去追逐一只鳄鱼
Animal a = Cat();
a.chase(Alligator()); // Not type safe or feline safe.
不要将动态列表用作类型化列表
#如果您想拥有一个包含不同类型元素的列表,dynamic
列表是个不错的选择。但是,您不能将 dynamic
列表用作类型化列表。
此规则也适用于泛型类型的实例。
以下代码创建一个 dynamic
的 Dog
列表,并将其赋值给一个类型为 Cat
的列表,这将在静态分析期间生成错误。
void main() {
List<Cat> foo = <dynamic>[Dog()]; // Error
List<dynamic> bar = <dynamic>[Dog(), Cat()]; // OK
}
运行时检查
#运行时检查处理编译时无法检测到的类型安全问题。
例如,以下代码在运行时会抛出异常,因为将狗的列表转换为猫的列表是错误的
void main() {
List<Animal> animals = <Dog>[Dog()];
List<Cat> cats = animals as List<Cat>;
}
来自 dynamic
的隐式向下转型
#静态类型为 dynamic
的表达式可以隐式转换为更具体的类型。如果实际类型不匹配,转换将在运行时抛出错误。考虑以下 assumeString
方法
int assumeString(dynamic object) {
String string = object; // Check at run time that `object` is a `String`.
return string.length;
}
在此示例中,如果 object
是 String
,则转换成功。如果它不是 String
的子类型,例如 int
,则会抛出 TypeError
。
final length = assumeString(1);
类型推断
#分析器可以推断字段、方法、局部变量和大多数泛型类型实参的类型。当分析器没有足够的信息来推断特定类型时,它会使用 dynamic
类型。
这是一个类型推断在泛型中如何工作的示例。在此示例中,名为 arguments
的变量持有一个映射,该映射将字符串键与各种类型的值配对。
如果您显式指定变量类型,可能会这样写
Map<String, Object?> arguments = {'argA': 'hello', 'argB': 42};
或者,您可以使用 var
或 final
并让 Dart 推断类型
var arguments = {'argA': 'hello', 'argB': 42}; // Map<String, Object>
Map 字面量从其条目推断其类型,然后变量从 Map 字面量的类型推断其类型。在此 Map 中,键都是字符串,但值具有不同的类型(String
和 int
,它们的上界是 Object
)。因此,Map 字面量的类型是 Map<String, Object>
,arguments
变量的类型也是如此。
字段和方法推断
#没有指定类型并重写超类字段或方法(的字段或方法)会继承超类方法或字段的类型。
没有声明或继承类型但使用初始值声明的字段,将根据初始值获得推断类型。
静态字段推断
#静态字段和变量从其初始化器推断类型。请注意,如果遇到循环(即,推断变量的类型依赖于已知该变量的类型),则推断会失败。
局部变量推断
#局部变量类型从其初始化器(如果有)推断。后续赋值不考虑在内。这可能意味着推断的类型过于精确。如果是这样,您可以添加类型注解。
var x = 3; // x is inferred as an int.
x = 4.0;
num y = 3; // A num can be double or int.
y = 4.0;
类型实参推断
#构造函数调用和 泛型方法 调用的类型实参是根据出现上下文的向下信息以及构造函数或泛型方法参数的向上信息结合推断出来的。如果推断结果与您想要或预期的不符,您可以随时显式指定类型实参。
// 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())
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 中的常用类型,例如 int
或 num
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
可以同时保留特定的可迭代类型(List
或 Set
)和元素类型。在使用边界进行推断之前,如果不在丢失类型安全或特定类型信息的情况下,这是不可能的。
(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({})
将是编译时错误,因为先前的算法无法保留 mySet
是 Set
的信息。
有关使用边界进行推断算法的更多信息,请阅读设计文档。
替换类型
#当您重写方法时,您是用可能具有新类型(在新方法中)的内容替换具有一种类型(在旧方法中)的内容。类似地,当您将实参传递给函数时,您是用具有另一种类型(实际实参)的内容替换具有一种类型(具有声明类型的形参)的内容。何时可以将具有一种类型的内容替换为具有子类型或超类型的内容?
替换类型时,以消费者和生产者的角度思考会有所帮助。消费者吸收类型,生产者生成类型。
您可以将消费者的类型替换为超类型,将生产者的类型替换为子类型。
让我们看看简单类型赋值和泛型类型赋值的示例。
简单类型赋值
#将对象赋值给对象时,何时可以用不同的类型替换一种类型?答案取决于对象是消费者还是生产者。
考虑以下类型层次结构

考虑以下简单赋值,其中 Cat c
是一个消费者,Cat()
是一个生产者
Cat c = Cat();
在消费位置,用消费任何类型 (Animal
) 的东西替换消费特定类型 (Cat
) 的东西是安全的,因此允许将 Cat c
替换为 Animal c
,因为 Animal
是 Cat
的超类型。
Animal c = Cat();
但是用 MaineCoon c
替换 Cat c
会破坏类型安全,因为超类可能提供具有不同行为的 Cat 类型,例如 Lion
MaineCoon c = Cat();
在生产位置,用更具体的类型 (MaineCoon
) 替换生产一种类型 (Cat
) 的东西是安全的。因此,以下操作是允许的
Cat c = MaineCoon();
泛型类型赋值
#这些规则适用于泛型类型吗?是的。考虑动物列表的层次结构——Cat
的 List
是 Animal
的 List
的子类型,也是 MaineCoon
的 List
的超类型

在以下示例中,您可以将 MaineCoon
列表赋值给 myCats
,因为 List<MaineCoon>
是 List<Cat>
的子类型
List<MaineCoon> myMaineCoons = ...
List<Cat> myCats = myMaineCoons;
反过来呢?可以将 Animal
列表赋值给 List<Cat>
吗?
List<Animal> myAnimals = ...
List<Cat> myCats = myAnimals;
此赋值无法通过静态分析,因为它会创建隐式向下转型,这对于非 dynamic
类型(例如 Animal
)是不允许的。
要使此类代码通过静态分析,可以使用显式转型。
List<Animal> myAnimals = ...
List<Cat> myCats = myAnimals as List<Cat>;
但是,显式转型在运行时可能仍然会失败,具体取决于被转型列表的实际类型 (myAnimals
)。
方法
#重写方法时,生产者和消费者规则仍然适用。例如

对于消费者(例如 chase(Animal)
方法),可以将参数类型替换为超类型。对于生产者(例如 parent
getter 方法),可以将返回类型替换为子类型。
更多信息请参阅重写方法时使用健全的返回类型和重写方法时使用健全的参数类型。
协变参数
#一些(很少使用的)编码模式依赖于通过将参数类型重写为子类型来收紧类型,这是无效的。在这种情况下,您可以使用 covariant
关键字告诉分析器您是故意这样做的。这会移除静态错误,并在运行时检查无效参数类型。
以下展示了如何使用 covariant
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 的更多信息