第八单元:多参数函数

Unit 8: Functions with Multiple Parameters
边际森林 Border Forest · 返回荆棘农场 → 函数巡逻

8.1 从单参数到多参数

From One Parameter to Many

8.1.1 一个参数不够用(One Parameter Isn't Enough)

8.1.1.1 当函数需要多份信息 When a function needs more than one piece of information

上一单元你学会了给函数加一个参数,让它能处理不同的目标。但现实里,很多任务需要同时告诉函数好几件事。比如"去某个坐标建陷阱",函数光知道"建什么"还不够,还得知道"建在哪儿"——这就需要 xy 两个坐标。一个参数装不下这么多信息。

"返回荆棘农场"关卡开门见山地点破:"你的函数可以定义不止一个参数。" 这正是本单元的核心本领——让你自己定义的函数接收多个参数,从而一次性接收"做什么 + 在哪里 + 怎么做"的全部指令。

CodeCombat 多参数函数引入示意图 - codecombat.cn
图1:单参数函数像单孔插座,多参数函数像多孔插座——能同时接收多份信息

8.1.2 内置函数早有多参(Built-in Functions Already Take Several)

8.1.2.1 moveXY、buildXY 就是榜样 moveXY and buildXY are the examples

关卡用一个你早就熟悉的例子安慰你:"就像 moveXY 接受两个参数那样,你创建的函数也可以定义不止一个参数!" 仔细想想,hero.moveXY(35, 34) 里的 3534 就是两个参数;而 hero.buildXY("fire-trap", 40, 20) 更厉害,它有三个参数:一个字符串和两个数字。你一直在使用多参数函数,现在只是要自己来定义它们。

内置函数参数个数参数类型
hero.moveXY(x, y)2 个数字, 数字
hero.buildXY(type, x, y)3 个字符串, 数字, 数字
hero.attack(enemy)1 个对象

8.1.3 形参与实参再回顾(Parameters vs. Arguments, Again)

8.1.3.1 两个名字,分清场合 Two terms, two places

关卡再次强调了上单元的关键区分:"形参(parameter)是在函数定义里的参数。实参(argument)是函数被调用时传进的实际参数值!" 在多参数时代,这个区分依然成立,只不过现在每个位置都有一对形参/实参的对应关系

术语英文出现位置含义
形参Parameter函数定义的括号里一串用逗号分隔的变量名
实参Argument函数调用的括号里一串用逗号分隔的具体值

8.2 多参数语法

Multi-Parameter Syntax

8.2.1 用逗号分隔形参(Separate Parameters with Commas)

8.2.1.1 在括号里列出多个名字 List multiple names in the parentheses

定义多参数函数,只需在 def 的括号里写下多个形参名,用逗号隔开。然后在函数体里,这几个形参都能像普通变量一样使用:

# 函数 maybeBuildTrap 定义了两个参数 x 和 y!
def maybeBuildTrap(x, y):
    hero.moveXY(x, y)          # 用 x、y 作为移动坐标
    enemy = hero.findNearestEnemy()
    if enemy:
        hero.buildXY("fire-trap", x, y)  # 再用 x、y 作为建造坐标
💡 多个空盒子,按顺序排好队

定义 maybeBuildTrap(x, y) 时,相当于准备了两个贴好标签的空盒子:第一个叫 x,第二个叫 y。当你调用 maybeBuildTrap(43, 50),系统会按顺序43 装进 x,把 50 装进 y。盒子的顺序不能乱,否则装错了东西。

8.2.2 顺序必须一一对应(Order Must Match)

8.2.2.1 位置决定身份 Position determines identity

"封闭的十字路口"关卡郑重提醒:"调用函数时,参数顺序需要同定义时保持一致。" 这是多参数最关键、也最容易出错的规则。第一个实参对应第一个形参,第二个对应第二个……严格按位置一一对接。

maybeBuildSomething ( "fire-trap" , 40 , 20 ) 1st 2nd 3rd def maybeBuildSomething ( typeToBuild , x , y ):
顺序错位的后果:如果你把 maybeBuildSomething(40, "fire-trap", 20) 这样写,函数会以为 40 是要建造的"类型"、"fire-trap" 是 x 坐标——完全乱套,程序会出错。

8.2.3 参数类型可以各异(Parameters Can Have Different Types)

8.2.3.1 字符串、数字、对象混着来 Strings, numbers, objects all welcome

关卡明确说明:"函数可以定义很多参数,类型也可以各异,诸如字符串、数字等。"buildXY 为例,它的三个参数里 typeToBuild 是字符串,xy 是数字。同一个函数的不同形参,可以承载完全不同种类的信息。

8.3 多参数的威力

The Power of Multiple Parameters

8.3.1 坐标参数 x, y(Coordinate Parameters)

8.3.1.1 一个函数巡逻整张地图 One function patrols the whole map

最经典的多参数用法,就是把 xy 坐标作为参数传入。这样同一个函数能被带去地图上的任何位置执行任务。定义一次"去某点干活"的逻辑,就能在所有入口、所有标记处反复调用,每次只换坐标。

def maybeBuildTrap(x, y):
    hero.moveXY(x, y)
    enemy = hero.findNearestEnemy()
    if enemy:
        hero.buildXY("fire-trap", x, y)

while True:
    maybeBuildTrap(43, 50)   # 上方入口
    maybeBuildTrap(25, 34)   # 左侧入口
    maybeBuildTrap(43, 20)   # 底部入口
妙处所在:注意 xy 在函数里被用了两次——先用于 moveXY 移动,再用于 buildXY 建造。同一对坐标,既决定"走到哪",又决定"建在哪"。这种复用让代码格外紧凑。

8.3.2 字符串+数字混搭(Mixing Strings and Numbers)

8.3.2.1 一个函数,造围栏也造陷阱 One function builds both fences and traps

当你把"建造类型"也变成参数,函数就更万能了。"封闭的十字路口"用 maybeBuildSomething(typeToBuild, x, y) 把建造物类型连同坐标一起参数化——同一个函数,既能造 "fence" 又能造 "fire-trap"。关卡点评道:"现在你可以为 'fence' 和 'fire-trap' 使用同一个函数,而无需两个不同函数,这样代码就会简洁很多!"

def maybeBuildSomething(typeToBuild, x, y):
    hero.moveXY(x, y)
    enemy = hero.findNearestEnemy()
    if enemy:
        hero.buildXY(typeToBuild, x, y)  # 类型也来自参数

8.3.3 重构:化繁为简(Refactoring: Simplify Complexity)

8.3.3.1 把重复的内层逻辑抽成函数 Extract repeated inner logic into a function

多参数函数还有一项重要用途:重构(Refactoring)。当代码里塞满了嵌套的 if 语句、显得又长又乱时,可以把最内层的核心逻辑抽取出来,封装成一个带参数的函数,主循环里只用一行调用它。代码立刻变得清爽易读。

"Agrippa 重构"关卡正是教这个:原本一大堆嵌套判断里"该劈斩还是该普攻"的逻辑,被抽进 cleaveOrAttack(enemy) 函数,主循环只需在合适时机调用它一次。关卡总结:"函数能够帮你简化代码,使代码更易读。"

8.4 实战关卡精讲

Level Strategy & Analysis

这一节聚焦每一关的策略、思路与算法设计。看懂"为什么要这样设计参数",你就能自己写出完整解法。

8.4.1 返回荆棘农场(Thornbush Farm)

8.4.1.1 策略:一个函数守住所有入口 Strategy: one function guards every entrance

CodeCombat 返回荆棘农场关卡 - codecombat.cn
图2:返回荆棘农场——同一个 maybeBuildTrap(x, y) 守住上、左、下三个入口

这是你第一次定义带两个参数的函数。农场有多个入口,食人魔可能从任意一个冲进来。难点在于意识到每个入口的处理逻辑完全相同——走过去、看有没有敌人、有就建陷阱——只是坐标不同。算法思路是:把这套逻辑封进 maybeBuildTrap(x, y),主循环里依次传入各入口坐标即可。

思路要点:本关有 A、B 多个变体,区别仅在于入口坐标不同(如 (20, 34)(38, 20)(56, 34) 等)。函数体一字不改,只换调用时的实参——这正体现了多参数函数"一次定义、处处复用"的价值。

8.4.2 Agrippa 重构(Agrippa Refactored)

8.4.2.1 策略:把嵌套逻辑抽成函数 Strategy: extract nested logic into a function

CodeCombat Agrippa 重构关卡 - codecombat.cn
图3:Agrippa 重构——把"劈斩或普攻"的内层逻辑封装成独立函数

这一关的精髓不是打法,而是代码组织。原本的解法有很多层嵌套 if,读起来费劲。重构的思路是:找出最内层那段"判断技能冷却、决定劈斩还是普攻"的代码,把它抽成 cleaveOrAttack(enemy) 函数。主循环负责"找敌人、判断距离",到了出手环节只需一行 cleaveOrAttack(enemy)

def cleaveOrAttack(enemy):
    if hero.isReady("cleave"):
        hero.cleave(enemy)   # 技能好了 → 劈斩
    else:
        hero.attack(enemy)   # 否则 → 普攻
重构思想:"重构"指在不改变功能的前提下改善代码结构。把重复或冗长的逻辑提炼成函数,是程序员最常用的整理手段。代码更短、更清晰、更易维护——这就是函数的核心价值之一。

8.4.3 封闭的十字路口(Closed the Crossroads)

8.4.3.1 策略:三参数 + 顺时针巡逻 Strategy: three parameters + clockwise patrol

CodeCombat 封闭的十字路口关卡 - codecombat.cn
图4:封闭的十字路口——上下通道造火焰陷阱,左右通道造围栏

这一关把参数升级到三个,而且类型不同。任务要求顺时针绕村庄移动,在上下通道建 "fire-trap"、在左右通道建 "fence"。算法巧思在于:不为两种建造物各写一个函数,而是把"建什么"也作为参数。于是 maybeBuildSomething(typeToBuild, x, y) 一个函数通吃,调用时传入不同的类型字符串和坐标即可。

算法妙处:把"差异点"提炼为参数,是设计通用函数的关键思维。这里上下/左右的差异有两处——建造物类型不同、坐标不同。两个差异都变成参数后,函数体就完全统一了。差异越多被参数化,函数复用范围越广。

8.4.4 贪婪陷阱(Greed Traps)

8.4.4.1 策略:靠"诱饵"判断而非敌人 Strategy: judge by the bait, not the enemy

CodeCombat 贪婪陷阱关卡 - codecombat.cn
图5:贪婪陷阱——巡逻四角,只在看到硬币(诱饵)时才建陷阱

这一关需要自己从零编写多参数函数,没有现成范例。战术核心很巧妙:不收集硬币,而是把硬币当作"诱饵",只在看到硬币时才在该处建火焰陷阱(因为农民也会游荡,不能见人就建)。算法思路是 maybeBuildTrap(x, y):移动到坐标 → findNearestItem() 找硬币 → 若有硬币就 buildXY("fire-trap", x, y)。主循环巡逻四个角的通道。

关键算法点:注意这里的判断条件从"有没有敌人"换成了"有没有硬币"。同样是多参数 + 坐标巡逻的模式,但判断依据可以灵活替换。理解了模式的骨架,就能套用到各种新场景。

8.4.5 最后的大炮(概念挑战 · The Final Kithmaze)

8.4.5.1 策略:自己定义带坐标的检查点函数 Strategy: define your own checkpoint function

CodeCombat 最后的大炮关卡 - codecombat.cn
图6:最后的大炮——巡逻每个 X 标记,发现敌人就攻击

这是一道概念挑战,要求独立定义并使用带多个参数的函数。任务是在循环里移动到每个 X 标记,发现敌人就攻击。算法思路很直接:写 checkPoint(x, y)——移动到 (x, y) → 找最近敌人 → 有就攻击。主循环里把各个检查点坐标作为实参逐一传入。

def checkPoint(x, y):
    hero.moveXY(x, y)
    enemy = hero.findNearestEnemy()
    if enemy:
        hero.attack(enemy)

while True:
    checkPoint(10, 30)
    checkPoint(30, 10)
    checkPoint(50, 30)

8.4.6 函数巡逻(组合挑战 · Patrol Buster)

8.4.6.1 策略:综合运用所有函数本领 Strategy: combine everything you've learned

CodeCombat 函数巡逻关卡 - codecombat.cn
图7:函数巡逻——定义、调用、带参数,三项本领一次用全

这是一道组合挑战,目标是把"定义函数 + 使用函数 + 带参数的函数"三项技能全部用上,巡逻所有 X 标记并击败食人魔。算法思路与"最后的大炮"一脉相承:定义 patrol(x, y) 函数完成"移动—找敌—攻击",主循环依次传入各标记坐标。一个干净的多参数函数,就能优雅地完成整张地图的巡逻。

挑战意义:组合挑战没有标准答案,鼓励你自由设计。无论坐标怎么排、巡逻顺序怎么定,只要正确运用了多参数函数,就达成了目标。这是对本单元所有知识的一次综合检验。

8.5 常见错误

Common Mistakes

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

8.5.1.1 多参数环节的典型问题 Typical issues with multiple parameters

CodeCombat 多参数常见错误示意 - codecombat.cn
图8:实参顺序错位、个数不匹配,是多参数环节最常见的失误
易错点清单:
  • 实参顺序错位——把 maybeBuildSomething(40, "fence", 34) 写错了次序,函数会把数字当类型、字符串当坐标。
  • 实参个数对不上——定义了 3 个形参却只传 2 个实参(或反之),函数收不全或收太多信息。
  • 形参之间忘了逗号——写成 def maybeBuildTrap(x y):,缺少逗号会导致语法错误。
  • 形参名前后不一致——定义时叫 x, y,函数体里却写成了别的名字。
  • 建造物类型字符串拼错——"fire-trap""fence" 必须用引号且拼写准确。
  • 给自定义函数误加 hero.——调用 checkPoint(10, 30) 不要写成 hero.checkPoint(...)

本单元小结 ✨

恭喜你完成边际森林第八单元!你的函数从"只能接收一份信息"升级到了"同时接收多份信息、处理复杂任务"。配合重构思想,你已经能写出既灵活又整洁的代码。来回顾一下核心概念:

核心概念关键点对应关卡
多参数函数括号里写多个形参,逗号分隔返回荆棘农场
顺序对应实参按位置一一对接形参封闭的十字路口
类型各异字符串、数字、对象都能当参数封闭的十字路口
坐标参数 x, y一个函数走遍全图返回荆棘农场、贪婪陷阱
类型参数化同一函数造围栏也造陷阱封闭的十字路口
重构抽取内层逻辑、简化代码Agrippa 重构
形参当变量复用同一参数在函数里可多次使用返回荆棘农场
概念/组合挑战独立设计多参数函数最后的大炮、函数巡逻

会传多参、懂重构的英雄,才能驾驭真正复杂的战场。
继续守护边际森林吧,勇敢的程序员!🌲🛡️🎯

CodeCombat 编程知识 · 边际森林:第八单元

CodeCombat教研团队