去做题

第十五单元:越界防护 · 安全访问二维数组

Unit 15: Bounds Protection — Safe Access to 2D Arrays
计算机科学 5 · 安息云峰 Cloudrip Mountain · 梦游者 → 大炮陆战队 → 雪花莲

15.1 越界的危险

The Danger of Going Out of Bounds

欢迎来到计算机科学 5 的第十五单元!在之前的单元里,你已经学会了怎样用嵌套循环遍历二维数组、用 grid[i][j] 读取每一个格子。可是当地图不再"方方正正"、当坐标来自随机或外部来源时,一个粗心的索引就会让程序崩溃。本单元你将学会一件专业程序员每天都在做的事——边界检查(bounds checking):在访问数组元素之前,先确认这个位置真的存在。

本单元主线:访问二维数组的某个格子 grid[row][col] 前,先用 row < len(grid) 确认行存在,再用 col < len(grid[row]) 确认列存在。只有两条都成立,才安全地读取那个格子。这套"先检查、后访问"的习惯能彻底避免 IndexError 索引越界异常,尤其是在每行长度都不一样的不规则数组上。

15.1.1 什么是索引越界(What Is Out-of-Bounds Access)

15.1.1.1 伸手去够不存在的格子 Reaching for a cell that isn't there

数组的索引是从 0 开始编号的。一个有 3 个元素的数组,合法索引只有 0、1、2——最大索引永远是长度减一。如果你去访问 grid[3],就像伸手去够一个根本不存在的格子:程序无法凭空造出数据,只能报错停下。二维数组更容易出错,因为你要同时保证"行"和"列"两个索引都合法。

🏚️ 越界就像敲不存在的门牌号
一条街上只有 1 号到 3 号三栋房子。你拿着"4 号"的地址去送信——可 4 号根本不存在,邮差只能空手而归并大喊"查无此址!"。数组访问也一样:索引必须落在 0 到 长度-1 之间,超出这个范围,Python 就会"查无此址"地报错。

15.1.2 IndexError 索引越界异常(The IndexError Exception)

15.1.2.1 越界会抛出异常、终止程序 Out-of-range access raises an exception

当访问的索引超出数组范围时,Python 会抛出 IndexError(列表越界)异常。关卡"雪花莲"的说明就直接演示了两种典型的越界:行对但列错,以及行本身就不存在。这两种情况都会让程序中断——在游戏里,就意味着大炮朝着不存在的坐标开火、任务失败。

# 假设 forestMap 的第 0 行只有 3 个元素(索引 0、1、2)
# 正确的行,但错误的列:
forestMap[0][4]   # ← IndexError:第 0 行没有第 4 列

# 错误的行,任意列:
forestMap[5][1]   # ← IndexError:forestMap[5] 根本不存在
异常 = 程序急刹车:IndexError 一旦被抛出,如果没有处理,后面的代码就全部停止执行。这就是为什么"边界检查"如此重要——与其让程序撞墙崩溃,不如提前看清路况、绕开不存在的格子。
扣哒世界 IndexError 索引越界示意 - koudashijie.com
图1:索引从 0 开始,越过数组末尾就会触发 IndexError 异常

15.2 二维数组回顾

Reviewing 2D Arrays

15.2.1 数组的数组(An Array of Arrays)

15.2.1.1 像电子表格一样的行与列 Rows and columns like a spreadsheet

正如"大炮陆战队"关卡所说:"二维数组是每个元素都是数组的数组。因此,我们得到了类似电子表格的东西。"外层数组的每个元素本身又是一个数组——外层的索引是"行",内层的索引是"列"。要取出某个格子,就写两对方括号:先选行,再选列。

# 一个 2 行 3 列的二维数组
grid = [
    [0, 1, 2],   # ← 第 0 行
    [3, 4, 5]    # ← 第 1 行
]

grid[0][0]   # 是 0(第 0 行、第 0 列)
grid[1][2]   # 是 5(第 1 行、第 2 列)
grid[1][0]   # 是 3(第 1 行、第 0 列)
grid[行][列] —— 先选行,再选列 列0 列1 列2 行0 0 1 2 行1 3 4 5 grid[1][2]=5

15.2.2 len 测量长度(Measuring Length with len)

15.2.2.1 len 告诉你"有几行、每行几列" len reveals how many rows and columns

要做边界检查,先得知道数组"有多大"。函数 len() 返回一个数组里元素的个数。对二维数组来说,len(grid) 是行数;而 len(grid[row]) 是第 row 行的列数。这正是边界检查的两把"尺子"。

grid = [[0, 1, 2], [3, 4, 5]]

len(grid)        # 2 —— 一共有 2 行
len(grid[0])     # 3 —— 第 0 行有 3 列
len(grid[1])     # 3 —— 第 1 行有 3 列
长度与最大索引:如果 len(grid) 是 2,那么合法的行索引就是 0 和 1——最大索引 = 长度 − 1。所以判断"索引合法"的条件是 索引 < 长度,而不是 索引 <= 长度。这个"小于"非常关键,多一个等号就会越界。

15.3 边界检查模式

The Bounds-Checking Pattern

15.3.1 检查行 row(Checking the Row)

15.3.1.1 先确认这一行存在 First confirm the row exists

安全访问的第一步:确认行索引 row 落在合法范围内,也就是 row < len(grid)。只有这一行真的存在,我们才有资格去看它有几列。如果连行都不存在,谈论"列"就毫无意义——就像先要确认门牌号所在的那条街存在,才能谈门牌号。

row = 1
if row < len(forestMap):
    # 到这里,说明 row 是一个合法的行索引
    # ...接着才检查列...
    pass

15.3.2 检查列 col(Checking the Column)

15.3.2.1 再确认这一列存在 Then confirm the column exists

确认行存在之后,第二步是检查列索引:col < len(forestMap[row])。注意这里用的是 len(forestMap[row])——是那一行自己的长度,而不是整个数组的长度。这一点在不规则数组里尤其重要,因为每行的列数可能都不一样。关卡"雪花莲"给出的正是这个"两层嵌套 if"的标准写法:

row = 1
col = 3
if row < len(forestMap):
    # 如果 row 是正确的索引
    if col < len(forestMap[row]):
        # 现在我们才能安全地取到单元格
        hero.say(forestMap[row][col])
安全访问 grid[row][col] 的两道关卡 ① row < len(grid)? 这一行存在吗? 是 ② col < len(grid[row])? 这一列存在吗? 是 ✅ 安全访问 grid[row][col] 否 否 跳过 —— 绝不访问不存在的格子

15.3.3 用逻辑运算符合并(Combining with Logical Operators)

15.3.3.1 用 and 把两步写成一行 Merge both checks with and

两层嵌套的 if 逻辑清晰,但也可以用逻辑运算符 and 写得更简洁。关键在于 and 的短路特性:它先算左边,只有左边为真才会去算右边。这恰好符合我们的需要——先确认 row 合法,再去算 len(forestMap[row])。如果行不存在,右边根本不会执行,也就不会触发越界。

# 第二部分仅在第一部分为真时才检查(短路求值)
if row < len(forestMap) and col < len(forestMap[row]):
    hero.say(forestMap[row][col])
顺序不能颠倒:必须先写 row < len(forestMap),再写 col < len(forestMap[row])。如果把两者对调,那么当 row 越界时,Python 会先去算 forestMap[row] 来取长度——这本身就是一次越界访问,程序当场崩溃,短路保护也就失效了。
写法A:嵌套 if(清晰)
if row < len(grid):
    if col < len(grid[row]):
        use(grid[row][col])
写法B:and 合并(简洁)
if row < len(grid) and \
   col < len(grid[row]):
    use(grid[row][col])

15.4 不规则行长度

Jagged Rows (Irregular Row Lengths)

15.4.1 每行列数不同(Rows of Different Lengths)

15.4.1.1 参差不齐的"数组的数组" A jagged array of arrays

大多数地图都是整齐的矩形,每行列数相同。但二维数组并不要求这样——正如"雪花莲"关卡强调的:"数组各行的长度不同。所以我们在访问前需要检查坐标的正确性。"这种每行长度不一的数组叫做不规则数组(jagged array)。它正是边界检查的"用武之地":因为你无法用一个统一的列数去套所有行。

# 一个不规则的森林地图:每行列数都不一样
forestMap = [
    [1, 1, 0],          # 第 0 行:3 列(索引 0~2)
    [0, 1],             # 第 1 行:2 列(索引 0~1)
    [1, 1, 0, 0, 1]    # 第 2 行:5 列(索引 0~4)
]
不规则数组:每行长度不同 行0 1 1 0 len=3 行1 0 1 len=2 行2 1 1 0 0 1 len=5
为什么必须用 len(grid[row]):在上图里,第 1 行只有 2 列。如果你想访问 grid[1][2],虽然行 1 存在,但该行没有索引 2 的列——用整体列数去判断就会出错,只有用该行自己的长度 len(grid[1]) 才能得到正确的 2。这就是"逐行测量"的意义。

15.5 实战关卡精讲

Level Strategy & Analysis

这一节聚焦每一关的策略、思路与算法设计,只展示用于说明的关键片段,完整通关程序请你自己动手完成。

关卡 · 梦游者

😴 梦游者 —— 照图建栅栏,遍历规则网格

扣哒世界 梦游者关卡 - koudashijie.com
图2:梦游者——按 Senick 准备的网格图,在标 1 的位置筑栅栏,放行农民、拦住雪人

策略:读图 1 就建栏,别惊醒雪人

本关是本单元的入门关,先复习"规则二维数组"的遍历。村里混进了梦游的雪人,我们要按猎人 Senick 的fenceMap(一个由 0 和 1 组成的网格)建立栅栏系统:1 是要建栅栏的地方,0 是空格。核心约束是不能用战斗手段(会惊醒雪人),只能悄悄筑墙,让熟睡的农民通过、把雪人拦在外面。建完栅栏后英雄要立刻回村,因为雪人能察觉到英雄。

思路拆解

算法是标准的嵌套循环遍历二维数组:外层 for i in range(len(fenceMap)) 逐行,内层 for j in range(len(fenceMap[i])) 逐列。对每个格子,如果 fenceMap[i][j] 为 1(真),就用关卡预先给好的 convertCoor(i, j) 把"行列坐标"转成游戏世界的 (x, y) 坐标,移动过去并 buildXY("fence", ...) 建栏。注意内层用 len(fenceMap[i]) 逐行取列数——这是养成安全习惯的开始,即使本关网格是规则的。

# 关键片段:嵌套循环遍历 fenceMap,遇 1 就建栏
for i in range(len(fenceMap)):
    for j in range(len(fenceMap[i])):
        if fenceMap[i][j]:              # 值为 1 即为真
            pos = convertCoor(i, j)
            hero.moveXY(pos['x'], pos['y'])
            hero.buildXY("fence", pos['x'], pos['y'])
用 range(len(...)) 拿到索引:本关既要用到值(判断是否为 1),又要用到行列号(传给 convertCoor),所以用 range(len(...)) 遍历索引最合适——这样 i、j 就是现成的行列坐标。
建完就撤:关卡特别提醒,雪人能察觉英雄,所以循环结束后要 hero.moveXY(29, 17) 回到安全的村子里。别筑完栏还傻站在雪人旁边。
关卡 · 大炮陆战队

💥 大炮陆战队 —— 按指定坐标精确读取

扣哒世界 大炮陆战队关卡 - koudashijie.com
图3:大炮陆战队——读取着陆区地图的指定格子,谨慎报出坐标与内容,让大炮精确开火

策略:只报正确坐标,谨慎开火

本关用大炮把士兵和"反陷阱弹"发射到山上的村庄广场。着陆区地图 landingMap 是一个字符串二维数组,每个格子是 "peasant"(农民)、"trap"(陷阱)或 "clear"(空地)。你向大炮"说出"行、列和那里的内容,大炮听到 "trap" 或 "clear" 就会开火。策略的核心是精确:广场上有农民,必须只报出示例代码给定的正确坐标,绝不能报错坐标——否则炮弹会打偏或误伤农民。

思路拆解

本关不需要循环,而是练习用 array[i][j] 精确读取指定格子并说出来。关卡给出了一串要处理的坐标点(如第 2 行第 1 列、第 1 行第 0 列……),你只需依次用 landingMap[row][col] 取值、用 hero.say() 报出坐标与内容即可。关键教训是"越界的后果":关卡演示了如果写成 landingMap[9001][19] 这种超范围的索引,就会触发引用/索引错误、大炮 BOOM 打偏。所以务必照给定坐标读取,行列不要写反、不要超范围。

# 关键片段:读取指定坐标的值并报出
cell = landingMap[0][0]              # 第 0 行第 0 列
hero.say("行0列0 " + cell)

hero.say("行2列1 " + landingMap[2][1])
hero.say("行1列0 " + landingMap[1][0])
hero.say("行0列2 " + landingMap[0][2])
hero.say("行1列3 " + landingMap[1][3])
行列不能反:landingMap[2][1] 是"第 2 行第 1 列",而 landingMap[1][2] 是"第 1 行第 2 列"——两者完全不同。第一个方括号永远是行,第二个永远是列。写反了大炮就打错地方。
规则地图 vs 不规则地图:本关的 landingMap 每行列数相同(规则矩形),所以照给定坐标读取就很安全。但关卡也提醒你:"里面的数组可以有不同的长度"——这正是下一关"雪花莲"要面对的挑战,也是本单元边界检查的真正战场。
关卡 · 雪花莲

❄️ 雪花莲 —— 不规则地图上的安全访问

扣哒世界 雪花莲关卡 - koudashijie.com
图4:雪花莲——森林地图每行长度不同,射击前必须先做边界检查,只朝陷阱开火

策略:先检查,后开火,弹药珍贵

本关是本单元的压轴关,把"边界检查"用到极致。森林地图 forestMap 是不规则二维数组:1 是安全的空地,0 是陷阱,而且每行长度都不同。任务是清除陷阱,但弹药珍贵、只够清理陷阱的数量,还不能乱射引起火灾。关卡给了你一串要检查的坐标点数组 cells(其中有些坐标可能越界!),策略核心是:对每个坐标先做边界检查,确认格子存在、且值为 0(陷阱),才报出坐标开火。

思路拆解

算法分三层判断,缺一不可。第一,遍历 cells,从每个字典里取出 row 和 col。第二,做两级边界检查:先 if row < len(forestMap) 确认行存在,再 if col < len(forestMap[row]) 确认这一行有这一列——因为每行长度不同,必须用 len(forestMap[row]) 逐行测量。第三,只有前两关都通过、且 forestMap[row][col] == 0(是陷阱)时,才 hero.say() 报出"行 X 列 Y 开炮!"。这样既不会越界崩溃,也不会浪费弹药朝空地开火。

# 关键片段:遍历坐标点,三层判断后才开火
for cell in cells:
    row = cell["row"]
    col = cell["col"]
    if row < len(forestMap):            # ① 行存在吗?
        if col < len(forestMap[row]):    # ② 这一行有这一列吗?
            if forestMap[row][col] == 0:  # ③ 是陷阱吗?
                hero.say("行" + row + "列" + col + "开炮!")
也可以用 and 合并前两层:如果你觉得三层缩进太深,可以把前两个边界检查用 and 合成一句:if row < len(forestMap) and col < len(forestMap[row]):,再在里面判断是否为陷阱。效果完全相同,代码更扁平。
别跳过检查直接访问:本关的 cells 里故意放了越界坐标。如果你省掉边界检查,直接写 if forestMap[row][col] == 0,一遇到越界坐标就会抛出 IndexError,程序崩溃、任务失败。这正是本单元要教会你的核心教训:坐标来自外部时,访问前必须检查。

15.6 常见错误

Common Mistakes

15.6.1 新手最容易踩的坑(Pitfalls for Beginners)

15.6.1.1 边界检查的典型问题 Typical issues with bounds checking

扣哒世界 边界检查常见错误速查 - koudashijie.com
图5:检查顺序颠倒、用错长度、把 < 写成 <=,是本单元最常见的失误
易错点清单:
  • 检查顺序颠倒——必须先查 row 再查 col;先访问 forestMap[row] 取长度而行已越界,会当场崩溃,短路保护失效。
  • 列检查用错长度——应是 len(forestMap[row])(该行的列数),而不是 len(forestMap)(行数)或 len(forestMap[0])(只有第 0 行的列数)。
  • 把 < 写成 <=——合法索引是 0 到 长度-1,用 索引 < 长度 判断;写成 <= 会正好越界一格。
  • 行列写反——第一个方括号是行、第二个是列;grid[row][col] 和 grid[col][row] 含义完全不同。
  • 完全省略边界检查——当坐标来自随机或外部来源(如关卡给的 cells 数组)时,直接访问极易越界崩溃。
  • 把规则数组的经验套到不规则数组上——以为"每行都一样长",结果在参差不齐的地图上越界。
  • 忘记从字典取值——遍历 cells 时要先 cell["row"]、cell["col"] 取出坐标,别直接把字典当索引用。
  • 混淆"值"和"索引"——forestMap[row][col] == 0 判断的是格子里的值(是不是陷阱),而 row、col 是位置,两者别搞混。

本单元小结 ✨

恭喜你掌握了一项专业程序员每天都在用的防御技能——边界检查!这一单元你理解了索引从 0 开始、最大索引等于长度减一,越过范围就会抛出 IndexError 让程序崩溃。你学会了安全访问二维数组的黄金模式:先用 row < len(grid) 确认行存在,再用 col < len(grid[row]) 确认列存在,两条都成立才读取 grid[row][col];也学会了用 and 的短路特性把两步合成一句。你还认识了每行长度不同的不规则数组,明白了为什么列检查必须用"该行自己的长度"。在"梦游者、大炮陆战队、雪花莲"三关里,你从规则网格的遍历,一步步走到不规则地图上的安全访问。来回顾一下核心概念:

核心概念关键点对应关卡
索引从 0 开始最大索引 = 长度 − 1三关
IndexError 异常越界访问会崩溃终止雪花莲
len 测长度len(grid) 行数 / len(grid[r]) 列数三关
检查行row < len(grid)雪花莲
检查列col < len(grid[row])雪花莲
and 短路合并先真才算右边,顺序不能反雪花莲
不规则行长度每行列数不同需逐行测量雪花莲
精确读取 grid[i][j]第一括号行、第二括号列大炮陆战队
嵌套循环遍历range(len(...)) 逐行逐列梦游者

懂得在伸手之前先看清脚下、在访问之前先确认存在的英雄,才是云峰之上真正稳健的程序员。
继续向雪山之巅挺进吧,勇敢的程序员!🏔️❄️🛡️

扣哒世界 编程知识 · 计算机科学 5(安息云峰):第十五单元

扣哒世界教研团队