卷 I · 认领CH 02深度 02/24

Dart:写给 Kotlin 工程师的两小时

直觉过境 「Dart 就是个砍掉协程的 Kotlin,语法扫一眼就行。」 改一下

八成对。Dart 确实是那种「你不用学,读一遍就会」的语言——静态类型、类、接口、空安全、Lambda,全都在你意料之中。但有八个地方会绊你的脚,其中两个(惰性的 Iterable、库级私有)会让你写出能编译、跑起来才发现不对的代码。这一章只讲这八个,其余的你自己看着写就行。

具名参数空安全惰性集合集合 if/for级联

先把「没有的东西」列清楚

学一门近亲语言,最快的路子不是看它有什么,而是看它缺什么——因为缺的那些,正是你会下意识去写、然后被编译器打脸的地方。

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 的新礼物:私有字段也能这么写了

以前私有字段吃不到这个便利,因为具名参数不能以下划线开头,只能手写初始化列表。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 的 childrenList<Widget>),所以你会习惯性地写 .toList()——这是好事。第二,更阴的是:如果闭包里有副作用,它会被执行多次,或者一次都不执行。

// ✗ 这行什么也不会发生:map 是惰性的,没人遍历它
items.map((i) => saveToDb(i));

// ✓ 想要副作用就用 for,别用 map
for (final i in items) saveToDb(i);

另外 where 就是 Kotlin 的 filterfold 就是 foldany/every 对应 any/allfirstWhere(…, 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. constfinal 不是同一个东西。final 是「只赋值一次」,运行时算;const 是「编译期就定死」。在 Flutter 里这个区别不是风格问题而是性能问题,下一章之后的第 4 章专门讲它。

2. == 默认是引用相等。没有 data class,两个字段完全一样的对象默认不相等。用在 Set/Map 键或者比较 state 时会静悄悄地出错。

3. 整数除法是 ~/7 / 2 得到 3.5double),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 代码风格里第一个要养成的肌肉记忆。

⇄ Compose 对照 · 本章小结

思路一样的:类型推断、空安全的语法、Lambda、扩展、泛型、字符串模板。这些你不用重学,写就完了。

要绕过去的:没有 data class(用 freezed)、没有 itmap 是惰性的(记得 .toList())、字段不做类型提升(先捞局部变量)、_ 是文件级私有。

白赚的:集合 if/for/展开——写 UI 时比 Kotlin 顺手,这是 Dart 为 Flutter 专门改造的地方。

这一章的一句话

Dart 对你来说不是一门新语言,是 Kotlin 的一个方言:拿掉 data class、协程和 it,加上专为构建 UI 设计的集合 if/for 与级联;真正会咬人的只有惰性 Iterable 和不做提升的字段这两处。

不过如果你对 Dart 的印象还停留在「没有 sealed、没有 when、建模很难受」,那已经过时两年了。下一章看看 Dart 3 补了什么货——有几样甚至比 Kotlin 的对应物更强。