Dart:写给 Kotlin 工程师的两小时
八成对。Dart 确实是那种「你不用学,读一遍就会」的语言——静态类型、类、接口、空安全、Lambda,全都在你意料之中。但有八个地方会绊你的脚,其中两个(惰性的 Iterable、库级私有)会让你写出能编译、跑起来才发现不对的代码。这一章只讲这八个,其余的你自己看着写就行。
先把「没有的东西」列清楚
学一门近亲语言,最快的路子不是看它有什么,而是看它缺什么——因为缺的那些,正是你会下意识去写、然后被编译器打脸的地方。
| Kotlin 里你天天用 | Dart 里 | 替代写法 |
|---|---|---|
data class | 没有 | 手写 ==/hashCode/copyWith,或者用 freezed 生成(第 22 章) |
object / companion object | 没有 | static 成员 + 私有构造 Foo._() |
it 隐式参数 | 没有 | 参数必须命名:(x) => x * 2 |
by lazy | 有等价物 | late final x = 昂贵计算();(首次读取时才算) |
apply / also / run | 没有 | 级联 ..(下面讲) |
suspend / 协程 / Dispatchers | 只有一半 | async/await 有,线程池和结构化并发没有(卷 V) |
private / internal | 只有一种 | 标识符前加 _,作用域是文件,不是类 |
检查异常 / throws | 没有 | 所有异常都是非检查的,编译器不管你抓不抓 |
绊脚点一:只有花括号里的才是具名参数
Kotlin 里任何参数都能具名调用。Dart 不行——参数分成位置参数和具名参数两类,在声明时就定死了:
// 位置参数:调用时必须按顺序,不能写名字
void a(String name, int age) {}
a('小林', 20);
// 方括号 = 可选的位置参数(少见)
void b(String name, [int age = 0]) {}
// 花括号 = 具名参数。默认可选,必填要写 required
void c({required String name, int age = 0}) {}
c(name: '小林', age: 20); // 顺序随便,可读性好
Flutter 的所有 Widget 构造函数都用第三种。所以你会看到满屏的 required this.xxx:
class CourseTile extends StatelessWidget {
const CourseTile({
super.key, // 把 key 传给父类,几乎每个 Widget 都有这一行
required this.title, // 「初始化形参」:直接赋值给同名字段
this.subtitle, // 可选,类型是 String?
this.onTap,
});
final String title;
final String? subtitle;
final VoidCallback? onTap;
...
}
this.title 这种写法叫初始化形参,等价于 Kotlin 主构造函数里的 val title: String——参数直接赋给同名字段,不用写 this.title = title;。
以前私有字段吃不到这个便利,因为具名参数不能以下划线开头,只能手写初始化列表。Dart 3.12(2026 年 5 月)修好了这个别扭:
// 以前:必须走初始化列表
class Hummingbird {
final String _petName;
Hummingbird({required String petName}) : _petName = petName;
}
// Dart 3.12 起:直接写
class Hummingbird {
final String _petName;
Hummingbird({required this._petName});
}
// 调用侧仍然用公开的名字,下划线被自动脱掉:
final h = Hummingbird(petName: 'Dash');
顺带一提,同版本还有一个实验中的主构造函数(primary constructors):class Point(final int x, final int y);——那基本就是 Kotlin 的主构造函数长出来了。要开 --enable-experiment=primary-constructors 才能用,别在正式项目里押注。
绊脚点二:Iterable 是惰性的,忘了 .toList() 就出事
这是从 Kotlin 过来最容易中招的一处。Kotlin 的 list.map { } 立刻返回一个新 List;Dart 的 .map() 返回一个惰性的 Iterable,什么都还没算。
final names = users.map((u) => u.name); // 类型是 Iterable<String>,没算 print(names.length); // 到这里才遍历一遍 print(names.length); // 又遍历一遍!
后果有两个。第一,你把它当 List 用会编译报错(Widget 的 children 要 List<Widget>),所以你会习惯性地写 .toList()——这是好事。第二,更阴的是:如果闭包里有副作用,它会被执行多次,或者一次都不执行。
// ✗ 这行什么也不会发生:map 是惰性的,没人遍历它 items.map((i) => saveToDb(i)); // ✓ 想要副作用就用 for,别用 map for (final i in items) saveToDb(i);
另外 where 就是 Kotlin 的 filter,fold 就是 fold,any/every 对应 any/all,firstWhere(…, orElse: …) 对应 firstOrNull——注意不给 orElse 且找不到时会抛异常,这也是个高频崩溃点。
绊脚点三:下划线是库级私有
_foo 在 Dart 里表示私有,但私有的边界是文件(严格说是库),不是类。也就是说:同一个 .dart 文件里的两个类,可以自由访问对方的 _ 成员。
这一条不是冷知识,它直接决定了 Flutter 的代码组织习惯:
// course_page.dart —— 一个文件里放一对
class CoursePage extends StatefulWidget {
const CoursePage({super.key});
@override
State<CoursePage> createState() => _CoursePageState();
}
// 私有 State 类,外部拿不到,但同文件的 CoursePage 能引用它
class _CoursePageState extends State<CoursePage> {
...
}
这就是为什么你到处看到 _XxxState 这种命名,也是为什么 Flutter 项目里「一个页面一个文件」是天然的组织方式。
绊脚点四:空安全的类型提升只认局部变量
写法上和 Kotlin 一模一样:?、!、??、??=、?.。区别在智能转换(Dart 叫类型提升)的范围:
String? name; // 局部变量
if (name != null) {
print(name.length); // ✓ 提升成 String
}
class Foo {
String? name; // 字段
void show() {
if (name != null) {
print(name.length); // ✗ 编译错误!字段不会提升
}
final n = name; // ✓ 先捞成局部变量
if (n != null) print(n.length);
}
}
为什么?因为字段随时可能被别的代码改掉(Dart 里 getter 甚至可能是个方法),编译器不敢保证。解法永远是同一句:先赋给一个局部 final 变量。Flutter 代码里满地都是 final onTap = widget.onTap; 就是这个原因。
还有一个你会用上的关键字是 late:
late final AnimationController _controller; // 声明时不赋值,但保证用之前一定赋
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this); // 在这里赋
}
late 是「我保证用之前会赋值,编译器你别管」。违约的代价是运行时抛 LateInitializationError,和 Kotlin 的 lateinit 一个脾气。另外 late final x = expensive(); 还兼职 by lazy:首次读取才计算。
两个专为 UI 而生的语法,Kotlin 没有
这两个你会天天写,值得单独拎出来。
集合 if / 集合 for / 展开
Kotlin 里往列表中间塞条件项,得写 buildList { } 或者 listOfNotNull。Dart 直接在字面量里支持控制流:
Column(
children: [
const Text('课程表'),
if (isTeacher) const Text('(教师视图)'), // 条件插入
for (final c in courses) CourseTile(title: c.name), // 循环展开
...extraWidgets, // 展开另一个列表
...?maybeNullList, // 空安全展开
if (loading) const CircularProgressIndicator() else const Text('加载完了'),
],
)
这是 Flutter 里构建界面最顺手的一个特性,没有它你就得回到「先建一个 List、再 add、再传进去」的丑写法。看到这个语法就该知道:Dart 是一门为了写 UI 而被专门改造过的语言。
级联 ..
相当于 Kotlin 的 apply:在同一个对象上连续调用,返回的还是那个对象。
final paint = Paint() ..color = Colors.blue ..strokeWidth = 2 ..style = PaintingStyle.stroke; // 对可空对象用 ?.. controller?..stop()..dispose();
亲手对一对:八处差异并排看
左边 Kotlin,右边 Dart,切换标签逐个过。建议在这里停三分钟——这八个点覆盖了你接下来写 Flutter 时 95% 会犯的语法级错误。
剩下的,你自己看着写就行
下面这些和 Kotlin 几乎逐字对应,不值得单独讲:字符串插值 '$name' 和 '${obj.field}'、三引号多行字符串、=> 单表达式函数体、abstract class / implements / extends、泛型与 extends 约束、运算符重载、typedef、异常 try/on/catch/finally(注意 Dart 用 on 异常类型 catch (e, stack) 这个略特别的写法)。
1. const 和 final 不是同一个东西。final 是「只赋值一次」,运行时算;const 是「编译期就定死」。在 Flutter 里这个区别不是风格问题而是性能问题,下一章之后的第 4 章专门讲它。
2. == 默认是引用相等。没有 data class,两个字段完全一样的对象默认不相等。用在 Set/Map 键或者比较 state 时会静悄悄地出错。
3. 整数除法是 ~/。7 / 2 得到 3.5(double),7 ~/ 2 才是 3。而且在 Web 上 int 其实是 JS 的 double,位运算行为会有差异——移动端不用管。
构造函数:Dart 比 Kotlin 多几种花样
构造函数是你写每个 Widget 都要碰的东西,Dart 这里比 Kotlin 多几个概念,值得单独认一下:
class Course {
final String id;
final String name;
int credits;
// 1. 普通构造 + 初始化形参(= Kotlin 主构造)
Course(this.id, {required this.name, this.credits = 0});
// 2. 命名构造函数(Kotlin 没有,很好用)
Course.empty() : id = '', name = '未命名', credits = 0;
Course.fromRow(List<String> row) : id = row[0], name = row[1], credits = 0;
// 3. 初始化列表:在构造体之前给 final 字段赋值、做断言
Course.checked(this.id, this.name, this.credits)
: assert(credits >= 0, '学分不能为负');
// 4. 工厂构造:可以返回缓存实例、返回子类、或返回 null 之外的东西
static final _cache = <String, Course>{};
factory Course.cached(String id, String name) =>
_cache.putIfAbsent(id, () => Course(id, name: name));
}
其中命名构造函数(Course.empty()、Course.fromJson())你会天天见到——第 22 章的 JSON 解析、各种 .of() 工厂,全靠它。Kotlin 里你得靠 companion object 的工厂方法模拟,Dart 直接是语言特性。工厂构造(factory)则能做「构造时其实返回一个缓存对象或子类」这种 Kotlin 主构造做不到的事——单例、对象池、根据参数返回不同实现,都用它。
Flutter 代码里几乎每个参数列表末尾都有一个多余的逗号:
Padding(
padding: const EdgeInsets.all(16),
child: const Text('你好'), // ← 注意这个结尾的逗号
)
这不是笔误,是刻意的——加了尾随逗号,dart format 就会把每个参数竖排成一行一个;不加,它会尽量挤成一行。写嵌套 Widget 时,你几乎总是想要竖排(可读、diff 干净、方便增删),所以养成到处加尾随逗号的习惯,让格式化器替你排版。这是 Flutter 代码风格里第一个要养成的肌肉记忆。
思路一样的:类型推断、空安全的语法、Lambda、扩展、泛型、字符串模板。这些你不用重学,写就完了。
要绕过去的:没有 data class(用 freezed)、没有 it、map 是惰性的(记得 .toList())、字段不做类型提升(先捞局部变量)、_ 是文件级私有。
白赚的:集合 if/for/展开——写 UI 时比 Kotlin 顺手,这是 Dart 为 Flutter 专门改造的地方。
这一章的一句话
Dart 对你来说不是一门新语言,是 Kotlin 的一个方言:拿掉 data class、协程和 it,加上专为构建 UI 设计的集合 if/for 与级联;真正会咬人的只有惰性 Iterable 和不做提升的字段这两处。
不过如果你对 Dart 的印象还停留在「没有 sealed、没有 when、建模很难受」,那已经过时两年了。下一章看看 Dart 3 补了什么货——有几样甚至比 Kotlin 的对应物更强。