Dart 语法要点(写给 JS/TS 背景的你)

要点

写给 JS/TS 背景、被 Flutter 带进 Dart 的人。Dart 的语法表面积不大,但五个机制决定你写出来的是”能跑的 Dart”还是”披着 Dart 外衣的 JS”:null safety 的纪律、Future 与 event loop 的同与不同、isolate 的并发模型、Dart 3 的 record 与模式匹配、extension 与 mixin 的组合方式。每节先说为什么这么设计,再给一张 TS 对照表。

一、Null Safety:类型里的”可能没有”

Dart 3 起 null safety 是强制的:类型默认不可空,可空要显式声明 String?。为什么值得 TS 开发者认真对待:TS 的 strictNullChecks 是渐进式检查,any 和 x as string 随时能把检查绕回史前时代;Dart 没有 any,类型收窄是闭环的——编译器拒绝把 String? 当 String 用,你必须在两条路里选一条:处理缺失,或用 ! 把风险推迟到运行时(运行时抛错,Flutter 里就是红屏)。

String? nickname;                 // 声明处就承认可能没有值

// 方式一:判空提升(推荐用于局部变量)
if (nickname != null) {
  print(nickname.length);         // 在这个分支里 nickname 已提升为 String
}

// 方式二:?? 给默认值
final display = nickname ?? '游客';

// 方式三:?. 链式访问,任一环节为 null 整体就是 null
final first = nickname?.characters.first;   // String?

// ⚠️ ! 是断言不是转换:这里错了就是运行时崩溃,不是返回 undefined
final forced = nickname!;

另一个 JS 没有的词:late。字段现在声明、稍后初始化(典型场景:Flutter 的 State 字段在 initState 里赋值),编译器因此不再要求声明处给初值。代价是检查推迟到运行时——忘记初始化就是 LateInitializationError,所以 late 只用在初始化时机确定的地方,不要拿它当偷懒的默认值。

TS 写法 Dart 写法 差异
string String 都不可空
string | null String? 记法不同,语义同
x?.y / x ?? d / x! 完全相同 可直接迁移
let x!: string(确定赋值断言) late String x 都是”我保证会初始化”,Dart 运行时兜底抛错
any 一路绿灯 没有 any Dart 绕过检查只剩 !,风险显式化

二、Future 与 async/await:可迁移的心智,更小的 API 面

Future<T> 对应 Promise<T>,async/await 语法几乎同形,try/catch/finally 照用。差异在三点。其一,类型是真的:Future<int> 声明后拿到的就是 int,不靠 .d.ts。其二,异步 API 面小得多——Dart 同样有 microtask 与 event 两级队列,心智模型可平移,但没有 JS 里 setTimeout、setImmediate、requestAnimationFrame 各归哪级队列的记忆负担,”microtask 歧义”这类经典面试题在 Dart 里基本没有存在感。其三,未 await 的 Future:JS 是 unhandledrejection 事件,Dart 会成为未捕获错误打印出来;靠 unawaited_futures 这类 lint 在编译期就逼你显式表态(unawaited(future) 或 await)。

// main 本身就可以是 async 的,入口不用包一层立即执行函数
Future<void> main() async {
  try {
    // Future.wait 对应 Promise.all:并行发起,任一失败整体失败
    final results = await Future.wait([
      fetchUser(),
      fetchOrders(),
    ]);
  } catch (e) {
    // await 的错误用 try/catch 接,不靠 .catch 链
    print('加载失败: $e');
  }
}
JS / TS Dart 备注
Promise Future 结构同
Promise.all Future.wait 语义同,失败即整体失败
AsyncIterable / RxJS Stream + await for Stream 是语言与标准库一级公民
unhandledrejection 未捕获错误 + unawaited_futures lint Dart 鼓励编译期表态

三、Isolate:单线程,但不是 JS 那种 event loop

和 JS 一样,Dart 的每个 isolate 内部是单线程事件循环,await 不会开新执行流——CPU 密集计算照样把事件循环占死,在 Flutter 里表现为掉帧。不一样的是解法:JS 要跳出主线程得借宿主环境的 Web Worker / worker_threads,Dart 的 isolate 是语言级概念:每个 isolate 有独立堆内存,isolate 之间不共享可变状态,只通过消息传递(SendPort/ReceivePort)通信。为什么这样设计:没有共享内存就没有数据竞争,不需要锁;代价是对象跨 isolate 传递要序列化,大数据搬动有成本。

import 'dart:isolate';

// Dart 2.19+ 的一行式 API:把重计算丢进新 isolate,拿回结果
Future<int> heavySum() => Isolate.run(() {
      var sum = 0;
      for (var i = 0; i < 100000000; i++) {
        sum += i;
      }
      return sum;   // 结果按消息传回,不是共享变量
    });

// Flutter 里等价的便捷封装:compute(函数, 参数)
维度 JS Dart
并发单元 Web Worker / worker_threads(宿主提供) Isolate(语言级)
共享内存 否,结构化克隆或 transferable 否,消息传递
最短路径 API 各有各的样板 Isolate.run / Flutter 的 compute
主线程卡顿 卡死整个页面 同样卡,但迁移到 isolate 是常规操作

最常见的误区:以为加了 async 就不卡了。async 只解决”等待”的并发(网络、文件、定时器),不解决”计算”的并行;解析一个 50MB 的 JSON 用 async 包装还是主 isolate 在算,照样掉帧。

四、Record 与模式匹配(Dart 3.0+)

TS 用元组和可辨识联合(discriminated union)解决的问题,Dart 3.0 用 record 与模式匹配(pattern matching)回答,而且是语言级而非类型层模拟。record 是匿名、不可变的聚合类型,最直接的收益是函数多返回值不用再造一个只用一次的类:

// 命名 record:字段自带名字,调用方解构可读
(double x, double y) midpoint((double, double) a, (double, double) b) =>
    ((a.$1 + b.$1) / 2, (a.$2 + b.$2) / 2);

final (mx, my) = midpoint((0, 0), (10, 20));   // 解构赋值

// sealed + switch 表达式:编译器检查穷尽性,漏分支直接编译失败
sealed class LoadState {}
class Idle extends LoadState {}
class Loading extends LoadState {}
class Loaded extends LoadState {
  Loaded(this.items);
  final List<String> items;
}

String describe(LoadState s) => switch (s) {
      Idle() => '待机',
      Loading() => '加载中',
      Loaded(:final items) => '已加载 ${items.length} 条',   // 对象模式里解构字段
    };

和 TS 的关键差别:TS 的收窄是控制流分析,漏一个 case 运行时才暴露 undefined;Dart 对 sealed 子类的 switch 由编译器强制穷尽,新增一个子类后所有漏掉它的 switch 立刻变红——重构安全性的量级不同。

TS Dart 3+ 差异
[number, string] 元组 (int, String) record Dart record 不可变
{x: number, y: number} (x: double, y: double) 命名字段写进类型
if (‘tag’ in v) 收窄 if (v case Loaded(…)) 模式可在任意布尔上下文用
never 穷尽性技巧 sealed + switch 编译期穷尽检查 Dart 是硬保证

五、Extension:给不是你的类型加方法

为什么需要它:给 String、int 或第三方库的类加个便捷方法,JS 时代的做法是改 prototype(全局污染)或写工具函数(调用难看)。Dart 的 extension 让你以静态方式挂成员,且必须 import 所在文件才生效,不污染全局。Flutter/Dart 标准库大量用它,比如 Theme.of(context).textTheme 链路里的很多便捷属性。

extension PriceX on int {
  String get asYuan => '¥$this.00';
}

print(99.asYuan);   // ¥99.00 —— 像原生方法一样调用

// ⚠️ 坑:extension 按静态类型派发,不参与运行时多态
dynamic x = 99;
// x.asYuan 会抛 NoSuchMethodError:dynamic 调用看不到 extension
TS Dart 差异
declaration merging / 模块扩充 extension on 目标类型 Dart 需显式 import 才可见
改 prototype(污染全局) 仅编译期织入,不改运行时对象 无跨库冲突
接口继承可复写 静态派发,不能被子类覆写 多态场景别用 extension

六、Mixin:横向组合,注意线性化

继承只能单根,能力复用靠 with 横向混入。Flutter 源码里到处是 mixin(如给 State 提供 vsync 的 SingleTickerProviderStateMixin)。TS 没有语言级 mixin,社区惯用法是 applyMixins 工具函数;Dart 把它做进语言并有明确语义——线性化:多个 mixin 同名成员时,后 with 的胜出,类自身实现再覆盖所有 mixin。

mixin Logger {
  void log(String msg) => print('[log] $msg');
}

mixin Timestamp {
  void log(String msg) => print('${DateTime.now()} $msg');   // 同名,覆盖先前者
}

class Service with Logger, Timestamp {}   // 线性化:Timestamp.log 生效

Service().log('启动');   // 输出带时间戳的版本

坑有二:同名成员的覆盖关系纯靠 with 顺序决定,读代码时必须看完整声明链;mixin 能携带状态,混多了字段来源难追。经验法则是 mixin 只放”能力”(日志、动画 tick、缓存),不放业务身份。

最佳实践

把 ! 当作用来还债的记账工具:每写一个,问自己这里为什么不可能为 null;说不出口就改用 if 判空或 ?? 默认值。实例字段判空不会像局部变量那样自动提升(if (widget.title != null) 之后 widget.title 仍是可空类型,因为两次访问之间理论上可能被改),需要先赋给局部变量。异步代码统一 try/catch 风格、开 unawaited_futures lint;重计算一律 Isolate.run。可辨识状态优先 sealed class + switch 表达式,让编译器替你盯穷尽性。

常见问题

Q1: 明明判过空了,编译器还说可空

多半是实例字段:类型提升只对局部变量和参数生效,字段随时可能被其他代码改掉,编译器不敢提升。把字段先读进局部变量再判空。

Q2: late 字段报 LateInitializationError

使用先于初始化。检查赋值路径是否一定发生在使用前(Flutter 里通常是 initState);做不到确定时序就去掉 late,改可空类型或给初值。

Q3: record 和普通类怎么选

一次性、小粒度、消费完就丢的聚合(坐标、键值对、多返回值)用 record;有身份、要复用、行为多于数据的,老实定义类。record 不可变,想”改”只能新建。

练习与实践

练习一:写 int? parseAge(String? raw)——null、空串、非数字、越界(0-150 之外)都返回 null,禁止用异常当流程控制。练习二:用 sealed class 表达 Idle / Loading / Loaded / Failed 四态,Failed 携带错误消息 record,switch 表达式穷尽处理后,再新增一个 Canceled 态,观察编译器把哪些 switch 点标红。练习三:给 String 写 extension:ellipsize(int max) 超长截断加省略号,并故意通过 dynamic 变量调用一次,验证 NoSuchMethodError 的存在。

下一步

这一篇补齐了语言层,下一篇《Flutter 基础知识参考字典》把框架侧的 Widget、布局、滚动、状态、导航做成可查条目,配合本文按需检索。写应用时遇到 setState 生命周期或报错先查字典;Dart 侧更深的语言规范以 dart.dev/language 为准。上手过程按《第一个 Flutter 应用》的顺序做一遍,语法就都有了落点;从 TS 转过来还想系统对比类型系统的,站内《TypeScript 语法要点》正好是本文的镜像篇目。

版本核实(撰写时检索,2024-04-29):Dart 当前稳定版为 3.3(随 Flutter 3.19 发布)。null safety 自 Dart 3.0(2023-05)起强制;record、模式匹配、class modifiers(sealed/base/final/interface/mixin class)均为 Dart 3.0 特性;Isolate.run 自 Dart 2.19 起可用。具体行为以 dart.dev 官方文档为准。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇