
前言
寄快递时,箱子上写着"书籍",工作人员就知道该按书籍处理。如果箱子里装的却是玻璃杯,只看标签安排搬运,东西可能会碎。标签要和里面的东西相符,才有用。
程序里的类型也在表达"这里应该放什么"。一个变量写着 String,我们希望拿到它时确实是字符串,能够放心地取长度、拼文字。如果它在某个环节悄悄变成了数字,错误最好在程序运行前就被发现。
Dart 的类型系统就是用来检查这些关系的。不过,var 没写类型,为什么也会报类型错误?Object 和 dynamic 看起来都能装各种值,为什么用起来不一样?弄清这几个问题,才能真正看懂 Dart 给出的类型提示。
一、类型系统在检查什么
类型系统是一套规则,用来判断值能放在哪里,以及对这个值能做哪些操作。
看一个最简单的例子:
dart
void main() {
String name = '小林';
print(name.length); // 2
// name = 18; // 错误:int 不能赋给 String
}
String 说明 name 应该保存字符串。既然是字符串,读取 length 就有依据;把整数放进去,Dart 会指出类型不合适。
这里有两个容易混淆的概念:
- 静态类型:代码中声明或推断出来的类型,Dart 分析代码时就能知道。
- 运行时类型:程序真正运行时,值所属的类型。
多数时候二者看起来一样,所以我们感觉不到区别。等一个变量声明为较宽的类型,却装着较具体的值时,区别就出现了。
dart
void showItem(Object item) {
print(item.runtimeType);
// print(item.length); // 错误:参数的静态类型 Object 上没有 length
}
void main() {
showItem('一本书'); // 输出 String
}
这次传进来的 item 是字符串,但 showItem 的参数声明为 Object,意味着下次也可以传入整数等别的对象。Dart 检查 item.length 时,不能只根据这一次传入的值假定它永远是字符串。
Dart 结合运行前的静态检查 和必要的运行时检查 来维护类型安全:静态类型写着 String 的表达式,运行时不能悄悄产生一个整数。有些错误可以提前发现;有些涉及动态值或强制转换的错误,要等运行时才能确认。Dart 类型系统
二、什么值能赋给什么类型
2.1 较具体的值可以放进较宽的位置
把"动物"看作较宽的类别,"猫"看作其中一种。要求一只动物时,给一只猫可以;要求一只猫时,随便给一只动物不行,因为那只动物也许是狗。
Dart 中,子类型与父类型的赋值遵循这个方向:需要父类型的地方,可以提供子类型的值。反过来不能直接保证安全。

图中的箭头表示"这个值可以放进哪种变量",不是把猫变成另一种动物。Cat 的值可以放进 Animal 变量;一个只知道是 Animal 的值,不能直接保证它是 Cat。
dart
class Animal {}
class Cat extends Animal {}
class Dog extends Animal {}
void main() {
Animal animal = Cat(); // 可以:猫是动物
animal = Dog(); // 也可以:狗是动物
// Cat cat = animal; // 错误:animal 也可能是狗
print(animal.runtimeType); // Dog
}
再看 Dart 常见的数字类型:int 和 double 都属于 num,所以 num 变量可以保存两者。
dart
void main() {
num price = 12;
price = 12.5;
// int count = price; // 错误:price 也可能是 double
print(price); // 12.5
}
注意,能赋值不等于自动转换 。num price = 12 没有把整数改造成小数;只是允许一个较宽的变量保存整数。若确实需要把数字转成别的形式,应明确使用相应的转换方法。Dart 类型系统:类型替换
2.2 可空类型也有自己的范围
String 表示非空字符串,String? 表示字符串或 null。因此非空字符串可以放进可空变量,可空变量却不能直接放进非空变量。
dart
void updateName(String? maybeName) {
String name = '小林';
// name = maybeName; // 错误:maybeName 可能为 null
if (maybeName != null) {
name = maybeName; // 检查后,这里可以当作 String 使用
}
print(name);
}
void main() {
updateName(null); // 小林
updateName('小王'); // 小王
}
检查后类型变得更具体,叫作类型提升 。它不是把值转换成另一种类型,而是 Dart 根据已经通过的条件,确认当前分支里可以怎样使用这个值。空安全篇已介绍过可空操作符,这里要记住它们背后的类型关系。理解 Dart 空安全
三、var、Object、dynamic 有什么区别
这三个写法常被误认为都是"随便放什么"。实际上,它们回答的是不同问题。
3.1 var:让 Dart 帮你写出类型
dart
void main() {
var count = 3; // 推断为 int
count = 4;
// count = '四'; // 错误:推断后,count 仍然是 int
print(count); // 4
}
var 省略的是类型名称,不是类型检查。Dart 根据初始值推断 count 是 int,后面不会因为赋了新值就随意改成 String。
若这个位置本来就需要保存整数或小数,应把范围写宽一些:
dart
num amount = 3;
amount = 4.5;
推断也依赖提供的信息。只写 var value; 而没有初始值时,Dart 无法从这一行推断出具体类型,value 会是 dynamic。集合的类型参数也可能因信息不足而推断为 dynamic,例如空列表 var items = \[\]; 是 List。写成 var items = \[\];,后续添加非字符串时就能提前发现错误。Dart 类型系统:类型推断
3.2 Object:能保存各种非空对象,操作仍受检查
dart
void main() {
Object item = '小林';
item = 18; // 可以:两者都是 Object 的子类型
// print(item.length); // 错误:Object 没有 length
print(item.toString()); // 可以:Object 提供 toString()
}
Object 表示"我知道这是一个非空对象,但暂时不限定它是哪一种"。它能接收很多值,Dart 仍会检查你调用的成员是否对所有 Object 都成立。需要同时容纳 null,可以使用 Object?。
3.3 dynamic:允许动态调用,风险留到运行时
dart
void main() {
dynamic item = '小林';
print(item.length); // 2
item = 18;
// print(item.length); // 静态检查不拦,运行时会出错
}
dynamic 不只允许放不同类型的值;它还让 item.length 这样的调用绕过通常的静态成员检查。调用能否成功,要看运行时实际装的值。因此,Object 与 dynamic 的关键差别在于使用这个值时,Dart 还能提前帮你检查多少。
| 写法 | 可以保存什么 | 调用特有成员时 |
|---|---|---|
| var name = '小林' | 推断后按 String 检查 | 可调用 String 的成员 |
| Object item(方法参数) | 各种非空对象 | 先确认具体类型,才能调用其特有成员 |
| dynamic item = '小林' | 各种值 | 静态检查通常不拦,可能在运行时报错 |
有时我们从外部数据得到的值确实暂时不确定类型,可以在入口处接收它,再尽早判断、转换成明确的类型;不必让 dynamic 一路传到程序各处。
四、知道值的类型,还是要求它变成某种类型
4.1 is:先判断,再使用
is 用于判断一个值是否属于某种类型。判断成立后,Dart 通常可以在当前分支把变量提升为更具体的类型。
dart
void describe(Object value) {
if (value is String) {
print('文字长度:${value.length}');
} else {
print('不是字符串');
}
}
这里没有把数字转换成字符串;is String 只是问"它本来是不是字符串"。
4.2 as:确认无误时再断言
as 则是断言并强制转换类型:告诉 Dart 按指定类型使用这个值。如果判断错了,运行时会失败。
dart
void main() {
Object value = '小林';
String name = value as String; // 实际是 String,成功
print(name.length); // 2
value = 18;
// String other = value as String; // 运行时类型转换错误
}
拿不准类型时,先用 is 判断更稳妥。as String 也不是把 18 变成 '18';真正需要文字表示时,用 toString() 等转换方法。Dart 运算符:类型测试
五、同一规则在集合和继承中如何起作用
前面讨论的是把一个值放进变量。把一组值交给函数,或者用子类替换父类时,Dart 也要保证调用方原本能够做的事依然安全。
5.1 集合要守住元素类型
printTotal() 需要从列表里取出整数并相加,所以参数写成 List。这个类型不仅描述列表,也规定了从列表取出的元素应该是什么。
dart
void printTotal(List<int> scores) {
print(scores.reduce((a, b) => a + b));
}
void main() {
final mixed = <dynamic>[10, '20'];
// printTotal(mixed); // 错误:List<dynamic> 不能直接当 List<int> 用
print(mixed); // 显示为 [10, 20],但第二个元素仍是字符串
final scores = <int>[10, 20];
printTotal(scores); // 30
}
mixed 中既有整数 10,也有字符串 '20'。打印列表时两者看起来都像数字,但 '20' 不能直接参与整数加法。即使列表当前恰好全是整数,List 也没有承诺以后取出的仍是整数,不能直接传给需要 List 的函数。
如果这些数据本来就应当全是整数,从创建列表时写 10, 20,Dart 就能在有人加入字符串时指出错误。这与单个变量的规则相同:要求整数的地方,不能只凭"现在看起来像整数"就交出未经确认的值。Dart 类型系统:不要把动态列表当作定型列表
5.2 子类重写方法,不能违背父类的约定
假设收容站 Shelter 的 receive() 方法答应接收任何 Animal。市区收容站 CityShelter 继承了它,别人可以把市区收容站当作普通收容站来调用。这种情况下,子类能改动方法的参数和返回值吗?
dart
class Animal {
Animal(this.name);
final String name;
}
class Cat extends Animal {
Cat(super.name);
}
class Dog extends Animal {
Dog(super.name);
}
class Shelter {
void receive(Animal animal) {
print('接收了${animal.name}');
}
Animal availableForAdoption() => Animal('一只动物');
}
class CityShelter extends Shelter {
@override
void receive(Animal animal) {
print('接收了${animal.name}');
}
// 不能改成 receive(Cat animal):调用方仍可能传入 Dog。
@override
Cat availableForAdoption() => Cat('小花');
}
void main() {
Shelter shelter = CityShelter();
shelter.receive(Dog('小黄')); // 接收了小黄
print(shelter.availableForAdoption().name); // 小花
}
先看 receive() 的参数。shelter 声明为 Shelter,调用方按父类约定可以传入 Dog。程序运行时,真正接收请求的是 CityShelter 对象。如果子类擅自把参数改成 Cat,这次传入 Dog 就无法兑现父类的约定,因此普通重写不能把参数类型缩窄。
再看 availableForAdoption() 的返回值。父类答应返回 Animal;子类返回 Cat,调用方仍然得到一只动物,所以这是允许的。接收参数时不能比原约定挑剔,提供结果时可以比原约定具体。 特殊场景可以用 covariant 缩窄参数,但相关检查会转到运行时;它不是普通重写的默认规则。Dart 类型系统:方法重写
六、总结
Dart 的类型系统让"这里能放什么、能做什么"成为可以检查的约定。
- 看赋值方向:需要较宽类型的地方,可以提供较具体的值;反过来要先证明或检查。
- 看变量的静态类型:它决定哪些操作能提前通过检查,不只看变量此刻装着什么。
- var 会推断类型,Object 保留静态检查,dynamic 把更多风险留到运行时。
- is 用于判断,as 是带运行时风险的类型断言;它们都不负责把整数变成字符串。
- 空安全、泛型和方法重写遵守的是同一件事:代码实际提供的值,必须符合对外承诺的类型。
遇到类型错误时,不妨先问:这个位置承诺接收什么类型?我给出的值可能是什么类型? 顺着这两个问题找,通常比直接加 dynamic 或 as 更容易找到真正的原因。
七、测试题
- var price = 12; price = 12.5; 为什么会报错?如果价格确实可能是整数或小数,该怎样声明?
- 方法的参数是 Object item,调用时传入了 '书'。为什么方法内不能直接写 item.length?写出一种安全的读取长度的方法。
- dynamic value = 10; print(value.length); 为什么可能通过静态检查,却在运行时出错?
- 一个方法需要 List,手里有 1, 2。仅凭两个元素现在都是整数,能直接把这个列表传进去吗?为什么?
- 父类方法接收 Animal 并返回 Animal。子类能否把参数改为只接收 Cat?能否把返回类型改为 Cat?说出理由。