Libraries & imports(库与导入)
Dart 使用库(library)组织代码和控制名称可见性,使用 import 引入其他库公开的 API。每个 Dart 文件本身都属于一个库,即使文件中没有写 library 指令;多数应用代码只需要合理拆分文件并正确导入,不必显式命名库。
学习 Dart 库与导入时,重点需要掌握以下问题:
dart:、package:和相对 URI 分别指向哪里?- 如何用前缀解决同名冲突?
show、hide和export分别控制什么?- 以下划线开头的名称为什么是库私有,而不是文件私有?
part与普通import有什么区别?
导入一个库
import 指令接收一个 URI,并把目标库的公开名称引入当前库:
import 'dart:math';
void main() {
print(max(12, 8)); // 12
}
导入指令写在文件顶层,位于普通声明之前。它不会像函数调用一样在执行到某一行时才生效,而是在编译阶段建立库之间的依赖关系。
三类常用 URI
Dart 导入最常见的 URI 有三类。
Dart SDK 库:dart:
dart: URI 指向 Dart SDK 提供的库:
import 'dart:convert';
void main() {
final json = jsonEncode({'language': 'Dart'});
print(json); // {"language":"Dart"}
}
常见示例包括 dart:core、dart:convert、dart:math 和 dart:io。其中 dart:core 会被自动导入,使用 String、List、print() 等核心 API 时通常不需要显式导入。
SDK 库并非在所有平台都可用。例如 dart:io 面向能够访问文件、套接字和进程的平台,浏览器应用不能直接依赖它。选择 SDK 库时需要同时考虑目标平台。
包内或第三方库:package:
package: URI 从包的 lib/ 目录开始解析:
import 'package:study_tools/formatters.dart';
这里的 study_tools 是包名,后面的路径对应这个包 lib/ 下的文件。项目需要先在 pubspec.yaml 中声明并解析第三方依赖,才能导入该依赖公开的库。
包自己的 lib/ 目录也可以通过包名导入。跨越多个子目录或提供对外 API 时,package: URI 通常比层层向上的相对路径更稳定。
相邻文件:相对 URI
相对 URI 根据当前文件的位置解析,适合导入附近的实现文件:
import 'models/user.dart';
import '../shared/validator.dart';
相对 URI 只描述文件位置,不包含 Windows 或 Unix 的绝对文件系统路径。URI 中统一使用 /,不要写 C:\... 或反斜杠路径。
同一项目应遵循统一的导入风格,避免同一个库一处使用 package: URI、另一处使用相对 URI。混用可能让读者误判库边界,也可能触发项目启用的 lint 规则。
使用前缀解决名称冲突
两个库导出了同名声明时,可以使用 as 为导入添加前缀:
import 'dart:convert' as convert;
import 'dart:math' as math;
void main() {
print(convert.jsonEncode({'score': math.max(80, 95)}));
}
添加前缀后,需要通过 convert.jsonEncode、math.max 访问对应名称。前缀只在当前库中用于消除歧义,不会修改被导入库中的声明名称。
当前代码即使暂时没有冲突,也可以用前缀表达 API 来源;但不必给每个导入机械地添加前缀,应以可读性为准。
只导入需要的名称
show 只暴露列出的名称,hide 则排除列出的名称:
import 'dart:math' show max, min;
void main() {
print(max(3, 7)); // 7
print(min(3, 7)); // 3
}
import 'dart:math' hide Random;
void main() {
print(sqrt(81)); // 9.0
}
show 和 hide 称为组合器(combinator),用于控制当前库能够通过这条导入访问哪些名称。它们不会修改原库,也不是为了减少最终程序体积而必须使用的优化手段。
组合器也可以和前缀一起使用:
import 'dart:math' as math show max;
void main() {
print(math.max(4, 9)); // 9
}
导出库的公开 API
export 让一个库把另一个库的公开名称重新暴露给自己的使用者。它常用于创建单一入口文件:
// lib/study_tools.dart
export 'src/formatter.dart';
export 'src/validator.dart' show validateEmail;
调用方只需要导入入口库:
import 'package:study_tools/study_tools.dart';
export 同样支持 show 和 hide。入口库可以借此公开稳定 API,同时把 src/ 中的实现细节留在包内部。
import 和 export 的方向不同:import 决定当前库自己能使用什么,export 决定当前库的使用者还能看到什么。导出某个库并不会自动让导出方自己的代码可以直接使用其中名称;需要使用时仍应添加 import。
库私有名称
Dart 中以下划线 _ 开头的标识符是库私有名称:
class UserRepository {
void save() {
_validate();
print('saved');
}
void _validate() {
print('validated');
}
}
其他库可以使用 UserRepository 和 save(),但不能直接访问 _validate()。这里的私有边界是库,而不一定是单个文件。
普通的两个独立 Dart 文件通常各自形成库,因此一个文件不能访问另一个文件中的 _privateName。如果多个文件通过 part 组成同一个库,它们则共享这些私有名称。
TypeScript 对比:TypeScript 的 ES 模块通常通过
export决定哪些文件级声明可被外部导入,还支持private类成员。Dart 的前导下划线直接表示库私有,私有边界可能包含由part连接的多个文件,没有与 TypeScript 文件模块完全相同的对应关系。
使用 part 拆分同一个库
part 可以把多个源文件组成同一个库。主文件声明各个部分:
// shapes.dart
part 'src/circle.dart';
String describeCircle(Circle circle) => 'radius: ${circle._radius}';
部分文件使用 part of 指回主库:
// src/circle.dart
part of '../shapes.dart';
class Circle {
Circle(this._radius);
final double _radius;
}
shapes.dart 和 circle.dart 属于同一个库,所以主文件可以访问 Circle 的私有字段 _radius。调用方只需导入主文件,不应把部分文件当作独立库使用。
对于普通业务模块,优先让每个文件成为独立库,并通过 import 组合公开接口。part 更适合代码生成或确实需要跨文件共享库私有实现的场景,因为它会让文件之间的耦合更紧。
显式 library 指令
文件可以使用 library 指令声明库:
library study_tools;
import 'dart:convert';
String encode(Object? value) => jsonEncode(value);
现代 Dart 代码通常可以省略这条指令;文件仍然会形成库。只有工具约定、生成代码或其他确切需求要求库声明时再添加,避免仅为了给文件起名而重复维护名称。
使用 part 时,部分文件优先通过 URI 写成 part of '../main_library.dart';,这样文件移动或重命名后的关系更容易由工具检查。旧代码也可能通过库名关联 part of,阅读时需要知道两种形式可能同时存在。
延迟导入
延迟导入使用 deferred as,并在首次使用前调用前缀的 loadLibrary():
import 'package:study_tools/report.dart' deferred as report;
Future<void> showReport() async {
await report.loadLibrary();
report.printReport();
}
它允许受支持的编译平台把某个库延后加载。具体平台是否支持、如何拆分产物以及加载失败如何处理,取决于 Dart 工具链和部署目标;使用前应核对当前目标平台的官方支持情况。
loadLibrary() 可以多次调用,库只会实际加载一次。调用完成前不能访问延迟库中的声明,因此加载逻辑通常需要显式处理等待状态和错误。
常见误区
误区一:把文件私有当成库私有
_name 的边界是库。独立文件通常各自是库,但 part 文件与主文件共享同一私有命名空间。
误区二:认为 export 同时完成了 import
导出只影响当前库的使用者。当前库自身要调用目标 API,仍需导入对应库。
误区三:用前缀访问未公开名称
前缀只能限定已经公开且未被 show、hide 排除的名称。写成 package._privateName 也不能绕过库私有边界。
误区四:把平台专属库用于所有目标
代码能解析 dart:io 不代表它能在浏览器运行。多平台项目需要隔离平台实现,必要时使用条件导入提供一致接口。
误区五:过度使用 part
part 让文件共享同一个库作用域,也会增加相互依赖。仅为了拆短文件时,独立库加 import 通常更清晰。
小结
- 每个 Dart 文件都属于一个库,通常不需要显式写
library指令。 dart:导入 SDK 库,package:从包的lib/目录解析,相对 URI 根据当前文件解析。as添加导入前缀,show和hide控制通过导入或导出可见的名称。export用于组织对外 API,但不会代替当前库所需的import。- 以下划线开头的名称是库私有,而不是绝对的文件私有。
part让多个文件组成同一个库并共享私有作用域,应只在确有需要时使用。- 延迟导入依赖目标平台和工具链支持,使用前需要核对当前部署环境。