Dart 之库与可见性

一、前言

在 Dart 中,把代码拆到多个文件很容易:新建文件,再用 import 引入。但导入了文件,不代表文件里的每个名称都能使用。初学时碰到"明明写了 import,怎么还提示找不到名称",往往是把文件、库和可见性混在了一起。

我们先运行两个小文件,看看哪个名称能用,再弄清 Dart 是怎样判断的。后面遇到类似报错,你也能自己找到原因。

二、从一个用不了的名称说起

把下面两个文件放在同一个目录。message.dart 负责生成问候语,main.dart 负责把它打印出来。

message.dart

dart 复制代码
const String _prefix = '你好,';

String welcome(String name) {
  return '$_prefix$name';
}

main.dart

dart 复制代码
import 'message.dart';

void main() {
  print(welcome('小林')); // 你好,小林
}

运行 main.dart,屏幕会打印"你好,小林"。welcome('小林') 接收名字,用 _prefix 拼好问候语,然后把结果交还给 main()。

现在试着在 main() 中增加一行:

dart 复制代码
print(_prefix); // 错误:这里找不到 _prefix

两行代码的差别要看清:print(welcome('小林')) 是调用 message.dart 提供的函数;print(_prefix) 是想从 main.dart 直接读取函数内部所用的名称。调用函数并不会让调用者自动获得函数能用的所有名称。

三、库决定名称能在哪里使用

Dart 用库来确定一组声明的可见范围。一个普通的 .dart 文件通常就是一个库,不用额外写 library 声明。因此,message.dart 和 main.dart 虽然放在同一个目录,仍是两个库。把文件放在同一目录,不会让它们变成同一个库。

Dart 让我们决定库中的哪些名称可以给其他库使用:

  • welcome 不以 _ 开头,其他库导入 message.dart 后可以使用它。
  • _prefix 以 _ 开头,只能在 message.dart 所属的库中使用。

可以想想在餐厅点餐:顾客从菜单上点一道菜,厨房用自己的调料和步骤把菜做好,再交给顾客。顾客能拿到菜,不等于能直接拿到厨房的调料。这个例子里,welcome() 相当于提供给外部使用的"点餐入口",_prefix 相当于只在里面使用的"调料"。main.dart 调用 welcome() 能拿到拼好的文字,却不能直接读取 _prefix。

对应到 Dart,welcome() 写在 message.dart 中,它读取本库的 _prefix 没问题。main() 写在另一个库中,只能拿到 welcome() 返回的文字。哪怕两个文件都在同一目录、哪怕 main.dart 已经导入 message.dart,这一点也不会改变。

welcome() 是供其他库使用的入口,_prefix 是它完成工作时用的细节。比如把前缀改成"欢迎你,",只需修改 message.dart;只要 welcome() 的调用方式不变,main.dart 不需要修改。把内部名称留在库内,可以减少文件之间的牵连。

3.1 下划线限制的是库,不是类

以下划线开头的名称写在类里,规则也一样。看同一个文件中的两个类:

dart 复制代码
class Counter {
  int _count = 0;
}

class CounterReader {
  int read(Counter counter) => counter._count;
}

_count 是 Counter 的成员,但这里 CounterReader 仍能读取 counter._count。原因不是两个类有什么特殊关系,而是它们写在同一个库里。把 CounterReader 移到另一个普通 Dart 文件,即使导入定义 Counter 的文件,counter._count 也会报错。

因此,看到 _ 时,不要先问"是不是同一个类",要先问"是不是同一个库"。类名、顶级函数名、变量名和类成员名都遵循这个规则。

四、使用别人的库:import

知道 welcome 是公开名称,还不足以在 main.dart 中直接调用它。import 'message.dart'; 告诉 Dart 当前库要使用 message.dart。删掉这句导入,main.dart 就找不到 welcome();但即使写了导入,也不能越过 _prefix 的限制。

判断一个名称能否从别的库使用,可以依次看:有没有导入、名称是否公开、导入时有没有限制名称。这三件事缺少一件,都可能导致"名称用不了"。

导入路径不是随便写的,常见有三类:

写法 含义
import 'message.dart'; 查找当前文件旁边的 message.dart
import 'dart:math'; 使用 Dart SDK 的数学库
import 'package:my_app/message.dart'; 使用 my_app 包中 lib/message.dart 对应的库

这里的包是有 pubspec.yaml 的代码单元,一个包里可以有多个库。my_app 只是示例名称,实际要写 pubspec.yaml 中配置的包名。package: 既可以指当前包,也可以指项目依赖的包。平常不写 import 'dart:core'; 也能用 String、List,是因为 Dart 会自动导入这个核心库。

4.1 两个库有同名名称

news.dart 和 books.dart 都有一个公开的 title:

news.dart

dart 复制代码
const String title = '今日新闻';

books.dart

dart 复制代码
const String title = '推荐图书';

如果两个库都导入到 main.dart,直接写 title 就分不清指哪个。给导入加前缀:

dart 复制代码
import 'news.dart' as news;
import 'books.dart' as books;

void main() {
  print(news.title);  // 今日新闻
  print(books.title); // 推荐图书
}

news.title 和 books.title 各自说明了来源。as 只是当前文件给导入取的前缀,不会修改原来两个库中的名称。

4.2 一个库只用到部分名称

假如 text_tools.dart 有两个函数:

dart 复制代码
String formatName(String name) => '姓名:$name';
String debugName(String name) => '调试:$name';

当前文件只需要 formatName(),可以这样写:

dart 复制代码
import 'text_tools.dart' show formatName;

void main() {
  print(formatName('小林')); // 姓名:小林
  // print(debugName('小林')); // 这里找不到 debugName
}

show formatName 表示这次只引入这个名称。如果想排除 debugName、同时保留其他公开名称,则把导入改成 import 'text_tools.dart' hide debugName;。两种写法都会让当前文件无法直接调用 debugName(),但表达的范围不同。

show 与 hide 只影响这一条导入语句。它们不会删除原库中的函数,也不能让以下划线开头的名称变成公开名称。另一个文件导入 text_tools.dart 时,可以作不同的选择。

五、让别人通过我使用名称:export

收到一份资料,自己能看,不代表其他人也会从你这里拿到它;你得主动转发。库之间也有这层区别:import 让当前库自己使用别的库的名称,export 才让其他库通过当前库使用那些名称。

一个项目可能有多个功能文件。使用者如果要逐个找到并导入它们,入口就会分散。可以在一个文件中用 export 汇集希望对外提供的名称。

假设有三个同目录文件。labels.dart 定义标签函数,shop.dart 是入口,main.dart 只和入口打交道:

labels.dart

dart 复制代码
String productLabel(String name) => '商品:$name';
String debugLabel(String name) => '调试:$name';

shop.dart

dart 复制代码
export 'labels.dart' show productLabel;

main.dart

dart 复制代码
import 'shop.dart';

void main() {
  print(productLabel('书')); // 商品:书
}

main.dart 没有直接导入 labels.dart,却能调用 productLabel(),因为 shop.dart 把它导出了。这里的 show productLabel 表示:通过 shop.dart 这个入口,只提供这个名称。

debugLabel() 本身没有变成私有。如果某个文件直接导入 labels.dart,仍能使用它。真正以下划线开头的名称则不同:即使写了 export,也不能让它在原库之外使用。

把 shop.dart 的语句换成 import 'labels.dart'; 再判断一次:shop.dart 自己能用标签函数,main.dart 能跟着用吗?不能。import 不会自动把导入的名称再交给别人。这就是 import 与 export 最重要的区别。

六、两个文件其实是同一个库:part

前面的文件各自属于不同的库。但文件不一定等于库:一本书可以分成上下册,分开装订仍属于同一部作品。part 和 part of 表达的就是"分成多个文件,仍属于同一个库"。这个比喻只帮助理解归属;真正决定归属的是 Dart 文件中的 part 和 part of 声明。

report.dart

dart 复制代码
part 'report_part.dart';

String reportTitle() => _title;

report_part.dart

dart 复制代码
part of 'report.dart';

const String _title = '本月报表';

main.dart

dart 复制代码
import 'report.dart';

void main() {
  print(reportTitle()); // 本月报表
}

report.dart 用 part 指定另一个文件,report_part.dart 用 part of 声明归属。两个文件共同组成一个库,所以 reportTitle() 能读取另一个文件中的 _title。main.dart 仍在库外,不能直接读取 _title。

如果把 part 改成 import,两个文件就成了两个库,report.dart 也不能再读取 _title。其他代码要使用这个库,应导入 report.dart,不能把 report_part.dart 当成独立库导入。

普通业务代码通常拆成独立的小库,用公开名称协作。确实需要多个文件共享一个库中的名称时,才考虑 part;生成代码是较常见的场景。

七、遇到"名称用不了",怎么判断

回头看 main.dart 读不到 _prefix 的例子。遇到类似情况,可以按顺序检查:

  1. 先确认两个名称是否属于同一个库。普通 Dart 文件通常各自是库;用了 part 的文件与主文件才属于同一个库。
  2. 如果在不同库,看名称是否以 _ 开头。若是,外部不能直接使用,只能调用原库提供的公开功能。
  3. 如果名称公开,检查当前文件是否导入正确的库,路径是否指向想要的文件。
  4. 如果通过统一入口导入,检查入口是否 export 了名称,以及 show 或 hide 是否排除了它。
  5. 如果多个库提供同名名称,给导入加 as 前缀,明确要使用哪一个。

能找到文件、名称对外可见、当前导入允许使用它,是三件不同的事。按这条路线检查,就能区分"路径没写对""名字本来不可见"和"入口没有导出"。

八、总结

库是 Dart 管理名称可见范围的单位。普通文件通常各自是一个库;以下划线 _ 开头的名称只在所属库中使用。

import 让当前库使用其他库的公开名称,export 让别的库通过当前库取得公开名称;part 则把多个文件组成同一个库。记住这三者连接的是谁和谁,比只记关键字更有用。

九、测试题

  1. message.dart 的 welcome() 能读取 _prefix,为什么 main.dart 导入了这个文件仍不能直接读取 _prefix?
  2. CounterReader 与 Counter 是两个类,CounterReader 为什么能读取 _count?把 CounterReader 移到另一个普通 Dart 文件后会怎样?
  3. 两个库都定义了 title,同时导入后应怎样写,才能明确使用其中一个?
  4. shop.dart 只写 import 'labels.dart';,其他文件导入 shop.dart 后能调用 productLabel() 吗?怎样修改?
  5. report.dart 为什么能读取 report_part.dart 中的 _title?如果改用 import,结果会怎样?
相关推荐
FungLeo3 小时前
成为全栈·Flutter App 篇·移动端错误与限流:429、重试和写请求边界
flutter·限流·dart·dio·错误处理·成为全栈
程序员老刘1 个月前
Dart Skills CLI 1.0发布,老刘年初的预言兑现了
flutter·ai编程·dart
程序员老刘1 个月前
现在回头看,Dart取消宏是无比正确的决定
flutter·ai编程·dart
GitLqr1 个月前
从代码复用走向生态:Flutter Dart Package 实战全指南
flutter·全栈·dart
GitLqr2 个月前
Flutter 实战:如何轻松实现图片与视频保存到手机相册
flutter·全栈·dart
iFlyCai2 个月前
深入理解Flutter:StatefulWidget生命周期全解析(一)
flutter·dart·状态管理·statefulwidget
GitLqr2 个月前
Flutter 实战:图片上传前的压缩优化,解决带宽与存储的“大户”问题
flutter·全栈·dart
GitLqr2 个月前
Flutter 3.47 与 Dart 3.13:你必须了解的迁移指南
flutter·ios·dart
杉氧2 个月前
原生交互:如何在 Flutter 中嵌入 Android/iOS 原生组件并解决手势冲突
android·flutter·dart