Dart 类型系统:var、final、const 与泛型
easydarttype-systemgenericsnull-safety
变量声明的三个维度
Dart 声明变量有多种关键字,主要沿两个维度划分:是否推断类型 和 是否可变。
var、dynamic 与显式类型
var name = 'Flutter'; // 推断为 String,之后不能赋非 String
dynamic anything = 'hello'; // 动态类型,可以赋任意值,放弃编译期检查
String explicit = 'world'; // 显式声明,与 var 推断结果等价
Object? obj = null; // 所有类型的根,可空
var 和显式类型声明在编译器看来完全等价——var x = 1 就是 int x = 1,类型在赋值时固定。
dynamic 是逃逸口:编译器不检查,所有类型检查推迟到运行时。就像把强类型语言当脚本语言用,牺牲安全换灵活。
Object? 和 dynamic 看起来相似,区别在于:
Object?仍受类型系统约束,不能直接调用方法,必须先做类型判断dynamic可以调用任何方法,编译器不报错(运行时可能崩溃)
Object? obj = 'hello';
obj.length; // ❌ 编译错误:Object? 没有 length
dynamic dyn = 'hello';
dyn.length; // ✅ 编译通过,运行时正确
dyn.nonExist(); // ✅ 编译通过,运行时崩溃
final 与 const:两种不变性
理解这两个关键字的本质差异至关重要:
final |
const |
|
|---|---|---|
| 何时确定值 | 运行时(只赋值一次) | 编译时(编译期常量) |
| 可用于实例变量 | ✅ | ❌(只能是 static const) |
| 对象本身可变 | 引用不变,对象内容可变 | 深度不变(整个对象树冻结) |
| Widget 优化 | 无特殊优化 | Flutter 框架跳过 rebuild |
// final:运行时确定
final now = DateTime.now(); // ✅ 运行时才知道当前时间
final list = [1, 2, 3];
list.add(4); // ✅ 引用不变,但内容可以改
// const:编译时确定
const pi = 3.14159; // ✅ 编译时已知
const today = DateTime.now(); // ❌ 运行时才能确定,不能 const
const list = [1, 2, 3];
list.add(4); // ❌ const 列表是不可变的,抛 UnsupportedError
Flutter 中 const Widget 的意义
在 Flutter 中,const 构造器有特殊的优化作用。两个相同 const 表达式会共享同一个实例(常量池复用):
// 这两个 Text 是同一个对象
const Text('Hello') == const Text('Hello'); // true(同一个实例)
// 非 const 每次都创建新实例
Text('Hello') == Text('Hello'); // false
更重要的是,const Widget 告诉 Flutter:「这个 Widget 永远不会变,跳过它的 rebuild」。在复杂的 Widget 树中,合理使用 const 可以显著减少 rebuild 次数。
// 好的实践:用 const 标记不会变化的 Widget
build(BuildContext context) {
return Column(
children: [
const Icon(Icons.star), // ✅ 不随父 Widget rebuild
Text(_dynamicText), // 需要 rebuild,不加 const
],
);
}
类型推断与类型检查
is 与 as
Object obj = 'Hello';
if (obj is String) {
// 进入 if 后,Dart 自动将 obj smart-cast 为 String
print(obj.length); // 无需强转,直接用
}
// as 是强制类型转换,失败时抛 TypeError
String s = obj as String; // 确信 obj 是 String 才用
Dart 的 smart cast(智能转换) 是空安全的重要组成部分:在 if (x is T) 分支内,编译器自动把 x 视为 T 类型,无需手动 as。
类型检查操作符
// 常见模式:switch 配合 sealed class(Dart 3.0+)
sealed class Shape {}
class Circle extends Shape { final double radius; Circle(this.radius); }
class Square extends Shape { final double side; Square(this.side); }
double area(Shape shape) => switch (shape) {
Circle(:final radius) => 3.14 * radius * radius,
Square(:final side) => side * side,
};
// Dart 编译器检查 sealed class 的所有子类是否都被处理
泛型
基本用法
// 泛型类
class Box<T> {
final T value;
Box(this.value);
}
Box<String> nameBox = Box('Flutter');
Box<int> numBox = Box(42);
泛型让代码复用同时保留类型安全,避免使用 dynamic 或 Object 造成的类型丢失。
泛型约束
用 extends 对类型参数加约束:
// T 必须是 num 或其子类(int、double)
T sum<T extends num>(List<T> list) {
return list.reduce((a, b) => (a + b) as T);
}
sum([1, 2, 3]); // T 推断为 int
sum([1.0, 2.5]); // T 推断为 double
sum(['a', 'b']); // ❌ 编译错误:String 不是 num
协变与逆变
Dart 的泛型是协变的(covariant),与 Java 有所不同:
List<String> strings = ['hello'];
List<Object> objects = strings; // ✅ Dart 允许(协变)
objects.add(42); // ✅ 编译通过,但运行时抛 TypeError
// 这是 Dart 泛型的一个已知缺陷:协变不是完全类型安全的
如果需要安全的逆变,可以使用 contravariant(通过函数参数实现)。在 Flutter 实践中,通常避免把 List<SubType> 当 List<SuperType> 使用。
泛型方法
// 泛型方法:类型参数在方法级别声明
T firstOrDefault<T>(List<T> list, T defaultValue) {
return list.isEmpty ? defaultValue : list.first;
}
// 调用时 T 自动推断
firstOrDefault(['a', 'b'], 'none'); // T = String
firstOrDefault([], 0); // T = int
late 关键字详解
late 有两个独立的用途,容易混淆:
用途一:延迟初始化(Late Initialization)
class DatabaseHelper {
late final Database _db; // 声明时无法初始化
Future<void> init() async {
_db = await openDatabase('app.db'); // 异步初始化
}
Future<List<Map>> query() async {
return await _db.query('users'); // 使用前已通过 init() 初始化
}
}
用途二:解除空安全警告
class MyWidgetState extends State<MyWidget> {
// AnimationController 必须在 initState 中初始化
// 用 late 告诉编译器:「我保证在使用前初始化」
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
}
}
注意:late 的安全保证由开发者承担,如果在初始化前访问,运行时会抛 LateInitializationError。这是一种「我知道自己在做什么」的声明。