跳到主要内容

Loops(循环)

循环用于重复执行一段代码。Dart 提供 forfor-inwhiledo-while:已知重复次数时通常使用 for,逐个读取可迭代对象时使用 for-in,根据条件决定是否继续时使用 whiledo-while

学习循环时,需要重点掌握以下问题:

  • 如何选择合适的循环形式?
  • 循环条件在什么时候检查?
  • breakcontinue 分别会改变哪一步?
  • 嵌套循环中如何明确跳出或继续外层循环?

使用 for 控制重复次数

经典 for 循环由初始化、条件和更新三部分组成:

void main() {
for (var index = 0; index < 3; index++) {
print('lesson $index');
}
}

执行顺序是:

  1. var index = 0 只在进入循环时执行一次。
  2. 每轮开始前检查 index < 3,结果必须是 bool
  3. 条件为 true 时执行循环体。
  4. 执行 index++,然后再次检查条件。

因此上例依次输出 lesson 0lesson 1lesson 2。循环变量 index 只在这个 for 循环及其循环体内可见。

初始化和更新部分可以省略。省略条件会形成无限循环,必须通过其他方式结束:

void main() {
var count = 0;

for (;;) {
if (count == 3) {
break;
}

print(count);
count++;
}
}

只有在退出条件清晰时才应使用无限循环,否则代码容易无法结束。

使用 for-in 遍历元素

for-in 按迭代顺序逐个读取 Iterable 中的元素。ListSet 都可以直接使用这种写法:

void main() {
const topics = ['variables', 'types', 'loops'];

for (final topic in topics) {
print(topic);
}
}

如果循环体不需要给当前元素重新赋值,优先使用 finalfor-in 适合只关心元素、不关心索引的场景。

需要同时读取列表索引和值时,可以遍历 indexed。它从 Dart 3.0 开始可用,并为每个元素产生一个 (int, T) record:

void main() {
const topics = ['variables', 'types', 'loops'];

for (final (index, topic) in topics.indexed) {
print('$index: $topic');
}
}

遍历 Map 时,通常遍历 entries,这样可以同时取得键和值:

void main() {
const progress = <String, int>{
'variables': 100,
'loops': 60,
};

for (final entry in progress.entries) {
print('${entry.key}: ${entry.value}%');
}
}

如果需要修改集合本身,应谨慎处理。在遍历可增长集合的过程中增加或删除元素,通常会抛出 ConcurrentModificationError。可以先创建副本再遍历:

void main() {
final scores = <int>[45, 80, 55];

for (final score in scores.toList()) {
if (score < 60) {
scores.remove(score);
}
}

print(scores); // [80]
}

使用 while 按条件重复

while 在每轮开始前检查条件。第一次检查就是 false 时,循环体一次也不会执行:

void main() {
var attempts = 0;

while (attempts < 3) {
print('attempt ${attempts + 1}');
attempts++;
}
}

使用 while 时,要确保循环体最终能够改变条件。上例中的 attempts++ 如果缺失,条件会一直为 true,循环也不会结束。

Dart 不会把数字、字符串或对象自动转换成布尔值,循环条件必须是 bool

void main() {
var remaining = 2;

// while (remaining) {} // 编译错误:int 不是 bool
while (remaining > 0) {
print(remaining);
remaining--;
}
}

使用 do-while 至少执行一次

do-while 先执行循环体,再检查条件,因此循环体至少会运行一次:

void main() {
var number = 5;

do {
print(number);
number++;
} while (number < 5);
}

虽然 number < 5 第一次检查时就是 false,程序仍会输出一次 5。当业务流程要求“先执行,再决定是否重试”时,do-while 比普通 while 更直接。

使用 break 提前结束循环

break 会立即结束它所在的最近一层循环,程序从循环后的语句继续执行:

void main() {
const scores = [45, 72, 91, 58];

for (final score in scores) {
if (score >= 90) {
print('first excellent score: $score');
break;
}
}
}

上例找到第一个不低于 90 的分数后,不再检查剩余元素。

使用 continue 跳过当前轮次

continue 会停止当前轮循环体的剩余部分,并进入下一轮:

void main() {
const scores = [45, 72, 91, 58];

for (final score in scores) {
if (score < 60) {
continue;
}

print('passed: $score');
}
}

在经典 for 循环中,执行 continue 后仍会先运行更新部分,再检查下一轮条件。在 while 中则会直接回到条件检查,因此不要把必须执行的状态更新放在可能被 continue 跳过的位置。

void main() {
var number = 0;

while (number < 4) {
number++;

if (number.isOdd) {
continue;
}

print(number); // 2、4
}
}

使用标签控制嵌套循环

在嵌套循环中,普通 breakcontinue 只影响最近一层循环。Dart 可以为循环添加标签,明确指定要控制的外层循环:

void main() {
outerLoop:
for (var row = 0; row < 3; row++) {
for (var column = 0; column < 3; column++) {
if (row == 1 && column == 1) {
break outerLoop;
}

print('($row, $column)');
}
}
}

break outerLoop 会直接结束两层循环。continue outerLoop 则会结束当前外层轮次,进入外层循环的下一轮。标签能表达复杂控制流,但频繁使用会增加阅读难度;可以拆分函数时,通常应优先让每个函数只负责一层逻辑。

forEach() 的区别

Iterable.forEach() 也能依次处理元素,但它接收的是回调函数:

void main() {
const topics = ['variables', 'types', 'loops'];

topics.forEach((topic) {
print(topic);
});
}

回调中的 return 只会结束当前回调,不会像 break 一样停止整个遍历;回调内也不能使用循环的 breakcontinue。需要提前结束、跳过元素或使用 await 顺序执行异步操作时,通常优先使用 for-in

TypeScript 对比

Dart 与 TypeScript 都提供经典 forfor-of/for-inwhiledo-whilebreakcontinue 和标签,但有两个容易混淆的差异:

对比项DartTypeScript
遍历元素for (final item in items)for (const item of items)
条件类型必须是 bool会按 JavaScript 规则转换为真值或假值

TypeScript 的 for-in 遍历对象的可枚举属性名,不能直接类比 Dart 的 for-in。遍历数组元素时,TypeScript 使用 for-of

const topics = ['variables', 'types', 'loops'];

for (const topic of topics) {
console.log(topic);
}

两种语言中的 breakcontinue 基本作用相同。真正需要记忆的是:Dart 的 in 表示从 Iterable 中逐个取得元素,而 TypeScript 通常用 of 表达这个意图。

选择循环的简单规则

需求推荐写法
已知开始值、结束条件和更新规则for
逐个读取 Iterable 的元素for-in
每轮开始前判断是否继续while
循环体必须至少执行一次do-while
找到目标后立即结束在循环中使用 break
忽略当前元素并处理下一个在循环中使用 continue

优先选择最能直接表达意图的循环形式。无论使用哪一种,都要保证退出条件能够到达,并避免在遍历期间直接改变同一个集合的长度。