在函数之间传递数组与关联映射
使用 nameref 和间接展开,安全地将复杂数据结构传入函数并从函数中传出。
在函数之间传递数组与关联映射 是 CoddyKit 上的免费 Linux Command Line & Bash Scripting Mastery 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Linux Command Line & Bash Scripting Mastery 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Linux Command Line & Bash Scripting Mastery 课程共包含 4 节课。
为什么将数组传递给函数很棘手
在 Bash 中,数组和关联数组不是一等值。尝试将数组传递给函数时,不能直接使用 $my_array——这样只会展开第一个元素。
请看下面这个有问题的尝试:
my_func "${my_array[@]}"函数接收到的是由单词组成的扁平列表。它无法知道一个元素在哪里结束、下一个元素在哪里开始,并且数组结构也完全丢失了。
Bash 提供了两种可靠的机制来传递复杂数据结构:
- 名称引用(
declare -n)——通过名称引用另一个变量(Bash 4.3 及更高版本) - 间接展开(
${!varname[@]})——访问名称存储在另一个变量中的变量(较旧版本的 Bash)
本课将介绍这两种技术,帮助您编写能够处理数组和映射的健壮、可复用函数。
使用名称引用按名称传递数组
现代 Bash 中最简洁的方法是使用名称引用:将数组的名称作为字符串传递,然后在函数内部声明一个引用该数组的局部变量。
关键语法:
declare -n ref="$1"——现在,ref就是传入的第一个参数所指定变量名的别名- 您可以像操作原始数组一样读取、遍历和修改
ref
这样可以避免复制整个数组,并且函数可以处理任意大小的数组。
#!/usr/bin/env bash
print_array() {
declare -n arr="$1" # nameref: arr -> caller's variable
echo "Array has ${#arr[@]} elements:"
for elem in "${arr[@]}"; do
echo " - $elem"
done
}
fruits=(apple banana cherry)
print_array fruits在函数内部修改调用者的数组
由于名称引用是真正的别名,通过名称引用进行写入会修改调用者的原始变量。您无需回显值,也无需使用全局变量。
因此,名称引用非常适合在原地转换数组的函数,例如规范化字符串、筛选元素或排序。
#!/usr/bin/env bash
to_uppercase() {
declare -n _arr="$1"
for i in "${!_arr[@]}"; do
_arr[$i]="${_arr[$i]^^}" # ^^ = uppercase in Bash 4+
done
}
words=(hello world bash)
echo "Before: ${words[@]}"
to_uppercase words
echo "After: ${words[@]}"通过名称引用返回新数组
函数还可以通过将数组名称作为输出参数接收,来写入调用者提供的数组。调用者创建一个空数组,传入它的名称,然后由函数填充该数组。
这种模式类似于 C 语言中传递指向结果缓冲区的指针这一经典惯用法,只不过这里完全通过 shell 实现。
#!/usr/bin/env bash
get_even_numbers() {
local -a source=("$@")
# Last argument is the output array name — pop it off
local out_name="${source[-1]}"
unset 'source[-1]'
declare -n _out="$out_name"
_out=() # clear
for n in "${source[@]}"; do
(( n % 2 == 0 )) && _out+=("$n")
done
}
declare -a evens
get_even_numbers 1 2 3 4 5 6 7 8 evens
echo "Evens: ${evens[@]}"名称引用冲突陷阱
名称引用有一个关键陷阱:如果局部名称引用变量与调用者的变量同名,Bash 就会进入无限自引用循环,导致脚本失败。
例如,如果调用者将数组命名为 arr,而函数执行 declare -n arr="$1",名称引用 arr 就会指向自身。
避免这种情况的约定:为内部名称引用变量添加下划线前缀(例如 _arr、_ref、_out)。调用者几乎不会使用这类名称,因此发生冲突的可能性极低。
#!/usr/bin/env bash
# DANGER: caller uses variable named 'arr'
arr=(one two three)
bad_func() {
declare -n arr="$1" # collision! arr refers to itself
echo "${arr[@]}"
}
good_func() {
declare -n _arr="$1" # underscore prefix avoids collision
echo "${_arr[@]}"
}
# bad_func arr # would hang / error
good_func arr按名称传递关联数组(映射)
关联数组(declare -A)与名称引用配合使用时,工作方式完全相同。将映射的名称作为字符串传递,并在函数内部声明名称引用,即可完整读写所有键值对。
对于必须在多个库函数之间共享的配置映射、选项解析器和查找表,这种方式非常有用。
#!/usr/bin/env bash
print_map() {
declare -n _map="$1"
echo "Map contents:"
for key in "${!_map[@]}"; do
printf ' %-15s => %s\n' "$key" "${_map[$key]}"
done
}
declare -A config
config[host]="localhost"
config[port]="5432"
config[db]="myapp"
print_map config使用名称引用合并两个映射
由于名称引用为函数提供了实时别名,因此可以简洁地实现映射合并等实用工具。按名称传入目标映射和一个或多个源映射;函数遍历源映射的键,并将它们写入目标映射。
名称引用使您能够编写一个通用的 merge_map 函数,用于处理脚本中的任意关联数组。
#!/usr/bin/env bash
merge_map() {
# merge_map dest src
declare -n _dest="$1"
declare -n _src="$2"
for key in "${!_src[@]}"; do
_dest["$key"]="${_src[$key]}"
done
}
declare -A defaults=([timeout]=30 [retries]=3 [verbose]=false)
declare -A overrides=([retries]=5 [log_level]=debug)
merge_map defaults overrides
for k in "${!defaults[@]}"; do
echo "$k = ${defaults[$k]}"
done间接展开:兼容较旧版本的 Bash
如果必须支持Bash 4.0–4.2(或较旧版本 macOS 的默认 shell),则无法使用 declare -n。替代方案是使用 ${!varname} 进行间接展开。
间接展开会将 varname 的值当作变量名进行求值,并返回该变量的值。对于数组,还需要结合特殊语法才能获取所有元素。
与名称引用相比,它有以下限制:
- 实际上只能读取(写回需要使用
eval,这会带来注入风险) - 如果不使用
eval,就无法安全地遍历关联数组的键 - 只要可能,就应使用名称引用;仅在必要时才退回使用间接展开
#!/usr/bin/env bash
sum_array() {
local arr_name="$1"
# Indirect: build a temporary copy by expanding arr_name[@]
local -a items
eval "items=( \"\${${arr_name}[@]}\" )"
local total=0
for n in "${items[@]}"; do
(( total += n ))
done
echo "$total"
}
numbers=(10 20 30 40)
result=$(sum_array numbers)
echo "Sum: $result"构建模块化脚本库
在专业的 Bash 项目中,实用函数会存储在库文件中(例如 lib/array_utils.sh),由需要它们的脚本加载。
具体模式如下:
- 每个库文件只定义函数,不产生顶层副作用
- 使用哨兵变量防止重复加载:
[[ -n $__LIB_LOADED ]] && return; __LIB_LOADED=1 - 使用者脚本通过
source或.加载库 - 函数通过名称引用交换复杂数据,使所有逻辑保持自包含
这样得到的脚本易于测试、复用和维护,而不是一次性的 shell 小技巧。
实际示例:返回映射的选项解析器
一个实际应用场景是:选项解析器将结果填充到调用者提供的关联数组中。调用者将数组名称作为第一个参数传入,后面接实际的 "$@" 参数。
这种模式使任何脚本都能复用同一个解析器,无需复制代码,也不依赖全局变量。
#!/usr/bin/env bash
parse_opts() {
declare -n _opts="$1"
shift
while [[ $# -gt 0 ]]; do
case "$1" in
--host) _opts[host]="$2"; shift 2 ;;
--port) _opts[port]="$2"; shift 2 ;;
--user) _opts[user]="$2"; shift 2 ;;
*) echo "Unknown option: $1" >&2; shift ;;
esac
done
}
declare -A opts=([host]=localhost [port]=5432 [user]=admin)
parse_opts opts --host db.example.com --port 3306
echo "Connecting to ${opts[host]}:${opts[port]} as ${opts[user]}"在一个函数中组合输入和输出名称引用
函数可以同时持有多个名称引用,一个用于输入,一个用于输出。这样就能实现纯转换函数:从源结构读取数据,计算结果,再将结果写入目标结构,全程不接触全局状态。
为每个名称引用使用不同的下划线前缀名称,即使调用者的变量名称很简单,也能避免冲突。
#!/usr/bin/env bash
filter_map_by_key_prefix() {
# Usage: filter_map_by_key_prefix src_map dest_map prefix
declare -n _src="$1"
declare -n _dst="$2"
local prefix="$3"
_dst=()
for key in "${!_src[@]}"; do
if [[ $key == ${prefix}* ]]; then
_dst["$key"]="${_src[$key]}"
fi
done
}
declare -A settings=(
[db_host]=localhost [db_port]=5432
[cache_ttl]=300 [cache_driver]=redis
[app_debug]=true
)
declare -A db_settings
filter_map_by_key_prefix settings db_settings db_
for k in "${!db_settings[@]}"; do
echo "$k = ${db_settings[$k]}"
done知识检查:名称引用与数组传递
请检验您对在 Bash 函数之间传递数组和映射的理解。
回顾:在 Bash 中传递复杂数据结构
本课介绍了以下内容:
- 数组无法按值传递——应改为将变量的名称作为字符串传递。
declare -n ref="$varname"会创建名称引用:它是调用者变量的实时别名。读取和写入都会作用于原始变量。需要 Bash 4.3 及更高版本。- 当内部变量与外部变量同名时,就会发生名称引用冲突。为内部名称引用添加前缀即可避免冲突(例如
_arr、_map)。 - 输出参数通过传入空结果数组或映射的名称来工作;函数通过名称引用填充它。
- 在一个函数中使用多个名称引用,可以实现没有全局副作用的纯输入到输出转换。
- 间接展开(
${!varname[@]}+eval)是 Bash < 4.3 中较旧的备用方案,但存在注入风险,并且实际上只能读取。 - 将这些技术与加载的库文件结合起来,即可构建模块化、可复用、易于测试的 Bash 代码库。
常见问题解答
「在函数之间传递数组与关联映射」课时是免费的吗?
是的 — 「在函数之间传递数组与关联映射」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Linux Command Line & Bash Scripting Mastery 课程的其余内容,请升级到 CoddyKit PRO。 Linux Command Line & Bash Scripting Mastery 课程共包含 4 节课。
「在函数之间传递数组与关联映射」这节课中我会学到什么?
使用 nameref 和间接展开,安全地将复杂数据结构传入函数并从函数中传出。 你通过在浏览器中直接运行的动手代码来练习 Linux Command Line & Bash Scripting Mastery,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Linux Command Line & Bash Scripting Mastery 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Linux Command Line & Bash Scripting Mastery 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。
「在函数之间传递数组与关联映射」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Linux Command Line & Bash Scripting Mastery 课中编写并运行代码吗?
能。每节 Linux Command Line & Bash Scripting Mastery 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 使用局部作用域与返回码设计函数
- 构建并引入可复用的 Bash 库
- 使用 getopts 解析标志与参数
- 在函数之间传递数组与关联映射