目录
- [一、Dart 语言介绍](#一、Dart 语言介绍)
- [1.1 SDK 安装](#1.1 SDK 安装)
- [1.2 Hello World](#1.2 Hello World)
- [1.3 Dart 语言特点](#1.3 Dart 语言特点)
- 二、基础语法
- [2.1 常见数据类型](#2.1 常见数据类型)
- [2.1.1 数值类型(int、double)](#2.1.1 数值类型(int、double))
- [2.1.2 字符串(String)](#2.1.2 字符串(String))
- [2.1.3 布尔类型(bool)](#2.1.3 布尔类型(bool))
- [2.1.4 集合类型(List、Set、Map)](#2.1.4 集合类型(List、Set、Map))
- [2.1.5 其他类型(dynamic、Object、void、Never)](#2.1.5 其他类型(dynamic、Object、void、Never))
- [2.1.6 记录(Records)](#2.1.6 记录(Records))
- [2.2 变量和常量](#2.2 变量和常量)
- [2.2.1 变量声明与类型推断](#2.2.1 变量声明与类型推断)
- [2.2.2 final 和 const](#2.2.2 final 和 const)
- [2.2.3 通配符变量()](#2.2.3 通配符变量())
- [2.2.4 空安全(Null Safety)](#2.2.4 空安全(Null Safety))
- [2.3 流程控制](#2.3 流程控制)
- [2.3.1 if-else](#2.3.1 if-else)
- [2.3.2 for 循环](#2.3.2 for 循环)
- [2.3.3 while 和 do-while](#2.3.3 while 和 do-while)
- [2.3.4 switch 表达式(Dart 3)](#2.3.4 switch 表达式(Dart 3))
- [2.4 操作符](#2.4 操作符)
- [2.5 异常处理](#2.5 异常处理)
- [2.1 常见数据类型](#2.1 常见数据类型)
- 三、函数
- [3.1 函数声明](#3.1 函数声明)
- [3.2 位置可选参数与命名参数](#3.2 位置可选参数与命名参数)
- [3.3 默认参数值](#3.3 默认参数值)
- [3.4 匿名函数与闭包](#3.4 匿名函数与闭包)
- [3.5 函数作为一等公民](#3.5 函数作为一等公民)
- 四、类、接口与对象
- [4.1 类的定义](#4.1 类的定义)
- [4.1.1 构造函数(默认构造函数、命名构造函数、工厂构造函数)](#4.1.1 构造函数(默认构造函数、命名构造函数、工厂构造函数))
- [4.1.2 成员变量与方法](#4.1.2 成员变量与方法)
- [4.1.3 Getter 和 Setter](#4.1.3 Getter 和 Setter)
- [4.2 继承](#4.2 继承)
- [4.3 抽象类与隐式接口](#4.3 抽象类与隐式接口)
- [4.4 Mixin](#4.4 Mixin)
- [4.4.1 声明一个 Mixin](#4.4.1 声明一个 Mixin)
- [4.4.2 限制只能被特定子类 mixin](#4.4.2 限制只能被特定子类 mixin)
- [4.4.3 使用 Mixin](#4.4.3 使用 Mixin)
- [4.4.4 多重混入时的"大招覆盖"顺序(线性化机制)](#4.4.4 多重混入时的“大招覆盖”顺序(线性化机制))
- [4.5 枚举类型](#4.5 枚举类型)
- [4.6 扩展方法(Extension)](#4.6 扩展方法(Extension))
- [4.7 可见性](#4.7 可见性)
- [4.1 类的定义](#4.1 类的定义)
- 五、异步编程
- [5.1 同步阻塞与单线程模型](#5.1 同步阻塞与单线程模型)
- [5.2 Future 与 async/await](#5.2 Future 与 async/await)
- [5.3 Stream](#5.3 Stream)
- 六、其它特性与总结
- [6.1 泛型](#6.1 泛型)
- [6.2 库与导入](#6.2 库与导入)
- [6.3 级联操作符(..)](#6.3 级联操作符(..))
- [6.4 总结](#6.4 总结)
接下来这个系列的文章,是 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
- 下载 Flutter SDK bundle
- 解压 SDK
- 将 Flutter 添加到 PATH
dart
// 终端执行以下命令验证安装
// 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() 输出到控制台。
dart
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 位双精度浮点数)。它们的父类型是 num。num 类型的变量可以同时接收 int 和 double 类型。
和 Kotlin 相比,Dart 没有 Byte、Short、Long、Float 这些类型,数值体系更简洁。Dart 中也不存在装箱拆箱的问题,因为 int 和 double 本身就是对象。
dart
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前缀表示原始字符串,不处理转义字符
dart
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,只有 true 和 false 两个值。和 Kotlin 的 Boolean 一样。
需要注意,Dart 中只有 bool 类型的值能用于条件判断,这点和 Kotlin 一致,但不同于 JavaScript 的 truthy/falsy 机制。
dart
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 的集合基本一致,支持泛型、扩展方法、展开操作符(...)等。
dart
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}
展开操作符:它的作用是将一个集合中的所有元素"平铺"并插入到另一个集合中。
dart
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 访问 - 命名字段:放在花括号内,通过字段名访问
记录最实用的场景是让函数返回多个值,不需要再为了一组返回值专门定义一个类。
dart
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 个以上的任意数量字段,还原生支持命名字段。
有几个反直觉的设计需要重点理解:
- Records 中的字段分为
位置字段和命名字段。比如:var record = ('first', a: 2, b: true, 'last');中的a: 2和b: true就是命名字段。
在 Dart 中,位置字段(没有名字的字段)和命名字段(大括号里的字段)是两条互不干扰的平行线。不论你把命名字段夹在什么地方,位置字段都会自动忽略它们,并按照自己出现的先后顺序,从左到右依次编号为 $1, $2, $3 ......
-
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 关键字。不可变变量用 final 或 const。
dart
// 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'))。
dart
final a = 1;
const b = 2;
2.2.3 通配符变量(_)
用 _ 声明的变量是一个占位符,表示"我不需要这个值"。它的值不可访问,但如果有初始化表达式,表达式仍会执行。同一个作用域内可以有多个 _ 声明,不会冲突。
dart
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 类似,表示"这个变量稍后会初始化",但使用前必须保证已赋值,否则运行时报错。
dart
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 中不行,需要用三元运算符 ? :。
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++)。
dart
var message = StringBuffer('Dart is fun');
for (var i = 0; i < 5; i++) {
message.write('!');
}
// 输出
Dart is fun!!!!!
dart
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 用法完全一致。
dart
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,用 => 连接分支,用 _ 表示默认分支:
dart
var color = "red";
var fruit = switch(color) {
"red" => "apple",
"yellow" => "banana",
"green" => "pear",
_ => "orange", // 不支持 default 语句, 需要使用 _ 来匹配所有其他情况
};
print(fruit);
传统的 switch 语句也仍然支持。
dart
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 一样..:级联操作符,后面单独讲
dart
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 的异常处理用 throw、try-catch-finally。
和 Kotlin 的区别:
- Dart 用
catch而不是 Kotlin 的catch (e: ExceptionType),用on关键字指定异常类型 - Dart 的
try-catch是语句,不是表达式(Kotlin 的 try 可以作为表达式) - Dart 中可以 throw 任意对象,不限于 Exception 子类(但不推荐)
dart
// 自定义异常
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 的= 表达式。用来简化只有一行代码的函数或方法。
dart
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 的构造函数都使用命名参数,这个特性非常重要。
dart
// 定义:用 {} 包裹
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 的默认参数值机制类似。
dart
// 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
// 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 }。
dart
// 匿名函数赋值给变量
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 那样加
::
dart
// 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
// 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 没有这个区分,所有构造函数地位平等。
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 可以达到类似效果。
dart
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。也可以用 get 和 set 关键字自定义:
dart
// 自定义 getter
String get fullName => '$firstName $lastName';
和 Kotlin 的自定义 getter/setter 概念一致。
dart
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 引入了类修饰符(
base、final、sealed、interface),可以限制类的继承和实现权限
dart
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可以包含抽象方法(没有方法体的方法),用于被继承
dart
// 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
dart
// 声明一个 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() 时,程序是这样流转的:
-
第一步:程序先去 Player 找 attack()。发现 Player 自己没写,于是顺着链条向左找,找到了最右边的 Mixin ── Blade。
-
第二步:进入 Blade 的 attack(),第一行遇到了 super.attack()。
- 核心关键:在 Mixin 中,super 不是指亲爹 Fighter,而是指它在链条中左边紧挨着的那个类。所以这里的 super 指向了 Sword。
-
第三步:程序被推入 Sword 的 attack(),第一行又遇到了 super.attack()。它左边紧挨着的是 Fighter。
-
第四步:程序进入 Fighter,打印了 👊 拳头攻击...。Fighter 执行完毕。
-
第五步:程序弹回 Sword,继续执行下一行,打印了 ⚔️ 倚天剑加成...。Sword 执行完毕。
-
第六步:程序弹回 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 能力接近。
简单枚举
dart
// 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
}
增强枚举的构造函数限制,构造函数声明:有两个必须要死守的底层规则:
- 必须是 const:枚举的构造函数必须带有 const 关键字。
- 变量必须是 final:所有的成员变量(如 label、code)都必须被声明为 final(只读)。
原因:枚举在编译期就已经是常量对象了,在运行期绝对不允许动态修改枚举实例内部的数据。
4.6 扩展方法(Extension)
Dart 的扩展方法用 extension 关键字声明,可以给现有类"添加"方法,和 Kotlin 的扩展函数概念一致。
dart
// 给 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 没有
public、private、protected、internal这些修饰符 - 默认所有成员都是公开的
- 以
_开头的名称(变量、方法、类)是库级私有的(library private),只在当前文件(library)内可见 - 没有包级可见性的概念
和 Kotlin 对比:Kotlin 的 private 是类级私有,Dart 的 _ 是文件级私有。Kotlin 的 internal 是模块级可见,Dart 没有对应概念。
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}');
}
注意,同步方法中在读文件的过程中,程序会产生卡死(阻塞)现象。
-
代码解析:为什么会卡死?(凶手是 readAsStringSync)
代码中调用了 file.readAsStringSync()。这个带有 Sync 后缀的方法是 Dart 的同步阻塞方法。当程序运行到这一行时,它会强行霸占 Dart 的单线程,命令它:"现在所有人都不准动,全力把磁盘上的文件读进内存,读不完不准执行下一行!" 。如果是 Flutter 应用程序,在这期间,屏幕渲染会被瞬间切断,UI 界面彻底冻结,用户无法进行任何点击。
-
深度思考:既然会卡死,为什么 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 完成。
dart
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 常用于状态管理和事件处理
dart
// 用 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 来理解。
Kotlin
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关键字),泛型类型信息在运行时部分保留
dart
// 泛型类
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),也可以用 library 和 part 关键字组合多个文件为一个库。
dart
// --- 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 特有的语法,允许对同一个对象连续执行多个操作,不需要重复写对象名:
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 的学习。