Skip to content

Dart 完整实践:流式分类账与失败测试 ​

文件合同与输出 ​

前置:基础与建模、Future与Stream。这是独立Dart CLI,标准库即可完成。输入为无BOM UTF-8,无表头;每行name|amount,name是1–32个ASCII字母/数字/下划线/短横线、首字符字母;金额1–999且无前导零、空格或符号。最多10000行,空文件成功但空行失败;支持LF/CRLF及末行无换行。输入不是任意CSV,不处理引号转义或多行字段。

输出分类按String排序,每行name=total;ASCII名称使跨平台排序一致。成功0;格式/读取/参数错误2,stderr说明原因、stdout无部分报告。所有整数在当前上限下不超过9990000;CLI运行于Dart VM,不把本轮验证写成JavaScript后端测试。输入逐行解码但一个超长行仍可先占内存,应在外层根据部署需求限制文件大小与字节行长。

完整源码与错误测试 ​

examples/dart/ledger下仅需以下文件,不需要pubspec或第三方包;test.dart是使用明确check与throw的测试程序,避免只依赖可被关闭的语言assert。

ledger.dart
dart
import 'dart:async';
import 'dart:convert';
import 'dart:io';

class Entry {
  final String name;
  final int amount;
  const Entry(this.name, this.amount);
}
final _row = RegExp(r'^([A-Za-z][A-Za-z0-9_-]{0,31})\|([1-9][0-9]{0,2})$');
Entry parse(String line, int number) {
  final match = _row.firstMatch(line);
  if (match == null || match.end != line.length) {
    throw FormatException('line $number: expected name|1..999');
  }
  return Entry(match[1]!, int.parse(match[2]!));
}
Future<String> summarize(Stream<String> lines) async {
  final totals = <String, int>{};
  var number = 0;
  await for (final line in lines) {
    if (++number > 10000) throw const FormatException('more than 10000 rows');
    final entry = parse(line, number);
    totals.update(entry.name, (value) => value + entry.amount, ifAbsent: () => entry.amount);
  }
  final names = totals.keys.toList()..sort();
  return names.map((name) => '$name=${totals[name]}\n').join();
}
Future<void> main(List<String> args) async {
  try {
    if (args.length != 1) throw ArgumentError('usage: dart run ledger.dart FILE');
    final lines = File(args.single).openRead().transform(utf8.decoder).transform(const LineSplitter());
    stdout.write(await summarize(lines));
  } on Object catch (error) {
    stderr.writeln(error);
    exitCode = 2;
  }
}
test.dart
dart
import 'dart:async';
import 'ledger.dart';
void check(bool value) { if (!value) throw StateError('assertion failed'); }
Future<void> main() async {
  check(await summarize(Stream.fromIterable(['z|2', 'a|3', 'z|4'])) == 'a=3\nz=6\n');
  for (final line in ['a|0', 'a|01', 'a|2|3', 'a| 2', 'a|1000', 'x!|1', '']) {
    try { parse(line, 1); throw StateError('accepted invalid'); } on FormatException { /* expected */ }
  }
  var cancelled = false;
  final controller = StreamController<String>(onCancel: () { cancelled = true; });
  final work = summarize(controller.stream);
  controller.add('a|0');
  try { await work; throw StateError('accepted invalid stream'); } on FormatException { check(cancelled); }
  await controller.close();
  var finished = false;
  final original = Future<void>.delayed(const Duration(milliseconds: 30), () { finished = true; });
  try { await original.timeout(Duration.zero); throw StateError('timeout missing'); } on TimeoutException { }
  await original;
  check(finished); // timeout does not cancel original Future
  print('Dart module: parse, totals, stream cancellation, Future timeout passed');
}

parse构造不可变字段的Entry;RegExp匹配保证分组存在后,两个!都有局部证明;未校验的外部String不应随意加!。totals只在当前异步函数内更新,不跨isolate共享。map.update处理已存在与首次记录,最后复制键列表后排序,避免在遍历映射时改变结构。库如果收到坏行会抛FormatException并退出await-for;上游订阅取消由测试的onCancel明确检查。

运行与正常/失败验证 ​

保存sample.txt三行z|2、a|3、z|4,采用无BOM UTF-8。Windows PowerShell7可用Set-Content -Encoding utf8NoBOM sample.txt @('z|2','a|3','z|4');Linux Bash可用printf 'z|2\na|3\nz|4\n' > sample.txt。在工程目录:

shell
dart analyze ledger.dart test.dart lab.dart
dart run test.dart
dart run ledger.dart sample.txt
dart run lab.dart

CLI预期a=3、z=6;测试预期Dart module: parse, totals, stream cancellation, Future timeout passed。故意把最后一行改为z|0,退出2且没有前两行的部分汇总。Windows检查LASTEXITCODE,Bash检查?。没有输入文件和多传参数也都必须失败,不能输出空成功。仓库的verify.py对每门实际启动17类进程场景,含非法编码、格式、10001行、无参数与缺文件;结果有Windows/Linux分别记录。

分析器、堆栈与性能 ​

A value of type ... can't be assigned先看var推断、?和泛型;不要把所有变量改dynamic绕过分析器。LateInitializationError是延迟字段先读,Null check operator used on a null value是!的不变量被打破;两者不是同一种“SDK问题”。FormatException可能来自业务parse,也可能来自UTF-8解码,应结合堆栈和消息判断。相对路径相对启动目录,不能假定相对ledger.dart。

在编辑器给parse入口与catch设断点,查看行号、原始line及match;异常断点可在抛出时停住,避免只看到最终CLI错误。运行性能实验保持相同结果,观察迭代与物化差别;Dart VM首次运行有JIT预热,AOT编译可用dart compile exe ledger.dart -o ledger,它验证不同部署产物,不能拿一次JIT时间当AOT性能。

Flutter资料的去向与边界 ​

旧Dart基础中的安装、final集合可变、回调和对象例子已经融入本课程。Flutter资料属于框架层:SDK自带Dart、doctor检查目标依赖、create/devices/run流程仍有参考价值,保留第一个Flutter应用作为补充,并明确它的设备运行未在本语言CLI批次验证。Widget生命周期、跨页状态、导航和平台构建不属于本项目的完成声明。

旧HelloWorld里的Android Build Tools28.0.3与旧IDE菜单是特定旧工程问题,不能作为当前安装步骤;去Flutter检查章节核对实际错误。启动锁先确认其他Flutter/IDE进程仍在工作,不能照旧笔记递归删除SDK锁。国内包源依赖当前网络和政策,按官方镜像说明检查,不在正文固定个人路径或无依据切源。

综合复习 ​

练习:增加最大类别数限制并在超过时首错退出,写StreamController测试确认取消;证明timeout没有停止Future;解释final Map和不可变Entry的区别;在输入有效前不发stdout的设计解决什么问题?

参考方向

只有首次出现新name时检查不同键数,边界包含上限与上限+1;测试等待summarize的错误,再验证onCancel。保留原Future并等待完成后检查副作用,不把超时当取消。final仅冻结Map变量绑定,Entry的final字段限制字段重赋;二者都不意味着递归冻结任意对象。结果先汇总后输出防止坏输入产生一半看似成功的报告,但不提供文件系统事务。

依据:dart analyze、File.openRead、Flutter安装排查。核对2026-10-17。

继续阅读