Dart类型体系
Dart语言是类型安全的:它使用静态类型检查和运行时检查来确保变量值与变量的静态类型保持匹配。虽然类型是必须的,但类型的声明因类型推导的存在而是可选的。
本页聚焦于Dart 2中所增加的类型安全我。有关Dart语言包括类型的完整介绍,参见 语言导览。
静态类型检查的一个好处是能够使用静态分析器来发现编译时的 bug。
可以通过对泛型类添加类型标来修复大部分静态分析错误。最常见的泛型类是集合类型List<T>
和 Map<K,V>
。
例如,在如下的代码中 printInts()
函数打印一个整型列表, main()
创建一个列表并将其传递给 printInts()
。
1 2 3 4 5 6 7 8 |
class Cat extends Animal { ... } class Dog extends Animal { ... } void main() { List<Cat> foo = <dynamic>[Dog()]; // Error List<dynamic> bar = <dynamic>[Dog(), Cat()]; // OK } |
以上代码在调用printInts(list)
时会导致对list
(已高亮显示)的类型错误:
1 |
error • The argument type 'List' 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")
中的引号会让代码通过静态分析并且运行时不报出错误或警告。
1 2 3 4 5 6 7 8 |
void printInts(List<int> a) => print(a); void main() { var list = <int>[]; list.add(1); list.add(2); printInts(list); } |
什么是可靠性?
可靠性关乎确保程序不会进入某种无效状态。一个可靠的类型体系意味着你永远不会进入一种表达运行值与表达式静态类型不匹配的状态。例如,如果一个表达式的静态类型为String
,在运行时你所运行的结果一定也仅仅会是字符串类型的。
Dart的类型体系,如同Java和C#中的类型体系一样,是可靠的。它使用静态类型检查(编译时错误)和运行时检查的组合来强制实现可靠性。例如,向int
类型赋值String
会报出编译时错误。使用as String
将Object
投射给字符串在对象不是字符串时会报出运行时错误。
可靠性的优点
一个可靠的类型体系有如下好处:
- 在运行时暴露类型相关的漏洞。
一个可靠的类型体系强制代码对类型不会模棱两可,因此那些在运行时非常难以发现的类型相关问题在编译时就暴露出来了。 - 更为可读的代码。
代码更易于阅读,因为可以依赖于实际拥有指定类型的值。在可靠的Dart代码,类型不会掺假。 - 更易于维护的代码。
借助可靠的类型体系,在修改一段代码时,类型体系在其它段代码崩溃时会对你发出警告。 - 更好的预先 (AOT) 编译。
虽然没有类型也可以进行AOT编译,但所生成的代码效率更低。 Tips for passing static analysis
大多数静态类型的规则很容易理解。以下是一些不那么明显的规则:
- 使用重载方法时使用可靠的返回类型。
- 在重载方法时使用可靠的参数类型。
- 不要使用动态(dynamic)列表来作为有类型列表。
让我们来详细地看下这些规则,示例使用如下的类型等级结构:
使用重载方法时使用可靠的返回类型
子类中方法的返回类型必须是超类中方法相同的返回类型或其子类型。思考以下Animal类中的getter方法:
1 2 3 4 |
class Animal { void chase(Animal a) { ... } Animal get parent => ... } |
父级
getter返回类型为 Animal。在HoneyBadger子类型中,你可以将getter的返回类型替换为HoneyBadger (或其它Animal的子类型),但不允许使用没有关联的类型。
1 2 3 4 |
class HoneyBadger extends Animal { void chase(Animal a) { ... } HoneyBadger get parent => ... } |
1 2 3 4 |
class HoneyBadger extends Animal { void chase(Animal a) { ... } Root get parent => ... } |
在重载方法时使用可靠的参数类型
重载方法的参数必须对应超类中参数的类型相同,或是其子类型。不要通过替换类型为原参数的子类型来“收紧”参数的类型。
思考Animal类中的 chase(Animal)
方法:
1 2 3 4 |
class Animal { void chase(Animal a) { ... } Animal get parent => ... } |
chase()
方法接收一个 Animal类型。HoneyBadger追逐所有东西。 可以重载 chase()
方法来接收任何东西(Object)。
1 2 3 4 |
class HoneyBadger extends Animal { void chase(Object a) { ... } Animal get parent => ... } |
以下代码将chase()
方法的参数由Animal收紧为其子类Mouse。
1 2 3 4 5 |
class Mouse extends Animal {...} class Cat extends Animal { void chase(Mouse x) { ... } } |
这段代码类型不安全,因为这时可以定义一只猫来追逐鳄鱼。
1 2 |
Animal a = Cat(); a.chase(Alligator()); // Not type safe or feline safe |
不要使用动态列表来作为有类型列表
动态列表对于想要在列表中放入不同种类的内容时非常好。但是,不应使用动态列表来作为有类型列表。
这条规则也适用于泛型的实例。
以下代码创建一个动态列表 Dog,并将其赋值一个类型为Cat的列表,在静态分析时会产生报错。
1 2 3 4 5 6 7 8 |
class Cat extends Animal { ... } class Dog extends Animal { ... } void main() { List<Cat> foo = <dynamic>[Dog()]; // Error List<dynamic> bar = <dynamic>[Dog(), Cat()]; // OK } |
运行时检查
在 Dart VM and dartdevc 这样的处理分析器无法捕捉到的安全问题的工具中进行类型检查。
例如,以下代码在运行时抛出错误,因为将Dog列表赋值给Cat列表是错误的:
1 2 3 4 |
void main() { List<Animal> animals = [Dog()]; List<Cat> cats = animals; } |
类型推导
分析器可以推导字段、方法、局部变量和大部分泛型参数的类型。在分析没有推导具体类型的足够信息时,它会使用 dynamic
类型。
以下是一个如何对泛型推导类型的示例。在该例中,名为arguments
的变量存储配对字符串键和不同类型值的映射。
如果显式地为变量添加类型,会像这样编写:
1 |
Map<String, dynamic> arguments = {'argA': 'hello', 'argB': 42};> |
另外,也可以使用 var
来让Dart来推导其类型:
1 |
var arguments = {'argA': 'hello', 'argB': 42}; // Map<String, Object> |
映射字面量通过输入内容推荐其类型,然后通过映射字面量的类型推导出变量的类型。在这个映射中,键都是字符串,但值为不同类型(字符串和整型,上一级的绑定均为Object)。因此映射映射字面量的类型为Map<String, Object>
,那么arguments
变量也是如此。
字段和方法推导
没有指定类型的从超类重载字段或方法的字段或方法,继承超类中字段或方法的类型。
没有声明或继承类型但声明了初始值的字段,会根据初始值来推导出类型。
静态字段推导
静态字段和变量通过初始化语句推导出其类型。注意如果碰到循环则推导会失败(即推导类型的变量需要知道其所依赖的变量的类型)。
局部变量推导
局部变量类型由它们的初始化语句推导其类型。后续的赋值不作为考虑。这可能会让类型的推导过于精确。如果这样的话,可以添加类型标。
1 2 |
var x = 3; // x 推导为int类型 x = 4.0; |
1 |
num y = 3; // num可为双精度或整型 |
类型参数推导
构造函数和泛型方法 调用的类型参数根据所发生的下行上下文信息和参数对构造函数或泛型方法的上行信息的组合进行推导。如果推导的行为与你所预期的不同,可以保持显式地指定类型参数。
1 2 3 4 5 6 7 8 |
// 推导类型你写入了<int>[]. List<int> listOfInt = []; // 推导类似你写入了<double>[3.0] var listOfDouble = [3.0]; // 推导为Iterable<int> var ints = listOfDouble.map((x) => x.toInt()); |
上例中, x
使用下行信息推导为 double
类型。闭包的返回类型使用上行信息推导为int
类型。Dar在推导map()
方法的类型参数<int>
时使用这个返回类型作为上行信息。
替代类型
在重载方法时,会将一种类型的内容(老方法中的)替换为或许为新类型的内容(新方法中)。类似地,在传递参数给函数时,替换有一种类型的内容(具有所声明类型的参数)为另一种类型的内容(实际参数)。什么时候可以替换一种类型的内容为带有子类型或超类型的内容呢?
在替换类型时,所它想成消费者和生产者会有帮助。消费者吸收类型,而生产者产生类型。
可以替换消费者的类型为子类型以及替换生产者的类型为子类型。
我们来看一下简单类型赋值以及以及泛型赋值。
简单类型赋值
在将对象赋值给对象时,什么时候可以替换一种类型为另一种类型?答案取决于对象是消费者还是生产者。
思考如下的类型等级结构:
考虑如下的简单赋值,其中 Cat c
是消费者而 Cat()
是生产者。
1 |
Cat c = Cat(); |
站在消费的位置,替换消息具体类型(Cat
)的内容为消费任意类型(Animal
)的内容是安全的,因此允许替换 Cat c
为 Animal c
,因为Animal是Cat的超类。
1 |
Animal c = Cat(); |
但替换 Cat c
为 MaineCoon c
会打破类型安全,因为超类可能提供一种Cat的类型的不同行为,如Lion:
1 |
MaineCoon c = Cat(); |
站在生产的位置,替换生产一种类型(Cat)的内容为更具体类型(MaineCoon)的内容是安全的。因此允许如下操作:
1 |
Cat c = MaineCoon(); |
泛型赋值
对于泛型的规则是一样的吗?是的。考虑动物列表的等级,Cat列表是Animal列表的子类型,同时又是MaineCoon列表的超类型:
在下例中,可以为myCats
赋值一个 MaineCoon
列表,因为 List<MaineCoon>
是List<Cat>
的子类型:
1 |
List<Cat> myCats = List<MaineCoon>(); |
那如果换个方向呢?是否可以将一个 Animal
列表赋值给 List<Cat>
?
1 |
List<Cat> myCats = List<Animal>(); |
这一赋值传递静态分析, 但创建一个隐式投射。它等价于:
1 |
List<Cat> myCats = List<Animal>() as List<Cat>; |
代码可能在运行时失败。 可以通过指定implicit-casts: false
来在分析选项文件中禁止隐式投射。
方法
在重载方法时,生产者和消费者规则仍然适用。例如:
对于消费者 (如 chase(Animal)
方法),,可以替换参数类型为子类型。对于生产者(如parent
getter方法),可以替换返回类型为子类型。
更多信息,参见e 在重载方法时使用稳定返回类型 和 在重载方法时使用稳定参数类型。
其它资源
以下资源具有有关可靠的Dart的更多信息: