C# 循环嵌套:循环里的「俄罗斯套娃」
引言:体育课上的报数
想象体育课上老师要求:「每排同学依次报数,第一排报完第二排报,直到所有排报完。」
这里其实有两层循环在运转:
- 外层循环:遍历每一排(第 1 排 → 第 2 排 → ……);
- 内层循环:每一排内部,每个人依次报数(1、2、3……)。
关键节奏是:老师喊「第一排」一次,第一排所有人都要报完,才会喊「第二排」。这就是循环嵌套的核心逻辑——外层转一圈,内层要转满一整圈。
一、先回顾:四类基础循环
在拆解嵌套之前,先快速回顾 C# 的四种单循环(详细内容见系列前文)。
1.1 for 循环:次数明确的「打卡机」
// 语法:for (初始化; 条件; 迭代) { 循环体 }
for (int i = 1; i <= 3; i++) // i 从 1 到 3,共执行 3 次
{
Console.WriteLine($"第 {i} 次打卡");
}适用场景:已知循环次数、需要索引(下标)的场景,如数组遍历、固定次数重复。
1.2 foreach 循环:只读遍历的「传送带」
// 语法:foreach (类型 变量 in 集合) { 循环体 }
string[] fruits = { "苹果", "香蕉", "橙子" };
foreach (string fruit in fruits) // 依次取出每个元素,无需关心下标
{
Console.WriteLine($"水果:{fruit}");
}适用场景:只读遍历集合,不需要修改元素、不需要下标的场景。注意:循环内不能给迭代变量赋值。
1.3 while 循环:条件驱动的「安检门」
// 语法:while (条件) { 循环体 } —— 先判断,后执行
int count = 0;
while (count < 3) // 条件满足才进门
{
Console.WriteLine($"第 {count + 1} 次通过");
count++; // 必须自己更新,否则死循环
}适用场景:不知道循环次数,只知道「满足什么条件就继续」,如轮询等待、条件重试。
1.4 do-while 循环:先干后验的「补票口」
// 语法:do { 循环体 } while (条件); —— 先执行,后判断,保底 1 次
string input;
do
{
Console.Write("请输入 q 退出:");
input = Console.ReadLine(); // 先拿到输入
} while (input != "q"); // 再判断要不要继续适用场景:必须先做一次才能决定要不要继续,如输入验证、分页拉取。
二、循环嵌套:定义与执行逻辑
2.1 官方定义
循环嵌套(Nested Loop):在一个循环语句的循环体内部,完整包裹另一个独立循环语句的代码结构。
外层循环 // 爸爸
{
// 外层循环体的一部分
内层循环 // 儿子:完整地住在爸爸肚子里
{
// 内层循环体
}
// 外层循环体的另一部分
}两条铁律:
- 完全包含:内层循环必须完整地写在外层循环的
{}内部,不能「骑墙」——内层循环的一半在外层里面、另一半在外面,这种交叉写法直接编译报错; - 独立完整:内层循环是一个语法上完整的循环,有自己的初始化、条件和迭代,可以单独拿出来运行。
2.2 执行流程图解
以最经典的「for 嵌套 for」为例:
for (int i = 1; i <= 2; i++) // 外层:2 轮
{
for (int j = 1; j <= 3; j++) // 内层:每轮 3 次
{
Console.WriteLine($"i={i}, j={j}");
}
}执行顺序(像时钟的时针和分针——时针走一格,分针转一整圈):
外层 i=1 开始
├── 内层 j=1 → 输出 i=1, j=1
├── 内层 j=2 → 输出 i=1, j=2
├── 内层 j=3 → 输出 i=1, j=3
└── 内层 j=4 → 条件不满足,内层结束
外层 i=2 开始
├── 内层 j=1 → 输出 i=2, j=1 ← 注意:j 重新从 1 开始!
├── 内层 j=2 → 输出 i=2, j=2
├── 内层 j=3 → 输出 i=2, j=3
└── 内层结束
外层 i=3 → 条件不满足,全部结束核心结论:外层循环每执行 1 次,内层循环会从头完整执行完所有迭代。总执行次数 = 外层次数 × 内层次数(上例:2 × 3 = 6 次输出)。
关键细节:内层的计数器 j 在每轮外层循环时都会重新初始化——因为 int j = 1 这句声明在内层 for 里,每轮都会重新执行。
三、所有合法的嵌套组合与适用场景
四种循环两两组合,理论上共有 4 × 4 = 16 种嵌套方式,全部合法。下面按实用性拆解最常见的组合,每个示例都可直接编译运行(基于 .NET 6+)。
3.1 for 嵌套 for:二维数组遍历的标配
场景:处理有「行 × 列」结构的数据,如二维数组、矩阵、表格。
// 场景:遍历 2 行 3 列的座位表
string[,] seats =
{
{ "张三", "李四", "王五" }, // 第 0 行
{ "赵六", "孙七", "周八" } // 第 1 行
};
for (int row = 0; row < seats.GetLength(0); row++) // GetLength(0):取行数
{
for (int col = 0; col < seats.GetLength(1); col++) // GetLength(1):取列数
{
Console.WriteLine($"座位[{row},{col}]:{seats[row, col]}");
}
}
// 输出:座位[0,0]:张三 → [0,1]:李四 → … → [1,2]:周八3.2 for 嵌套 foreach:按索引分组处理集合
场景:外层需要序号(第几批),内层只读遍历每批的数据。
// 场景:分 2 个小组打扫卫生,汇报每组成员
string[][] groups =
{
new[] { "小明", "小红" }, // 第 1 组
new[] { "小刚", "小丽", "小华" } // 第 2 组
};
for (int g = 0; g < groups.Length; g++) // 外层需要组号 → 用 for
{
Console.WriteLine($"--- 第 {g + 1} 组(共 {groups[g].Length} 人)---");
foreach (string member in groups[g]) // 内层只读遍历成员 → 用 foreach
{
Console.WriteLine($" 成员:{member}");
}
}3.3 foreach 嵌套 for:不规则集合迭代
场景:外层遍历的对象各不相同(长度不一),内层按各自的长度处理——比如「每个班级人数不同」。
// 场景:打印每个班级的成绩单,各班人数不同(交错数组)
int[][] classScores =
{
new[] { 90, 85 }, // 1 班只有 2 人
new[] { 78, 92, 88, 60 } // 2 班有 4 人
};
int classNo = 0;
foreach (int[] scores in classScores) // 外层:依次取出每个班
{
classNo++;
int total = 0;
for (int i = 0; i < scores.Length; i++) // 内层:按本班人数循环
{
total += scores[i];
}
Console.WriteLine($"{classNo} 班平均分:{total / scores.Length}");
}
// 输出:1 班平均分:87 2 班平均分:793.4 while 嵌套 for:条件驱动的批量处理
场景:外层「只要还有任务就一直干」(条件驱动),内层「每批处理固定数量」(次数驱动)。
// 场景:消息队列消费,每次最多取 3 条,直到队列清空
Queue<string> queue = new(new[] { "msg1", "msg2", "msg3", "msg4", "msg5", "msg6", "msg7" });
int batchNo = 0;
while (queue.Count > 0) // 外层:队列不空就继续(不知道要循环几批)
{
batchNo++;
int batchSize = Math.Min(3, queue.Count); // 本批实际数量
for (int i = 0; i < batchSize; i++) // 内层:固定处理本批
{
string msg = queue.Dequeue(); // 取出并移除一条
Console.WriteLine($"第 {batchNo} 批处理:{msg}");
}
}
// 输出:第 1 批处理 msg1-3,第 2 批处理 msg4-6,第 3 批处理 msg73.5 foreach 嵌套 do-while:逐项确认操作
场景:遍历每个待处理项,对每一项都需要「先执行一次操作,再问用户是否重试」。
// 场景:逐个上传文件,失败时询问是否重传(至少尝试一次)
string[] files = { "报告.docx", "照片.jpg" };
foreach (string file in files)
{
string answer;
int attempt = 0;
do
{
attempt++;
// 模拟:第 1 次必失败,第 2 次成功
bool success = attempt >= 2;
Console.WriteLine($"上传 {file}(第 {attempt} 次):{(success ? "成功" : "失败")}");
if (success) break; // 成功则不再询问
Console.Write("上传失败,输入 r 重传,其他键跳过:");
answer = Console.ReadLine();
} while (answer == "r"); // 用户要求重传才继续
}3.6 组合选型速查表
| 嵌套组合 | 外层职责 | 内层职责 | 典型业务场景 |
|---|---|---|---|
| for + for | 行 | 列 | 二维数组、矩阵、九九乘法表 |
| for + foreach | 批次(需序号) | 批内元素 | 分组汇报、分页展示 |
| foreach + for | 集合中的对象 | 对象内部按数量处理 | 交错数组、不等长数据 |
| while + for | 条件驱动(不知何时结束) | 固定批量 | 队列消费、批量任务 |
| foreach + do-while | 逐项遍历 | 每项至少操作一次+重试 | 文件上传重试、逐项确认 |
| do-while + for | 至少执行一轮的流程 | 轮内固定步骤 | 游戏回合、向导式表单 |
四、入门避坑指南
4.1 坑点1:内外层计数器混淆导致死循环
错误代码:
// 原意:3 行 3 列的网格
for (int i = 1; i <= 3; i++)
{
for (int j = 1; j <= 3; j++)
{
Console.WriteLine($"({i},{j})");
i++; // ❌ 手滑写成了 i++!内层把外层计数器改了
}
}问题分析:内层循环里误写 i++,外层的 i 被内层反复修改,i <= 3 这个外层条件的变化完全失控——轻则输出错乱,重则死循环或提前退出。复制粘贴外层代码改成内层时,最容易犯这个错。
修正代码:
// ✅ 内外层用不同的、有语义的变量名
for (int row = 1; row <= 3; row++) // row:行
{
for (int col = 1; col <= 3; col++) // col:列
{
Console.WriteLine($"({row},{col})");
}
}预防:嵌套循环的计数器别用 i、j 这种「孪生」名字,改用 row/col、x/y、page/item 等有业务含义的名字——写错时一眼就能看出来。
4.2 坑点2:修改 foreach 的迭代变量引发编译错误
错误代码:
int[] numbers = { 1, 2, 3 };
foreach (int num in numbers)
{
if (num == 2)
{
num = 99; // ❌ 编译错误 CS1656:
// 无法给 foreach 迭代变量赋值
}
Console.WriteLine(num);
}问题分析:foreach 的迭代变量(这里是 num)是只读的——它像传送带上递到你手里的「复制品」,你不能修改它,更不能指望改了它就能改原数组。这是 C# 的语法强制,嵌套在别的循环里也一样。
修正代码(需要修改元素时,改用带索引的 for):
int[] numbers = { 1, 2, 3 };
for (int i = 0; i < numbers.Length; i++)
{
if (numbers[i] == 2)
{
numbers[i] = 99; // ✅ 通过下标直接改原数组
}
Console.WriteLine(numbers[i]);
}
// 输出:1、99、34.3 坑点3:嵌套层级过深(超过 3 层)
错误代码(四层嵌套,真实项目中常见):
// ❌ 读这段代码像走迷宫
foreach (var order in orders) // 第 1 层
{
foreach (var item in order.Items) // 第 2 层
{
for (int i = 0; i < item.Options.Length; i++) // 第 3 层
{
while (item.Options[i].Stock > 0) // 第 4 层:晕了吗?
{
// 几十行业务逻辑……
}
}
}
}问题分析:
- 可读性:每多一层,读者要在脑子里多记一个「我在哪层」的上下文,超过 3 层后基本没人能顺畅读懂;
- 性能:总迭代次数是各层次数的乘积(10 × 10 × 10 × 10 = 1 万次),层数越深,次数爆炸越快;
- 维护:改内层逻辑时,很难评估对外层的影响。
修正代码(用「方法提取」降层):
// ✅ 把内层逻辑抽成独立方法,每层只做一件事
foreach (var order in orders)
{
ProcessOrderItems(order); // 主流程一眼看清:遍历订单 → 处理明细
}
static void ProcessOrderItems(Order order)
{
foreach (var item in order.Items)
{
ConsumeOptionsStock(item); // 第二层也清晰
}
}
static void ConsumeOptionsStock(OrderItem item)
{
for (int i = 0; i < item.Options.Length; i++)
{
// 库存扣减逻辑……(最深也只到这里的 while)
}
}经验法则:嵌套 ≤ 2 层是舒适区,3 层是警戒线,超过 3 层就该考虑提取方法或重构数据结构了。
五、性能与优化
5.1 时间复杂度:嵌套循环的「乘法效应」
时间复杂度(Time Complexity)是衡量「数据量增大时,程序耗时增长多快」的指标。嵌套循环的核心规律:总耗时 ≈ 外层次数 × 内层次数。
// 单层循环:执行 n 次 → 复杂度 O(n)
for (int i = 0; i < n; i++) { /* O(1) 操作 */ }
// 两层嵌套:执行 n × n 次 → 复杂度 O(n²)
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++) { /* O(1) 操作 */ }
}直观感受:n = 1000 时,单层是 1000 次,双层是 100 万次,三层是 10 亿次。嵌套每多一层,数据量稍大就会「卡成 PPT」。
5.2 优化手段一:提取公共计算到循环外
优化前:内层每轮都在重复计算同一个值。
// ❌ Math.Sqrt(baseValue) 算了 100 × 100 = 10000 次,结果都一样
for (int i = 0; i < 100; i++)
{
for (int j = 0; j < 100; j++)
{
double threshold = Math.Sqrt(baseValue); // 与 i、j 无关!
matrix[i, j] = values[i] * weights[j] / threshold;
}
}优化后:只算 1 次。
// ✅ 与循环变量无关的计算,提到循环外(这叫「循环不变量外提」)
double threshold = Math.Sqrt(baseValue); // 只算 1 次
for (int i = 0; i < 100; i++)
{
for (int j = 0; j < 100; j++)
{
matrix[i, j] = values[i] * weights[j] / threshold;
}
}性能对比(实测 Math.Sqrt 约 20ns/次):优化前仅开平方就耗时约 0.2ms;优化后这部分开销归零。单个函数越贵(比如数据库查询、字符串拼接),外提的收益越大——把数据库查询写在内层循环里,是生产环境性能事故的头号常客。
5.3 优化手段二:提前终止无用循环
场景:在 1000 × 1000 的矩阵里找一个目标值,找到就停。
// ❌ 优化前:找到了还傻乎乎跑完全程(100 万次)
bool found = false;
for (int i = 0; i < 1000; i++)
{
for (int j = 0; j < 1000; j++)
{
if (matrix[i, j] == target) found = true; // 只记录,不退出
}
}// ✅ 优化后:标志位 + break,找到立即停止
bool found = false;
for (int i = 0; i < 1000 && !found; i++) // 外层感知标志位
{
for (int j = 0; j < 1000; j++)
{
if (matrix[i, j] == target)
{
found = true;
break; // 内层立即停
}
}
}性能对比:假设目标在 [10, 20] 位置——优化前固定执行 100 万次;优化后只执行约 1 万次,提速 100 倍。若把查找逻辑封装成方法用 return 退出,还能省去标志位判断的开销(详见前篇《CSharp之break与continue》)。
5.4 优化手段三:避免嵌套内的资源重复创建
优化前:每轮都新建对象,产生大量垃圾回收压力。
// ❌ StringBuilder 创建 1000 次,格式化对象也在内层反复 new
for (int i = 0; i < 1000; i++)
{
for (int j = 0; j < 1000; j++)
{
var sb = new StringBuilder(); // 每轮新建
sb.Append($"[{i},{j}]").Append(data[i, j]);
results[i, j] = sb.ToString();
}
}优化后:对象复用。
// ✅ 循环外创建一次,循环内清空复用
var sb = new StringBuilder(); // 只创建 1 次
for (int i = 0; i < 1000; i++)
{
for (int j = 0; j < 1000; j++)
{
sb.Clear(); // 清空而非新建
sb.Append('[').Append(i).Append(',').Append(j).Append(']')
.Append(data[i, j]);
results[i, j] = sb.ToString();
}
}性能对比(100 万次迭代实测量级):重复 new 版本约产生 100 万个临时 StringBuilder 对象,GC(垃圾回收——.NET 自动回收无用内存的机制)被频繁触发,耗时约 400ms;复用版本 GC 几乎不工作,耗时约 150ms,提速约 2.5 倍且内存占用大幅下降。
5.5 优化清单速查
| 优化手段 | 一句话原则 | 收益量级 |
|---|---|---|
| 循环不变量外提 | 与 i、j 无关的计算别放循环里 | 计算越贵,收益越大 |
| 提前终止 | 找到/失败就 break,别跑冤枉路 | 最好情况提速 N 倍 |
| 对象复用 | Clear() 复用优于反复 new | 减少 GC,提速 2 倍+ |
| 降低嵌套层数 | 能查表别遍历,能单层别双层 | 复杂度数量级下降 |
六、实战案例
6.1 案例一:输出九九乘法表
需求分析:打印 1×1 到 9×9 的乘法表,第 n 行只打印到 n 列(三角形排列)。
1×1=1
2×1=2 2×2=4
3×1=3 3×2=6 3×3=9
……代码实现:
// 外层 for 控制「行」:第 1 行到第 9 行
for (int row = 1; row <= 9; row++)
{
// 内层 for 控制「列」:每行只打印到 row 列(关键:col <= row)
for (int col = 1; col <= row; col++)
{
// \t 是制表符,让每列对齐;col 在前 row 在后是乘法表的习惯写法
Console.Write($"{row}×{col}={row * col}\t");
}
Console.WriteLine(); // 一行打印完,换行(这行属于外层循环体)
}逻辑要点:
- 内层条件
col <= row是三角形的来源——第几行就打印几列; Console.Write(不换行)负责行内拼接,Console.WriteLine(换行)在外层负责换行——**「内层管内容,外层管换行」**是打印二维图形的通用套路;- 总打印次数 = 1+2+…+9 = 45 次,而非 81 次,说明内层次数可以依赖外层变量动态变化。
6.2 案例二:遍历并处理三维商品库存数组
需求分析:某仓库系统按「仓库 × 货架 × 层」三级存放商品,库存存于三维数组。需要:① 统计总库存;② 找出所有库存为 0 的货位并报警。
代码实现:
// 三维数组:[仓库, 货架, 层] → 库存数量
int[,,] inventory =
{
{ // 0 号仓库
{ 10, 0, 5 }, // 0 号货架的 3 层
{ 3, 7, 0 } // 1 号货架的 3 层
},
{ // 1 号仓库
{ 0, 0, 8 },
{ 6, 2, 4 }
}
};
int totalStock = 0;
int emptySlots = 0;
// 第一层:遍历仓库(GetLength(0) 取第一维长度)
for (int w = 0; w < inventory.GetLength(0); w++)
{
// 第二层:遍历当前仓库的货架
for (int s = 0; s < inventory.GetLength(1); s++)
{
// 第三层:遍历当前货架的每一层
for (int l = 0; l < inventory.GetLength(2); l++)
{
int stock = inventory[w, s, l]; // 取出当前货位库存
totalStock += stock; // 累加总库存
if (stock == 0)
{
emptySlots++;
Console.WriteLine($"⚠ 缺货报警:{w} 号仓库 {s} 号货架 {l} 层");
}
}
}
}
Console.WriteLine($"\n盘点完成:总库存 {totalStock} 件,缺货货位 {emptySlots} 个");
// 输出:
// ⚠ 缺货报警:0 号仓库 0 号货架 1 层
// ⚠ 缺货报警:0 号仓库 1 号货架 2 层
// ⚠ 缺货报警:1 号仓库 0 号货架 0 层
// ⚠ 缺货报警:1 号仓库 0 号货架 1 层
// 盘点完成:总库存 45 件,缺货货位 4 个逻辑要点:
- 三维数组用
GetLength(维度号)取每一维的长度,维度号从 0 开始; - 三层嵌套的执行节奏:外层仓库走 1 个,中间的货架全走一遍;每个货架内,所有的层全走一遍——总检查次数 = 2 × 2 × 3 = 12 次;
- 统计逻辑(累加)和报警逻辑(判断)都写在最内层,因为只有最内层才能拿到具体的货位数据。
6.3 案例三:批量校验用户提交的多组表单数据
需求分析:后台收到一批用户表单,每个表单包含多个字段。需要逐表单、逐字段校验:空字段跳过(continue),发现非法字符的表单整体作废(标志位 + break),全部校验完输出报告。
代码实现:
// 模拟提交的数据:每个表单是「字段名 → 值」的数组
string[][] forms =
{
new[] { "张三", "18", "zhangsan@qq.com" }, // 表单 1:合法
new[] { "", "25", "lisi@qq.com" }, // 表单 2:名字为空 → 跳过该字段
new[] { "王五xxxxscxxxxriptxxxx", "30", "a@b.com" }, // 表单 3:含非法字符 → 整单作废
};
string[] fieldNames = { "姓名", "年龄", "邮箱" };
int passCount = 0, failCount = 0;
for (int f = 0; f < forms.Length; f++) // 外层:逐个表单
{
bool formValid = true; // 当前表单的「健康标志位」
for (int field = 0; field < forms[f].Length; field++) // 内层:逐个字段
{
string value = forms[f][field];
// 规则1:空字段 → 跳过,不影响其他字段
if (string.IsNullOrWhiteSpace(value))
{
Console.WriteLine($"表单 {f + 1}:{fieldNames[field]}为空,跳过校验");
continue; // 只跳过本字段,表单继续校验
}
// 规则2:含非法字符 → 整个表单作废,不再校验剩余字段
if (value.Contains("<") || value.Contains(">"))
{
Console.WriteLine($"表单 {f + 1}:{fieldNames[field]}含非法字符,整单作废");
formValid = false; // ① 打标记
break; // ② 跳出内层字段循环
}
}
// 内层结束后,根据标志位统计结果
if (formValid)
{
passCount++;
Console.WriteLine($"表单 {f + 1}:校验通过 ✓");
}
else
{
failCount++;
}
}
Console.WriteLine($"\n批量校验完成:通过 {passCount} 份,作废 {failCount} 份");
// 输出:
// 表单 1:校验通过 ✓
// 表单 2:姓名为空,跳过校验
// 表单 2:校验通过 ✓
// 表单 3:姓名含非法字符,整单作废
// 批量校验完成:通过 2 份,作废 1 份逻辑要点:
continue处理「可容忍的小问题」(字段空),粒度是当前字段;break+ 标志位处理「不可容忍的大问题」(非法字符),粒度是整个表单——内层 break 退出字段循环后,外层靠formValid知道这份表单的最终状态;- 这个「外层管单据、内层管明细、标志位传状态」的结构,是批量数据处理的万能模板。
七、总结
7.1 核心知识点思维导图
循环嵌套
├── 定义与规则
│ ├── 内层完整包裹在外层 {} 内(禁止交叉)
│ ├── 外层执行 1 次 → 内层完整执行所有迭代
│ └── 总次数 = 外层次数 × 内层次数
├── 常见组合
│ ├── for + for → 二维数组 / 矩阵 / 图形打印
│ ├── for + foreach → 分批处理(需批号)
│ ├── foreach + for → 不等长集合(交错数组)
│ ├── while + for → 队列消费 / 批量任务
│ └── foreach + do-while → 逐项确认 / 失败重试
├── 避坑要点
│ ├── 计数器命名要有语义(row/col 优于 i/j)
│ ├── foreach 迭代变量只读,要改元素用 for + 下标
│ └── 嵌套超过 3 层 → 提取方法降层
├── 性能优化
│ ├── 循环不变量外提(尤其禁止内层查数据库)
│ ├── 找到即 break / return,不跑冤枉路
│ └── 对象 Clear() 复用,减少 GC
└── 跳转控制
├── continue → 跳过当前项(小问题)
├── break → 退出当前层(大问题)
└── 标志位 / return → 跨层退出(终止全局)7.2 进阶学习方向
- Parallel.For / PLINQ:当嵌套循环的各次迭代互不依赖时,可用并行循环让多核 CPU 同时干活;
- 递归:某些多层嵌套问题(如遍历文件夹树、菜单树)用递归表达更优雅,层数不受限;
- LINQ 的 SelectMany:处理「集合套集合」的扁平化查询,可以替代部分两层 foreach;
- Span<T> 与多维数组的内存布局:深入理解数组在内存中的排列方式,进一步优化大规模嵌套遍历的缓存命中率。
结语
循环嵌套的本质就一句话:「外层走一步,内层走一圈」。记住时钟的类比——时针(外层)走一格,分针(内层)必须转满 60 格。
写好嵌套循环的三重心法:
- 结构上:内层完整住在外层肚子里,计数器各管各的、名字要有区分度;
- 逻辑上:想清楚每层循环的「职责」——外层管什么维度,内层管什么维度,统计/判断逻辑该放在哪一层;
- 性能上:对「次数相乘」保持敬畏,能提前 break 就不跑全程,能循环外算好就不放循环里。
当你能一眼看出「这段嵌套里哪行代码执行了多少次」时,循环嵌套这个知识点就真正毕业了。