惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

U
Unit 42
T
Threatpost
C
CERT Recently Published Vulnerability Notes
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Security Archives - TechRepublic
Security Archives - TechRepublic
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
K
Kaspersky official blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Attack and Defense Labs
Attack and Defense Labs
N
News and Events Feed by Topic
Project Zero
Project Zero
H
Heimdal Security Blog
C
Cybersecurity and Infrastructure Security Agency CISA
Know Your Adversary
Know Your Adversary
Google Online Security Blog
Google Online Security Blog
W
WeLiveSecurity
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Schneier on Security
Schneier on Security
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
N
News | PayPal Newsroom
Hacker News - Newest:
Hacker News - Newest: "LLM"
H
Hacker News: Front Page
L
LINUX DO - 热门话题
Spread Privacy
Spread Privacy
T
Threat Research - Cisco Blogs
Cloudbric
Cloudbric
V
Vulnerabilities – Threatpost
Hacker News: Ask HN
Hacker News: Ask HN
S
Securelist
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
TaoSecurity Blog
TaoSecurity Blog
NISL@THU
NISL@THU
N
News and Events Feed by Topic
S
Security Affairs
The Last Watchdog
The Last Watchdog
T
Tor Project blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
T
The Exploit Database - CXSecurity.com
Simon Willison's Weblog
Simon Willison's Weblog
P
Palo Alto Networks Blog
AWS News Blog
AWS News Blog
P
Proofpoint News Feed
C
Cisco Blogs
C
Cyber Attacks, Cyber Crime and Cyber Security
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
L
LINUX DO - 最新话题
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
T
Tenable Blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
S
Schneier on Security

博客园 - SharpCJ

在 JetBrains IDE 中接入 OpenCode 并配置自定义模型 理解 Agent 中的 Slash Command:从概念到自定义命令实践 Harness Engineering:把 AI 真正接进工程流程 当 AI 开始写代码,谁来保证它不会翻车? Android 开发者为什么必须掌握 AI 能力?端侧视角下的技术变革 拆穿名词诈骗!用大白话理解晦涩难懂的AI概念 OpenClaw 大结局——接入个人微信 Android 性能分析工具 Perfetto 的基本使用 frp 内网穿透 0.63.0 教程 LangChain入门学习 Ubuntu 下 conda 设置 ComfyUI 基础教程(六) —— 图像的局部重绘 ComfyUI 基础教程(五) —— 应用 IP-Adapter 实现图像风格迁移 ComfyUI 基础教程(四) —— 应用 LoRA 模型控制图像生成特征 ComfyUI 基础教程(三) —— 应用 Controlnet 精准控制图像生成 ComfyUI 基础教程(二) —— Stable Diffusion 文生图基础工作流及模型、常用节点介绍 ComfyUI 基础教程(一) —— 本地安装部署 Stable Diffusion 小白的入坑铺垫 GPT-SoVITS —— 5s 声音样本就可以训练模型,复刻声音的 AI 应用 LM Studio + open-webui 快速本地部署大语言模型 Jetpack Compose(9)——自定义Composable Compose 延迟列表踩过的坑 Jetpack Compose(8)——嵌套滚动 Jetpack Compose(7)——触摸反馈
Dart 基础入门
SharpCJ · 2026-07-26 · via 博客园 - SharpCJ

接下来这个系列的文章,是 Flutter 的学习路线。如果你熟练掌握 Java/Kotlin + Compose,应该不会太难。本文是这个系列的第一篇,作为一个 Dart 语言的入门学习,文中可能会出现一些和 Kotlin 的对比。

一、Dart 语言介绍

Dart 是 Google 开发的一门面向对象语言,也是 Flutter 的基础语言。它在设计上针对客户端开发做了优化,支持 JIT 和 AOT 两种编译模式:开发时用 JIT 实现亚秒级热重载,发布时用 AOT 编译为原生机器码,兼顾开发体验和运行性能。

和 Kotlin 类似,Dart 也是静态类型语言,支持类型推断,并且内置了健全的空安全(Sound Null Safety)。不同之处在于,Dart 是单线程事件循环模型,通过 async/await 和 Future/Stream 处理并发,而不是像 Kotlin 那样依赖协程。

1.1 SDK 安装

如果只进行 Flutter 开发,不需要单独安装 Dart SDK。Flutter SDK 已经包含了完整的 Dart SDK,包括 dart 命令行工具(在 Flutter 的 bin 目录下)。

只有当你要做纯 Dart 后端开发或命令行工具时,才需要单独安装 Dart SDK。我想没有人会这么做吧。

Dart SDK 提供三个发布通道(Release Channel):

  • Stable(稳定版):大约每三个月发布一次,用于生产环境部署。版本号格式为 主版本.次版本.补丁,例如 3.4.0
  • Beta(测试版):大约每月发布一次,用于提前测试你的应用与未来稳定版的兼容性。版本号格式为 主版本.次版本.补丁-预发布号.预发布补丁,例如 3.5.0-123.1.beta
  • Dev(开发版):大约每周发布两次,包含最新的修复和实验性功能。版本号格式为 主版本.次版本.补丁-开发号.开发补丁,例如 3.5.0-123.1.dev

日常开发使用 Stable 通道即可。Beta 和 Dev 通道适合想提前体验新特性或为 Dart 团队反馈问题的开发者。

我这里使用手动安装方式:https://docs.flutter.cn/install/manual

  1. 下载 Flutter SDK bundle
  2. 解压 SDK
  3. 将 Flutter 添加到 PATH
// 终端执行以下命令验证安装
// dart --version

Dart SDK version: 3.12.2 (stable) (Tue Jun 9 01:11:39 2026 -0700) on "linux_x64"

// 或者 flutter --version

Flutter 3.44.8 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 058e0af2c2 (2 天前) • 2026-07-23 10:56:21 -0700
Engine • hash 13ffd72b2f9a5ca4db2a74ea52d5353ec2e8f939 (revision 0cd610717b) (47 hours ago) • 2026-07-23 16:11:34.000Z
Tools • Dart 3.12.2 • DevTools 2.57.0

1.2 Hello World

每个 Dart 应用的入口是一个顶层的 main() 函数,这和 Kotlin 的 fun main() 一样。使用 print() 输出到控制台。

void main() {
  print("Hello, World!");
}

// 输出: Hello, World!

1.3 Dart 语言特点

  • 纯面向对象:所有值都是对象,都继承自 Object,没有 Java 中的基本类型(int、double 本身就是对象)
  • 健全的空安全:类型系统在编译时和运行时都保证空安全,和 Kotlin 的空安全机制类似
  • 类型推断:可以用 var 声明变量,编译器自动推断类型,由于其支持类型推断,因此大多数变量的类型会由它们的初始化内容决定。
var a = 5;  // int
var s = "hello";  // String
  • JIT + AOT 双编译模式:开发时热重载,发布时编译为机器码
  • 单线程事件循环:通过事件队列和微任务队列调度,配合 async/await 处理异步

二、基础语法

在开始之前,先说明,在 Dart 语言中,官方推荐的标准缩进是每行前面空 2 个空格(不是 4 个空格,也不推荐使用 Tab 键)。这一点与 Java/Kotlin 有所区别。

不要手动空格,直接用格式化工具。

Dart 官方自带了名为 dart format 的格式化工具,它会强制把所有缩进统一为 2 个空格。在 IDEA 中,可一键自动对齐:

  • 动一键格式化:在编辑代码时,按下快捷键 Ctrl + Alt + L(Windows/Linux)或 Cmd + Option + L(macOS),IDEA 会瞬间把整篇代码调整为标准的 2 格缩进。

  • 保存时自动格式化:打开设置 Ctrl + Alt + S。搜索 Languages & Frameworks -> Dart。勾选 "Format code on save"(保存时格式化代码)。这样以后你只要按 Ctrl + S 保存,代码就会自动排版。

2.1 常见数据类型

2.1.1 数值类型(int、double)

Dart 的数值类型只有两种:int(64 位整数)和 double(64 位双精度浮点数)。它们的父类型是 numnum 类型的变量可以同时接收 int 和 double 类型。

和 Kotlin 相比,Dart 没有 Byte、Short、Long、Float 这些类型,数值体系更简洁。Dart 中也不存在装箱拆箱的问题,因为 int 和 double 本身就是对象。

void main() {
  // print("Hello, World!");
  print(add(1, 2));
  print(addNum(1.0, 2));
}

int add(int a, int b) {
  return a + b;
}

num addNum(num a, num b) {
  return a + b;
}

// 输出
3
3.0

2.1.2 字符串(String)

Dart 的字符串用法和 Kotlin 非常相似:

  • 单引号和双引号都可以
  • 三个引号(单或双)可以定义多行字符串,类似 Kotlin 的 """
  • 字符串插值用 $变量名${表达式},和 Kotlin 完全一致
  • r 前缀表示原始字符串,不处理转义字符
void testStr() {
  var s1 = "hello, ";
  var s2 = "my name is";
  var name = "sharpcj";

  var question = '''
    What's your name?
    ~1~
    How old are you?
  ''';

  var notHello = r"$s1 is not hello";

  var greet = "$s1 $s2 " + name + "\n" + question + "\n" + notHello;
  print(greet);
}

// 输出
hello,  my name is sharpcj
    What's your name?
    ~1~
    How old are you?
  
$s1 is not hello

2.1.3 布尔类型(bool)

Dart 的布尔类型是 bool,只有 truefalse 两个值。和 Kotlin 的 Boolean 一样。

需要注意,Dart 中只有 bool 类型的值能用于条件判断,这点和 Kotlin 一致,但不同于 JavaScript 的 truthy/falsy 机制。

void testBool() {
  var isTrue = true;
  var isFalse = false;
  print(isTrue);
  print(isFalse);
}

// 输出
true
false

2.1.4 集合类型(List、Set、Map)

Dart 的三种核心集合类型和 Kotlin 对应关系:

Dart Kotlin 字面量
List List [1, 2, 3]
Set Set
Map Map

用法上和 Kotlin 的集合基本一致,支持泛型、扩展方法、展开操作符(...)等。

void testList() {
  var list = [1,2,"Hello"];
  var set = {1, 1, 2, 3};  // 注意:Set 会自动去重
  var map = {"zh":"China", "en":"English"};
  print(list);
  print(set);
  print(map);
}

//输出
[1, 2, Hello]
{1, 2, 3}
{zh: China, en: English}

展开操作符:它的作用是将一个集合中的所有元素“平铺”并插入到另一个集合中。

var list1 = [1, 2, 3];
var list2 = [0, ...list1, 4, 5];
print(list2);

var map1 = {'a': "A", 'b': "B"};
var map2 = {...map1, 'c': 'C'};
print(map2);

// 输出
[0, 1, 2, 3, 4, 5]
{a: A, b: B, c: C}

2.1.5 其他类型(dynamic、Object、void、Never)

Dart Kotlin 说明
Object Any 所有非空类型的根类型
dynamic - 关闭静态类型检查,运行时动态分发
void Unit 表示没有有意义的返回值
Never Nothing 表示永远不会产生值的类型

dynamic 需要特别说明:它和 Kotlin 的 Any? 不同。dynamic 会完全关闭编译器的类型检查,调用任何方法都不会报错(运行时才检查),容易引入隐藏 bug,应尽量避免使用。

2.1.6 记录(Records)

记录(Record)是 Dart 3.0 引入的类型,用于将多个对象打包成一个整体。它有几个特点:

  • 匿名:不需要像 class 那样声明类型名称
  • 不可变:创建后字段不可修改
  • 固定大小:字段数量固定,不能动态增减
  • 结构化类型:两个 Record 的字段类型和字段名(如果有命名字段)相同,就视为同一类型,不需要显式继承

记录的字段分为两类:

  • 位置字段:直接放在括号内,通过 .$1.$2 等内置 getter 访问
  • 命名字段:放在花括号内,通过字段名访问

记录最实用的场景是让函数返回多个值,不需要再为了一组返回值专门定义一个类。

var record = ('first', a: 2, b: true, 'last');

(int, int) swap((int, int) record) {
  var (a, b) = record;
  return (b, a);
}

Dart 的 Records 完美对应了 Kotlin 中的 Pair、Triple 或者是自定义的 解构声明(Destructuring Declarations)。但 Dart 的 Records 比 Kotlin 的 Pair 更强大,因为它不仅支持超过 3 个以上的任意数量字段,还原生支持命名字段。

有几个反直觉的设计需要重点理解:

  1. Records 中的字段分为 位置字段命名字段 。比如:var record = ('first', a: 2, b: true, 'last'); 中的 a: 2b: true 就是命名字段。

在 Dart 中,位置字段(没有名字的字段)和命名字段(大括号里的字段)是两条互不干扰的平行线。不论你把命名字段夹在什么地方,位置字段都会自动忽略它们,并按照自己出现的先后顺序,从左到右依次编号为 $1, $2, $3 ……

  1. Records 索引是从 1 开始的,而不是像数组或者 List 那样从 0 开始。
var record = ('first', a: 2, b: true, 'last');
print(record);
print(record.$1);
print(record.a);
print(record.b);
print(record.$2);

// 输出
(first, last, a: 2, b: true)
first
2
true
last

2.2 变量和常量

2.2.1 变量声明与类型推断

Dart 用 var 声明变量,编译器自动推断类型,这一点和 Kotlin 一样。也可以显式指定类型。

和 Kotlin 的区别:Dart 中 var 声明的变量是可变的(对应 Kotlin 的 var),但没有 val 关键字。不可变变量用 finalconst

// 1. var:类型推断
var a = 'hello'; 
// a = 123; // ❌ 编译报错!因为 a 已经被锁定为 String 类型

// 2. Object:静态父类
Object b = 'hello';
// print(b.length); // ❌ 编译报错!Object 类里没有 length 属性,必须写成 (b as String).length

// 3. dynamic:动态类型
dynamic c = 'hello';
print(c.length); //  编译通过!
c = 123;         //  编译通过!

关于 dynamic 补充几点说明:

dynamic x = 'Hello'; // 初始是 String
print(x.length);     // 编译通过,输出 5

x = 123;             // 成功改变类型为 int
print(x + 10);       // 编译通过,输出 133

// 💥 危险操作:
// 编译完全不报错,但运行时会抛出 NoSuchMethodError 崩溃
// 因为此时 x 已经是数字 123(int 类型)了,而 int 类里面是没有 length 属性的。
x.length; 

dynamic vs Object vs var 的区别

注意: var 是声明变量的关键字,而非类型。

类型/关键字 类型是否可变? 编译期是否检查类型? 核心定义
dynamic 动态类型。彻底关闭静态类型检查。
Object 静态类型。是所有非空类型的基类。只能调用 Object 自身拥有的方法(如 toString()),调用其他方法必须强转。
var 类型推断关键字(非类型)。根据第一次赋值锁定类型,后续无法改变类型。

什么时候该用 dynamic?

虽然 dynamic 灵活,但它破坏了 Dart 的类型安全,极易引发运行时崩溃,并会导致 IDE 失去代码补全(提示)功能。因此,官方建议尽量少用。以下是仅有的几种合理使用场景:

  • 解析 JSON 或未知结构的外部数据:当从网络请求、本地数据库读取数据时,在转化为具体 Model 之前,通常使用 Map<String, dynamic> 来接收。

  • 处理极其泛化的跨类型逻辑:当你编写的某个底层函数确实需要接收任意类型,且内部逻辑需要依赖运行时反射或动态判断时。

2.2.2 final 和 const

这是 Dart 和 Kotlin 的一个重要区别。Kotlin 只有 val 表示引用不可变,而 Dart 区分了两个层级:

  • final:运行时常量,赋值一次后不可变。类似 Kotlin 的 val
  • const:编译时常量,值在编译期就确定。比 final 更严格。

const 不仅用于变量,还可以修饰构造函数,创建编译期常量对象。这在 Flutter 中很常见(比如 const Text('Hello'))。

final a = 1;
const b = 2;

2.2.3 通配符变量(_)

_ 声明的变量是一个占位符,表示"我不需要这个值"。它的值不可访问,但如果有初始化表达式,表达式仍会执行。同一个作用域内可以有多个 _ 声明,不会冲突。

var _ = 1;
var _ = "hello";

这个特性需要 Dart 3.7 及以上版本。常见使用场景是忽略不需要的返回值或解构中的某些字段。

和 Kotlin 对比:Kotlin 中 _ 也有类似用法(如解构声明中忽略组件),Dart 的通配符变量概念一致。

注意:_ 在 Dart 中还有另一个重要含义--以 _ 开头的名称是库级私有的(见 4.7 可见性)。这里说的是单独一个 _ 作为变量名的通配符用法,两者不要混淆。

2.2.4 空安全(Null Safety)

Dart 的空安全机制和 Kotlin 几乎完全一致:

功能 Dart Kotlin
可空类型 String? String?
安全调用 ?. ?.
非空断言 ! !!
空值合并 ?? ?:
延迟初始化 late lateinit

late 关键字和 Kotlin 的 lateinit 类似,表示"这个变量稍后会初始化",但使用前必须保证已赋值,否则运行时报错。

  String? name = "Lucy";
  name = null;
  print(name?.length);
  print(name!.length);  // 抛出异常

具有可空类型的未初始化变量的初始值为 null。即使是数字类型的变量最初也是空值,因为在 Dart 中数字也是对象。

2.3 流程控制

2.3.1 if-else

和大多数语言一样。但注意:Dart 的 if 是语句(statement),不是表达式。Kotlin 中 if 可以作为表达式赋值,Dart 中不行,需要用三元运算符 ? :

bool isFlg = true;
var greet = isFlg ? "Hello" : "Hi"; // 类似 Kotlin 中的 if (condition) {} else {}

2.3.2 for 循环

Dart 支持 for-in 和传统 for 循环,和 Kotlin 类似。但没有 Kotlin 的 0..10 区间语法,区间用 0.until(10)for (var i = 0; i < 10; i++)

var message = StringBuffer('Dart is fun');
for (var i = 0; i < 5; i++) {
message.write('!');
}

// 输出
Dart is fun!!!!!
List<String> list = ["a", "b", "c"];
for (var item in list) {
print(item);
}
list.forEach((item) {
    print(item);
  });

2.3.3 while 和 do-while

和 Kotlin 用法完全一致。

while (!isDone()) {
  doSomething();
}

do {
  printLine();
} while (!atEndOfPage());

break 和 continue

使用 break 来停止循环:

while (true) {
  if (shutDownRequested()) break;
  processIncomingRequests();
}

使用 continue 跳到下一个循环迭代:

for (int i = 0; i < candidates.length; i++) {
  var candidate = candidates[i];
  if (candidate.yearsExperience < 5) {
    continue;
  }
  candidate.interview();
}

如果使用迭代器,则有更简便的写法:

candidates
    .where((c) => c.yearsExperience >= 5)
    .forEach((c) => c.interview());

2.3.4 switch 表达式(Dart 3)

Dart 3 引入了 switch 表达式,语法上类似 Kotlin 的 when,用 => 连接分支,用 _ 表示默认分支:

  var color = "red";
  var fruit = switch(color) {
    "red" => "apple",
    "yellow" => "banana",
    "green" => "pear",
    _ => "orange",  // 不支持 default 语句, 需要使用 _ 来匹配所有其他情况
  };
  print(fruit);

传统的 switch 语句也仍然支持。

var color = "red";
String fruit;

switch (color) {
  case "red":
    fruit = "apple";
    break;
  case "green":
    fruit = "banana";
    break;
  case "blue":
    fruit = "pear";
    break;
  default:  // 这里使用 default 进行匹配。
    fruit = "orange";
}

print(fruit); // apple

2.4 操作符

大部分操作符和 Kotlin 一致,几个需要特别注意的:

  • is / is!:类型判断,对应 Kotlin 的 is / !is
  • ~/:整除运算符(Dart 特有,Kotlin 用 / 对 Int 类型自动整除)
  • ??=:如果变量为 null 则赋值,否则不操作(Dart 特有)
  • ?.:安全调用,和 Kotlin 一样
  • ..:级联操作符,后面单独讲
String? name = null;
var displayName = name ?? 'Guest'; // 如果 name 为 null,返回 'Guest',类似 Kotlin 中的 ?:
print(displayName); // 输出: Guest

在 Dart 中,想实现“如果变量是 null,就给它赋一个默认值;如果不是 null,就保持原样”,可以使用 ??=

int? age = null;
age ??= 18; // 如果 age 为 null,则赋值为 18
print(age); // 输出: 18

2.5 异常处理

Dart 的异常处理用 throwtry-catch-finally

和 Kotlin 的区别:

  • Dart 用 catch 而不是 Kotlin 的 catch (e: ExceptionType),用 on 关键字指定异常类型
  • Dart 的 try-catch 是语句,不是表达式(Kotlin 的 try 可以作为表达式)
  • Dart 中可以 throw 任意对象,不限于 Exception 子类(但不推荐)
// 自定义异常
class AgeInvalidException implements Exception {
  final String message;
  AgeInvalidException(this.message);

  @override
  String toString() => 'AgeInvalidException: $message';
}

void checkAge(int age) {
  if (age < 0) {
    throw AgeInvalidException('年龄不能为负数');
  }
  print('年龄合法: $age');
}

void main() {
  // 1. try-on-catch:按异常类型捕获
  try {
    checkAge(-1);
  } on AgeInvalidException catch (e) {
    print('捕获到自定义异常: $e');
  }

  // 2. try-catch:捕获任意异常
  try {
    checkAge(-5);
  } catch (e) {
    print('捕获到异常: $e');
  }

  // 3. try-catch-finally
  try {
    checkAge(18);
  } catch (e) {
    print('异常: $e');
  } finally {
    print('无论是否异常都会执行');
  }
}

三、函数

3.1 函数声明

Dart 函数用返回类型 + 函数名 + 参数列表声明。支持两种函数体:

  • 代码块函数体:{ ... }
  • 箭头函数体:=> 表达式,类似 Kotlin 的 = 表达式。用来简化只有一行代码的函数或方法。
int add(int a, int b) {
    return a + b;
}

num addNum(num a, num b) => a + b;

胖箭头后面表达式计算出的值,会自动作为函数的返回值,不需要(也不能)写 return。

3.2 位置可选参数与命名参数

这是 Dart 的一个特色,也是和 Kotlin 差异较大的地方。Dart 的参数分为两类:

  • 位置可选参数:用方括号 [ ] 包裹,按位置传递,可省略
  • 命名参数:用花括号 { } 包裹,按名称传递,调用时用 paramName: value 的形式

命名参数默认是可选的,如果必须传递,需要加 required 关键字。

在 Flutter 中,几乎所有 Widget 的构造函数都使用命名参数,这个特性非常重要。

// 定义:用 {} 包裹
void enableFlags({bool? bold, bool? hidden}) {
  print('bold: $bold, hidden: $hidden');
}

void main() {
  // 调用:必须指定参数名,但【顺序可以颠倒】
  enableFlags(bold: true, hidden: false);
  enableFlags(hidden: true, bold: false); // 调换顺序也完全合法
  enableFlags(bold: true);                // 可选:不传 hidden,hidden 默认为 null
}

位置可选参数(Positional Optional Parameters)—— 使用中括号 [ ] 。在定义函数时,把参数包裹在 [ ] 里面。调用时不需要写参数名,但必须严格按照它们声明的先后顺序(位置)来传参。

// 定义:用 [] 包裹。前两个是必填位置参数,后两个是可选位置参数
String say(String from, String msg, [String? device, String? time]) {
  return '$from says $msg' + (device != null ? ' via $device' : '');
}

void main() {
  // 调用 1:只传必填参数
  say('Bob', 'Howdy'); 
  
  // 调用 2:传第三个参数
  say('Bob', 'Howdy', 'iPhone'); 
  
  // ❌ 错误调用:不能跳过第三个直接传第四个!必须按顺序对齐位置
  // say('Bob', 'Howdy', '12:00'); // 编译器会把 '12:00' 当作 device
}

3.3 默认参数值

可选参数(无论是位置可选参数还是命名参数)都可以设置默认值,用 = 连接。和 Kotlin 的默认参数值机制类似。

// 1. 命名参数的默认值
void printUser({String name = '匿名用户', int age = 18}) {
  print('姓名: $name, 年龄: $age');
}

// 2. 位置可选参数的默认值
void setVolume([int volume = 50, int balance = 0]) {
  print('音量: $volume, 平衡: $balance');
}

void main() {
  printUser(); // 不传参,输出: 姓名: 匿名用户, 年龄: 18
  printUser(name: 'Sharpcj'); // 只传一个,输出: 姓名: Sharpcj, 年龄: 18
  
  setVolume(); // 输出: 音量: 50, 平衡: 0
}

默认参数值必须是常量(const),不能在运行时动态计算。不能在默认值里调用函数、创建普通对象

// ❌报错:默认值必须是编译时常量
void logMessage({DateTime time = DateTime.now()}) { ... } 

// ❌报错:不能在默认值里调用普通构造函数创建可变列表
void printTags({List<String> tags = ['Dart', 'Flutter']}) { ... } 

等价解决方案:

// 正确:利用 ??= 运算符在运行时兜底
void logMessage({DateTime? time}) {
  time ??= DateTime.now(); // 如果是 null,则在运行到这一行时获取当前时间
  print('时间: $time');
}

// 正确:使用 const 锁死列表
void printTags({List<String> tags = const ['Dart', 'Flutter']}) { ... }
  • 在开启空安全的 Dart 中:它的默认值就是 null。因此,它的类型必须带有问号(例如 int? age)。
  • 如果不想让它是 null,也不想写默认值,就必须加上 required 关键字强迫调用者必传。

Kotlin 的默认参数值支持运行期动态计算。比如在 Kotlin 中可以直接写 fun log(time: Long = System.currentTimeMillis())。
Dart 出于极致的性能优化考虑(提前优化编译结构),坚决强制要求默认值必须在编译期就能确定。

3.4 匿名函数与闭包

Dart 的匿名函数写法:(参数) { 函数体 }(参数) => 表达式

和 Kotlin 的 lambda 对比:

// Dart
var square = (int x) => x * x;

// Kotlin
// val square = { x: Int -> x * x }

Dart 的匿名函数同样支持闭包,可以捕获外部变量。

闭包是指一个函数能够访问其词法作用域中的变量,即使在该函数被传递到其它作用域执行时也是如此。换句话说,闭包"记住"了它定义时的环境。

和 Kotlin 对比:Kotlin 的 lambda 同样支持闭包,概念一致。区别在于语法形式--Dart 用 (params) { body }(params) => expr,Kotlin 用 { params -> body }

// 匿名函数赋值给变量
var add = (int a, int b) => a + b;
print(add(1, 2)); // 3

// 闭包:捕获外部变量
Function makeAdder(int base) {
  return (int x) => base + x; // 捕获了 base
}

void main() {
  var addTen = makeAdder(10);
  print(addTen(5)); // 15
}

3.5 函数作为一等公民

和 Kotlin 一样,Dart 中函数是一等公民,可以作为参数传递、作为返回值、赋值给变量。

  • 函数类型:(int, String) -> bool 表示接收 int 和 String、返回 bool 的函数类型
  • typedef:为函数类型起别名,类似 Kotlin 的 typealias
  • 函数引用:直接用函数名即可传递,不需要像 Kotlin 那样加 ::
// 1. 函数作为参数传递
void forEach(List<int> list, Function action) {
  for (var item in list) {
    action(item);
  }
}

// 2. typedef 为函数类型起别名
typedef IntOp = int Function(int, int);

int sum(int a, int b) => a + b;

void main() {
  // 3. 函数引用:直接用函数名传递
  forEach([1, 2, 3], print); // 1 2 3

  // 4. 函数类型变量
  IntOp op = sum;
  print(op(2, 3)); // 5
}

高阶函数:把函数作为参数,传入另外一个函数中,看下面的例子:

void cusForEach(List<int> list, Function(int) callback) {
  for (int i = 0; i < list.length; i++) {
    callback(list[i]);
  }
}

// 更优的写法:
// 将 Function(int) 改为 void Function(int)
// 这样明确限制了 callback 函数不需要返回值,类型安全度直接拉满
void cusForEach(List<int> list, void Function(int) callback) {
  for (int i = 0; i < list.length; i++) {
    callback(list[i]);
  }
}

void main() {
  var list = [1, 2, 3];

  cusForEach(list, (int x) => print(x));

  // 优化写法 1:去掉 int 关键字,由 Dart 自动推断 x 是 int
  cusForEach(list, (x) => print(x));
  
  // 优化写法 2(最高级):直接把 print 函数本身作为参数传进去
  cusForEach(list, print); 
}

对比 Kotlin 中的等效写法:

// Kotlin 高阶函数:用 (Int) -> Unit 声明函数类型
fun cusForEach(list: List<Int>, callback: (Int) -> Unit) {
    for (item in list) {
        callback(item)
    }
}

fun main() {
    val list = listOf(1, 2, 3)

    // 写法 1:完整 lambda
    cusForEach(list) { x -> println(x) }

    // 写法 2:it 关键字(Kotlin 特有,单参数时可省略参数名)
    cusForEach(list) { println(it) }

    // 写法 3:函数引用(需要 ::)
    cusForEach(list, ::println)
}

主要差异:

对比点 Dart Kotlin
函数类型语法 void Function(int) (Int) -> Unit
函数引用 直接用函数名 print 需要加 ::,如 ::println
单参数简写 (x) => print(x),省略类型 (x) => ... it 关键字,{ println(it) }
lambda 传参 放在括号内 f(list, (x) => ...) 尾随闭包,放括号外 f(list) { ... }

四、类、接口与对象

4.1 类的定义

4.1.1 构造函数(默认构造函数、命名构造函数、工厂构造函数)

Dart 的构造函数和 Kotlin 有较大区别:

  • 默认构造函数:和类名同名
  • 命名构造函数:ClassName.named(),一个类可以有多个命名构造函数,解决构造函数重载问题(Dart 不支持构造函数重载)
  • this.param 简写:在参数列表中用 this.字段名 直接赋值,类似 Kotlin 的主构造函数属性声明
  • 工厂构造函数:用 factory 关键字修饰,不一定每次都创建新实例(可以实现单例、缓存等)

和 Kotlin 对比:Kotlin 区分主构造函数和次构造函数,Dart 没有这个区分,所有构造函数地位平等。

class Student {
  String name;
  int age;

  // 1. 默认构造函数 + this 简写(类似 Kotlin 主构造函数属性声明)
  Student(this.name, this.age);

  // 2. 命名构造函数:解决 Dart 不支持构造函数重载的问题
  Student.named(String name) : this(name, 18);
  Student.fromString(String info) : this(info.split('-')[0], int.parse(info.split('-')[1]));

  // 3. 工厂构造函数:不一定创建新实例
  factory Student.guest() {
    return Student('Guest', 0);
  }

  @override
  String toString() => 'Student(name: $name, age: $age)';
}

void main() {
  print(Student('Lucy', 18));            // Student(name: Lucy, age: 18)
  print(Student.named('Jim'));           // Student(name: Jim, age: 18)
  print(Student.fromString('Tom-20'));   // Student(name: Tom, age: 20)
  print(Student.guest());               // Student(name: Guest, age: 0)
}

4.1.2 成员变量与方法

Dart 的成员变量(也叫实例变量)和 Kotlin 的属性类似,声明时可以初始化,也可以在构造函数中赋值。

和 Kotlin 的区别:Dart 没有主构造函数的属性声明语法(var name: String 在构造函数参数中),但用 this.name 可以达到类似效果。

class Student {
  String name;
  int age;
  bool _isActive = true; // 私有成员,库级可见(见 4.7 可见性)

  Student(this.name, this.age);

  // 成员方法
  void study(String subject) {
    print('$name 正在学习 $subject');
  }

  // 静态成员:属于类,不属于实例
  static String school = 'No.1 School';

  static void printSchool() {
    print('学校: $school');
  }
}

void main() {
  var s = Student('Lucy', 18);
  s.study('Math');          // Lucy 正在学习 Math
  print(s._isActive);       // true(同文件内可访问)

  // 静态成员通过类名访问
  print(Student.school);    // No.1 School
  Student.printSchool();    // 学校: No.1 School
}

4.1.3 Getter 和 Setter

Dart 中每个非 final 的成员变量都隐式生成了 getter 和 setter。也可以用 getset 关键字自定义:

// 自定义 getter
String get fullName => '$firstName $lastName';

和 Kotlin 的自定义 getter/setter 概念一致。

class Rectangle {
  double width;
  double height;

  Rectangle(this.width, this.height);

  // 自定义 getter:计算属性
  double get area => width * height;

  // 自定义 setter:可以在赋值时加校验
  set widthSafe(double w) {
    if (w < 0) throw ArgumentError('width 不能为负数');
    width = w;
  }
}

void main() {
  var r = Rectangle(3, 4);
  print(r.area); // 12.0(像访问属性一样调用 getter)

  r.widthSafe = 5; // 调用 setter
  print(r.area);    // 20.0
}

4.2 继承

Dart 使用 extends 关键字实现单继承,用 super 调用父类。和 Kotlin 的区别:

  • Dart 的类默认可以被继承,不需要像 Kotlin 那样加 open 关键字
  • 重写方法不需要 override 关键字(但建议加 @override 注解,编译器会检查)
  • Dart 3 引入了类修饰符(basefinalsealedinterface),可以限制类的继承和实现权限
class Person {
  String name;
  int age;

  Person(this.name, this.age);

  void describe() {
    print('$name, $age 岁');
  }
}

class Student extends Person {
  String school;

  // super 调用父类构造函数
  Student(String name, int age, this.school) : super(name, age);

  @override
  void describe() {
    super.describe(); // 调用父类方法
    print('就读于 $school');
  }
}

void main() {
  var s = Student('Lucy', 18, 'No.1 School');
  s.describe();
  // Lucy, 18 岁
  // 就读于 No.1 School
}

4.3 抽象类与隐式接口

Dart 的接口机制和 Kotlin 有本质区别:

  • Dart 没有 interface 关键字用于声明接口类型
  • 每个 class 都隐式定义了一个接口,包含该类的所有实例成员
  • 任何类都可以用 implements 关键字实现另一个类的接口(不继承实现,只承诺 API)
  • abstract class 可以包含抽象方法(没有方法体的方法),用于被继承
// 1. 抽象类:可以包含抽象方法,用 extends 继承
abstract class Animal {
  String name;
  Animal(this.name);

  void breathe(); // 抽象方法,没有方法体,子类必须实现

  void eat() {
    print('$name 正在吃东西'); // 具体方法,子类可以直接用
  }
}

class Dog extends Animal {
  Dog(String name) : super(name);

  @override
  void breathe() {
    print('$name 用肺呼吸');
  }
}

// 2. 隐式接口:每个类都隐式定义了接口
// 用 implements 实现接口,不继承实现,必须自己实现所有成员
class Robot implements Animal {
  @override
  String name;

  Robot(this.name);

  @override
  void breathe() => print('$name 不需要呼吸');

  @override
  void eat() => print('$name 充电中');
}

void main() {
  Dog('旺财').breathe(); // 旺财 用肺呼吸
  Dog('旺财').eat();     // 旺财 正在吃东西(继承自父类)

  Robot('R2').breathe(); // R2 不需要呼吸
  Robot('R2').eat();     // R2 充电中
}

注意:Dart 中并没有也不需要(因为隐式接口特性) interface 关键字。

使用 implements, Dart 中的子类必须实现所有的成员变量和成员方法。

4.4 Mixin

Mixin 是一种在多个类层次结构中复用代码的机制。Dart 用 mixin 关键字声明,用 with 关键字混入。

和 Kotlin 对比:Kotlin 没有独立的 mixin 概念,但接口的默认实现可以起到类似作用。Dart 的 Mixin 更显式、更灵活,可以限制只能 mixin 到特定类型的子类。

4.4.1 声明一个 Mixin

// 声明一个 Mixin
mixin Singer {
  void sing() => print('正在唱歌');
}
  • 怎么理解:mixin 类似于一个“技能包”或“插件”。它里面写好了具体的方法实现(sing()),但是它不能被实例化(你不能写 var s = Singer())。
  • 特点:任何类只要想拥有“唱歌”这个技能,都可以直接把它“插”进来复用,没有任何门槛。

4.4.2 限制只能被特定子类 mixin

// 用 on 限制只能被特定类型的子类 mixin
mixin Flyer on Animal {
  void fly() => print('正在飞');
}
  • 怎么理解:这里的 on 关键字 是一种类型安全锁。它表示:Flyer 这个会飞的技能包,只允许给 Animal 或者 Animal 的子类使用。
  • 为什么要这样做:
    • 防止滥用:比如你不想让一个汽车类(class Car)或者石头类(class Stone)去混入 Flyer 技能,就可以用 on Animal 锁死它。
    • 能力继承:在 Flyer 内部,由于它知道自己一定会被用在 Animal 身上,因此它可以直接调用 Animal 类里的公开属性和方法(比如可以直接在 fly() 里面使用 Animal 的 name 属性)。

4.4.3 使用 Mixin

// 用 with 混入多个 Mixin
class Bird extends Animal with Singer, Flyer {
  Bird(String name) : super(name);

  @override
  void breathe() => print('$name 用肺呼吸');
}
  • extends Animal:Bird 亲生父亲是 Animal,它满足了 Flyer on Animal 的硬性条件。
  • with Singer, Flyer:Bird 同时外挂了两个技能包。它不仅能继承父类,还能瞬间同时复用 Singer 的 sing() 和 Flyer 的 fly() 的代码。
  • : super(name):因为父类 Animal 的构造函数需要传 name,子类必须通过 super 把参数递交上去。

4.4.4 多重混入时的“大招覆盖”顺序(线性化机制)

实际开发中有一个非常重要的底层逻辑。如果多个 Mixin 包含了同名的方法,那么最终调用的是谁的?

// 1. 基类(亲爹)
class Fighter {
  void attack() {
    print('👊 拳头攻击:造成 10 点伤害');
  }
}

// 2. 技能包 A:倚天剑
mixin Sword {
  void attack() {
    super.attack(); // 这里的 super 指向它左边的类
    print('⚔️ 倚天剑加成:追加 50 点割裂伤害');
  }
}

// 3. 技能包 B:屠龙刀
mixin Blade {
  void attack() {
    super.attack(); // 这里的 super 同样指向它左边的类
    print('🪓 屠龙刀加成:追加 100 点暴击伤害');
  }
}

// 4. 玩家类:继承 Fighter,同时外挂 Sword 和 Blade
// 🚨 注意这里的顺序:Fighter -> Sword -> Blade
class Player extends Fighter with Sword, Blade {
  // 此时我们不重写 attack,直接看它会如何执行
}

void main() {
  var p = Player();
  p.attack(); 


// 输出
👊 拳头攻击:造成 10 点伤害
⚔️ 倚天剑加成:追加 50 点割裂伤害
🪓 屠龙刀加成:追加 100 点暴击伤害
}

在 Dart 中,多重混入(Multiple Mixins)的执行顺序遵循“后来居上”原则(在底层被称为线性化机制)。

简单来说,当多个 Mixin 中存在同名方法时,写在最右边的(最后的)Mixin 会覆盖左边的,而子类自身重写的方法拥有最高优先级。

为什么最右边的 Blade 的方法执行了,而且 super.attack() 居然把左边所有人的方法都串联起来执行了一遍?

这是因为 Dart 在编译时,会把极其复杂的网状继承关系,从左到右重组拉平为一条单向的线性链条:

【底层实际继承链】
Fighter (基类)  <--  Sword (左)  <--  Blade (右)  <--  Player (当前类)

当我们调用 p.attack() 时,程序是这样流转的:

  1. 第一步:程序先去 Player 找 attack()。发现 Player 自己没写,于是顺着链条向左找,找到了最右边的 Mixin ── Blade。
  2. 第二步:进入 Blade 的 attack(),第一行遇到了 super.attack()。
    • 核心关键:在 Mixin 中,super 不是指亲爹 Fighter,而是指它在链条中左边紧挨着的那个类。所以这里的 super 指向了 Sword。
  3. 第三步:程序被推入 Sword 的 attack(),第一行又遇到了 super.attack()。它左边紧挨着的是 Fighter。
  4. 第四步:程序进入 Fighter,打印了 👊 拳头攻击...。Fighter 执行完毕。
  5. 第五步:程序弹回 Sword,继续执行下一行,打印了 ⚔️ 倚天剑加成...。Sword 执行完毕。
  6. 第六步:程序弹回 Blade,继续执行下一行,打印了 🪓 屠龙刀加成...。整个链条圆满结束。
p.attack()
  → Blade.attack()          (第1层)
      → super.attack()
          → Sword.attack()   (第2层)
              → super.attack()
                  → Fighter.attack()   (第3层,到底了)
                  print 👊 拳头攻击
              print ⚔️ 倚天剑加成      (第2层回来,继续执行)
      print 🪓 屠龙刀加成              (第1层回来,继续执行)

总结

  • 如果 Mixin 里面没有写 super.xxx(),右边的同名方法会直接把左边的彻底顶替(覆盖)掉,左边的代码根本不会执行。
  • 如果 Mixin 里面写了 super.xxx(),它就会像一把“套娃大锁”,从右往左一路串联触发。

在 Dart 中最流行的网络请求库 Dio 拦截器的底层核心实现原理,就是利用了这个链式调用的思想。

4.5 枚举类型

Dart 的枚举用 enum 关键字声明,基本用法和 Kotlin 类似。Dart 2.17 之后支持增强枚举,可以给枚举值添加成员变量和方法,和 Kotlin 的 enum class 能力接近。

简单枚举

// 1. 基本枚举
enum Color { red, green, blue }

// 2. 增强枚举(Dart 2.17+):带成员变量和方法
enum Status {
  loading('加载中', 0),
  success('成功', 1),
  error('失败', -1);

  final String label;
  final int code;

  const Status(this.label, this.code);

  bool get isError => this == Status.error; // 自定义 getter 方法
}

void main() {
  print(Color.red);         // Color.red
  print(Color.values);      // [Color.red, Color.green, Color.blue]

  print(Status.success.label); // 成功
  print(Status.success.code);  // 1
  print(Status.error.isError); // true
}

增强枚举的构造函数限制,构造函数声明:有两个必须要死守的底层规则:

  1. 必须是 const:枚举的构造函数必须带有 const 关键字。
  2. 变量必须是 final:所有的成员变量(如 label、code)都必须被声明为 final(只读)。

原因:枚举在编译期就已经是常量对象了,在运行期绝对不允许动态修改枚举实例内部的数据。

4.6 扩展方法(Extension)

Dart 的扩展方法用 extension 关键字声明,可以给现有类"添加"方法,和 Kotlin 的扩展函数概念一致。

// 给 String 扩展一个方法
extension StringExt on String {
  String repeat(int times) {
    var result = '';
    for (var i = 0; i < times; i++) {
      result += this;
    }
    return result;
  }
}

void main() {
  print('abc'.repeat(3)); // abcabcabc
}

匿名扩展方法

如果扩展方法只在当前文件内部临时用一下,不需要被其他文件 import,可以把扩展的名字(StringExt)直接省去

// 省略名字,直接 extension on ...
extension on String {
  String get greet => 'Hello, $this!';
}

void main() {
  print('Sharpcj'.greet); // 输出: Hello, Sharpcj!
}

显式调用机制
当遇到重名冲突时:显式调用机制。引用的两个第三方库同时给 String 扩展了一个同名的 repeat 方法,编译器会报冲突错误。

// 假设有另一个扩展叫 MyStringExt,也包含 repeat 方法
void main() {
  // 显式指定使用 StringExt 里的 repeat
  print(StringExt('abc').repeat(3)); 
}

4.7 可见性

Dart 的可见性控制和 Kotlin 差异较大:

  • Dart 没有 publicprivateprotectedinternal 这些修饰符
  • 默认所有成员都是公开的
  • _ 开头的名称(变量、方法、类)是库级私有的(library private),只在当前文件(library)内可见
  • 没有包级可见性的概念

和 Kotlin 对比:Kotlin 的 private 是类级私有,Dart 的 _ 是文件级私有。Kotlin 的 internal 是模块级可见,Dart 没有对应概念。

// --- student.dart ---
class Student {
  String name;       // 公开,外部可访问
  int _age;          // 私有,仅当前文件(library)内可访问
  String _id = '';   // 私有

  Student(this.name, this._age);

  void _validate() { // 私有方法
    if (_age < 0) throw ArgumentError('年龄不能为负');
  }

  // 公开方法内部可以访问私有成员
  void printInfo() {
    _validate();
    print('姓名: $name, 年龄: $_age');
  }
}

void main() {
  var s = Student('Lucy', 18);
  print(s.name);       // Lucy(公开,可访问)
  // print(s._age);    // 编译错误:_age 是私有的
  s.printInfo();       // 姓名: Lucy, 年龄: 18
}

五、异步编程

Dart 的异步编程核心是 Future,配合 async/await 关键字使用。这和 Kotlin 的协程 + suspend 在概念上类似,但底层机制不同:

  • Kotlin 用协程,可以挂起不阻塞
  • Dart 是单线程事件循环,async/await 是对 Future 的语法糖,不会创建新线程

5.1 同步阻塞与单线程模型

使用同步的方法读一个文件:

const String filename = 'with_keys.json';

String _readFileSync(String filename) {
  final file = File(filename);
  final contents = file.readAsStringSync();
  return contents.trim();
}

void main() {
  // Read some data.
  String filename = 'with_keys.json';
  final fileData = _readFileSync(filename);
  final jsonData = jsonDecode(fileData);

  // Use that data.
  print('Number of JSON keys: ${jsonData.length}');
}

注意,同步方法中在读文件的过程中,程序会产生卡死(阻塞)现象。

  1. 代码解析:为什么会卡死?(凶手是 readAsStringSync)
    代码中调用了 file.readAsStringSync()。这个带有 Sync 后缀的方法是 Dart 的同步阻塞方法。当程序运行到这一行时,它会强行霸占 Dart 的单线程,命令它:“现在所有人都不准动,全力把磁盘上的文件读进内存,读不完不准执行下一行!” 。如果是 Flutter 应用程序,在这期间,屏幕渲染会被瞬间切断,UI 界面彻底冻结,用户无法进行任何点击。

  2. 深度思考:既然会卡死,为什么 Dart 还要提供 Sync 同步方法?
    既然同步会卡死,官方为什么不直接删掉 readAsStringSync 呢?因为它在特定的场景下非常有用:

  • 应用启动时读取基础配置:比如你的 APP 一打开,必须先拿到本地的 config.json 才知道要连接哪个服务器。这时候界面还没渲染出来,稍微卡个几毫秒让它同步读完,反而比写一堆异步回调更安全、更不容易出错。
  • 命令行工具(CLI 脚本):如果是用 Dart 写一个自动化打包脚本、文件重命名工具等,它没有 UI 界面。这时候卡死主线程就是它的正常业务流程,用同步方法写起来最直观。

终极铁律:
在写 Flutter UI 程序时,只要文件大于几 KB,或者你无法预知文件有多大(比如用户相册里的图片、本地数据库),请 100% 拒绝 Sync 方法,一律使用 async/await 异步版本。

5.2 Future 与 async/await

Future<T> 表示一个将在未来完成的异步操作,返回类型为 T。async 标记函数为异步函数,await 等待 Future 完成。

Future<String> _readFileAsync() async {
  final file = File(filename);
  final contents = await file.readAsString();
  return contents.trim();
}

// async/await 简化异步代码
void main() async {
  // Read some data.
  String filename = 'with_keys.json';
  final fileData = await _readFileAsync(filename);
  final jsonData = jsonDecode(fileData);

  // Use that data.
  print('Number of JSON keys: ${jsonData.length}');
}

async 和 await 关键字提供了一种声明式的方式来定义异步函数并使用它们的结果。

await 关键字仅适用于函数体前有 async 的函数。

5.3 Stream

Stream<T> 是 Dart 中表示异步事件序列的类型,类似 Kotlin 的 Flow。可以把它理解为"异步版的 Iterable"。

  • async* 标记的生成器函数可以 yield 数据产生 Stream
  • await for 消费 Stream 中的事件
  • 在 Flutter 中,Stream 常用于状态管理和事件处理
// 用 async* + yield 产生 Stream
Stream<int> countDown(int from) async* {
  while (from > 0) {
    await Future.delayed(Duration(seconds: 1));
    yield from--;
  }
}

// 用 await for 消费 Stream
Future<void> main() async {
  await for (var n in countDown(3)) {
    print(n);
  }
}

// 输出(每秒一个):
// 3
// 2
// 1

对比 Kotlin 协程 flow 来理解。

suspend fun countDownFlow(from: Int): Flow<Int> = flow {
    // 形参 from 是 val 无法修改,用一个可变 var 承接
    var current = from 
    while(current > 0) {
        delay(1000)
        emit(current--)
    }
}

fun main() = runBlocking {
    countDownFlow(3).collect {
        print(it)
    }
}

Dart 中,Stream 类似 Kotlin 中的 Flow, 是一个冷流,所以它也具备冷流的三大特征:

  • 懒加载(Lazy):只有下流有消费者时, Stream 中的逻辑才会执行。在 Dart 中你不写 await for(或 .listen()),里面的代码就不会执行。
  • 不存储状态(Stateless):流本身不保存数据,它只是数据的搬运工。
  • 一对一服务(Unicast):每一次有新的消费者收听,冷流都会重新、独立地从头到尾执行一遍内部的生产者代码。

熟悉 Kotlin 的小伙伴,可以在这里看一个对比:

概念维度 Dart (Stream) 机制 Kotlin (Flow) 机制
冷流生成器 async* 关键字 flow { ... } 构建器
向外发射数据 yield value emit(value)
消费/收集冷流 await for (var x in stream) stream.collect { x -> ... }
转化为热流 .asBroadcastStream() .shareIn().stateIn()

关于 Dart 中的热流,以及更多异步的知识,下一篇文章再详细介绍。

六、其它特性与总结

6.1 泛型

Dart 支持泛型,语法和 Kotlin 类似:List<int>Map<String, int>

和 Kotlin 的区别:

  • Dart 没有声明点变型(no in/out 关键字),泛型默认是不变的
  • Dart 没有具体化泛型(no reified 关键字),泛型类型信息在运行时部分保留
// 泛型类
class Stack<T> {
  final List<T> _items = [];

  void push(T item) => _items.add(item);

  T pop() => _items.removeLast();

  bool get isEmpty => _items.isEmpty;
}

// 泛型方法
T firstOf<T>(List<T> list) => list.first;

void main() {
  var stack = Stack<String>();
  stack.push('Hello');
  stack.push('World');
  print(stack.pop()); // World

  print(firstOf<int>([1, 2, 3])); // 1
}

在 Dart 的类型系统中,关于泛型型变(Variance)的支持情况可以用一句话概括:Dart 默认且强制所有的泛型参数都是“隐式协变”的,它在声明期不支持原生的逆变和不型变。

默认:无处不在的“协变(Covariance)”,在 Dart 中,如果 Dog 继承自 Animal,那么默认情况下,List 就会自动被视为 List 的子类型。这种“子类集随之成为父类集”的现象就是协变。

class Animal {}
class Dog extends Animal {}

void main() {
  //  完全合法!由于协变,List<Dog> 可以直接赋值给 List<Animal>
  List<Animal> animals = <Dog>[]; 
}

隐式协变虽然让代码写起来极其灵活(不需要像 Kotlin 那样写 List<out Animal>),但它破坏了编译期的绝对安全。

class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}

void main() {
  List<Animal> animals = <Dog>[]; 
  
  // ❌ 编译期完全不报错!因为 animals 静态类型是 List<Animal>,塞入 Cat 很合理
  // 💥 但是运行到这一行会直接抛出 TypeError 崩溃!
  // 因为底层的真实内存是一条死死限制了只能装 Dog 的 List<Dog> 数组。
  animals.add(Cat()); 
}

6.2 库与导入

Dart 用 import 导入库,和 Kotlin 的 import 类似,但语法有差异:

  • import 'dart:core':导入核心库(自动导入,不需要手动写)
  • import 'package:foo/foo.dart':导入第三方包
  • import 'src/utils.dart':导入相对路径文件
  • show / hide:控制只导入或排除特定成员
  • as:给库起别名

Dart 中每个 .dart 文件默认就是一个库(library),也可以用 librarypart 关键字组合多个文件为一个库。

// --- my_utils.dart ---
String greet(String name) => 'Hello, $name';

// --- main.dart ---
// 1. 导入相对路径文件
import 'my_utils.dart';

// 2. 导入第三方包
import 'package:http/http.dart' as http;

// 3. show / hide 控制导入成员
import 'dart:math' show pi, sqrt;  // 只导入 pi 和 sqrt
// import 'dart:math' hide Random;  // 导入除 Random 外的所有成员

void main() {
  print(greet('Lucy'));      // Hello, Lucy
  print(pi);                 // 3.141592653589793
  print(sqrt(16));           // 4.0
}

6.3 级联操作符(..)

级联操作符是 Dart 特有的语法,允许对同一个对象连续执行多个操作,不需要重复写对象名:

// 级联操作符示例
var paint = Paint()
  ..color = Colors.black
  ..strokeCap = StrokeCap.round
  ..strokeWidth = 5.0;

Kotlin 中没有这个操作符,通常用 apply 函数实现类似效果。在 Flutter 中构造 Widget 树时非常常用。

6.4 总结

本文覆盖了 Dart 语言的核心知识点:

  • 基础语法:数据类型、变量常量、流程控制、操作符
  • 函数:声明方式、命名参数(Dart 特色)、匿名函数、函数作为一等公民
  • 面向对象:类与构造函数、继承、隐式接口、Mixins、扩展方法
  • 异步编程:Future/async-await、Stream
  • 其它特性:泛型、库系统、级联操作符

对于有 Kotlin 基础的开发者来说,Dart 的学习曲线比较平缓。核心差异集中在几个点:命名参数语法、隐式接口机制、final/const 的区分、单线程异步模型、以及级联操作符。这些也是后续 Flutter 开发中会频繁用到的特性。

后续文章将继续 Dart 和 Flutter 的学习。