在Linux中二维数组的不同 Bash 实现
1. 简介
数组是项目的集合,每个项目都由唯一的键标识。当项目本身是数组时,我们有一个二维 (2D) 数组。
在本教程中,我们将深入研究Linux 下Bash 中的二维数组的概念。更具体地说,我们从不同数组类型的一般结构开始。然后我们讨论 Bash 如何处理这种结构。接下来,我们处理二维阵列的不同模拟。最后,我们总结了一些一般性的警告。
我们使用 GNU Bash 5.0.3 在 Debian 10.10 (Buster) 上测试了本教程中的代码。它是 POSIX 兼容的,应该可以在任何这样的环境中工作。
2. 数组
常规的一维 (1D) 数组只是具有索引的一行或一列数据:
[0][1][2][3]
(A B C D)
在这里,每个项目 ( element ) 都在括号中。上面是方括号中的唯一索引(key)。
要访问特定元素,我们使用它的 index 和 base。基数是数组的名称。例如,我们数组的第0 项等于a。 项目可以是任何受支持的数据类型。
2.1. 二维数组
我们可以像这样创建一个二维数组:
[0][1][2][3]
[0](a)(b)(c)(d)
[1](A)(B)(C)(D)
基本上,原始数组中的第0项现在是aA列——一个数组。我们使用第二个索引访问该数组的元素。例如,项目*[0,1]是A*。其他项目的情况类似。
二维数组中的行和列的长度通常是固定常数。
2.2. 混合阵列
使用固定的数组维度,我们可以转换上一节中的二维数组:
[0][1][2][3][4][5][6][7]
(a)(b)(c)(d)(A)(B)(C)(D)
我们将 2 行 4 列的 2D 数组映射到 8 (2*4) 个项目的 1D 列表中。要计算元素的索引,我们使用公式*(COLUMNS*row+col)。它“跳过”了COLUMNS元素行。之后,它获取当前行的 col元素。
重要的是,索引也称为键。在某些编程语言中,键的数据类型可以像项的数据类型一样变化。
几乎所有的编程语言都包含数组数据类型。事实上,在大多数弱类型语言中都有一种声明数组的方法。巴什也不例外。
Bash 中的数组项可以是任何东西……除了数组。那么我们如何在 Bash 中创建二维数组呢?
4. 2D Bash 数组
出于我们的目的,我们将最小化 2D 数组的定义。在本文中,它是我们可以使用两个单独的值一致地索引的任何数组。
由于 Bash 不支持开箱即用的 2D 数组,因此我们必须想出自己的方式来使用它们。让我们探索常见的实现。
4.1. 项目 2D 阵列模拟
在 Bash 中,数组只能通过分隔符与字符串区分开来。将数组作为项目的最简单方法之一是当场将它们从字符串转换:
$ sep=','
$ declare -a alpha=()
$ alpha+=("a${sep}b")
$ alpha+=("c${sep}d")
$ row=0
$ col=1
$ IFS="$sep" read -ra alpharow < <(printf '%s' "${alpha[$row]}")
$ echo "${alpharow[$col]}"
b
最初,我们选择逗号作为列分隔符*$sep*。接下来,定义数组,逐行追加。在这种情况下,第一行是*“a,b”,第二行是“c,d”*。
在结构上,IFS (内部字段分隔符)特殊的 shell 变量开始发挥作用。它的默认值包含空格、制表符和换行符。IFS允许像read 这样的内置函数来分隔数组项。
最后,上面的代码片段使用*$row和$col来索引主数组,尽管分两步。首先,整行$row在$sep上 被分割成一个新的数组$alpharow*。之后,$alpharow使用列*$col*进行索引,该列返回请求的元素。对于常量数组,这已经足够了。
显然,我们不能直接将分隔符$sep*用作普通字符*。可以通过多种方式将其影响降至最低:
- 使用稀有或转义字符作为分隔符
- 加倍/乘以或以其他方式增加每个实际的非分隔分隔符,相应地处理
- 使用字符组合作为分隔符
接下来,我们将讨论一个不受此特定缺点影响的实现。
4.2. 关联二维阵列模拟
这是 Bash 中关联数组的示例:
$ declare -A alpha=([0,0]=a [0,1]=b [1,0]=c [1,1]=d)
$ echo "${alpha[@]}"
c d b a
注意我们如何给出两个索引的外观。因此,我们理论上可以想象数组看起来像这样:
[0][1]
[0](/a)(b)
[1](/c)(d)
实际上,这些索引只是字符串键:
$ for k in "${!alpha[@]}"; do
printf "alpha[%s]=%s\n" "$k" "${alpha[$k]}"
done
alpha[1,0]=c
alpha[1,1]=d
alpha[0,1]=b
alpha[0,0]=a
$ echo "${alpha["1,0"]}"
c
我们可以在循环中生成键或像数字索引一样计算它们。
然而,即使是微小的偏差也会导致问题:
$ for (( col=0; col<2; col++ )); do
printf "alpha["1,%s"]=%s\n" "$col" "${alpha["1,$col"]}"
done
alpha[1,0]=c
alpha[1,1]=d
$ for (( col=0; col<2; col++ )); do
printf "alpha["1,%s"]=%s\n" "$col" "${alpha["1, $col"]}"
done
alpha[1,0]=
alpha[1,1]=
这两个循环之间的区别非常微妙——逗号和*$col*之间的空格。
在管理一个已经很复杂的结构时,这样的错综复杂并没有帮助。因此,存在一种更简单、更通用的解决方案。
4.3. 混合二维阵列仿真
我们可以将上述结构声明为常规数组:
$ ROWS=2
$ COLUMNS=2
$ declare -a alpha=()
$ alpha+=(a b)
$ alpha+=(c d)
$ echo "${alpha[@]}"
a b c d
到目前为止,我们有一个具有已知精确尺寸(COLUMNS,ROWS)的普通一维数组。要将其完全“转换”为 2D,索引计算也需要更改:
$ row=0
$ col=1
$ index=$((COLUMNS*$row+$col))
$ echo "${alpha[$index]}"
我们已经探索了上面的公式。与关联数组中的字符串不同,这里的索引仍然是 numeric。这是一个明显的优势,因为数字运算语法不太敏感。空格、前导零和其他此类伪影不会影响计算。
一维二维数组仿真有两个条件可以工作:
- 数组应该可以映射到一个精确的“矩形”,即COLUMNS*ROWS = ELEMENTS
- 我们事先知道数组的确切尺寸
克服这些限制需要牺牲一些自由。为此,我们可以结合已经讨论过的两种方法。
4.4. 复杂的二维数组实现
我们可能会放弃两个字符作为分隔符。例如逗号 - 用于行,换行 - 用于列。在这些条件下,整个二维数组最初可以是一个字符串:
$ alpharaw='a,b,c,d
e,f,g
h,i,,k
l,,n,o'
接下来,我们使用readarray 将“array to be”字符串*$alpharaw*中的行分隔到一个新数组中:
$ readarray -t -d $'\n' alpharows < <(printf '%s' "$alpharaw")
最后,我们创建用于读取、写入、删除和插入元素的 Bash 函数。我们将只提供一个读取函数的示例。它使用上面几节中讨论的概念:
$ get_element () {
alpharaw="$1"
row="$2"
col="$3"
sep="${4:-,}"
local alpharow alpharows
IFS=$'\n' readarray -t alpharows < <(printf '%s' "$alpharaw")
if [[ $row -ge ${#alpharows[@]} ]]; then
echo "Bad row."
return
fi
IFS="$sep" read -ra alpharow < <(printf '%s' "${alpharows[$row]}")
if [[ $col -ge ${#alpharow[@]} ]]; then
echo "Bad column."
return
fi
echo "${alpharow[$col]}"
}
$ get_element "$alpharaw" 3 2
n
虽然实现更复杂,但它的使用更加健壮。问题的可能性要小得多。
5. 注意事项
我们已经在上一节中提到了实现的缺点。为简洁起见,我们列举了主要的:
- 实现复杂度
- 性能问题
- 稀疏数组
- 复杂而脆弱的语法
- 分隔符转义
当然,这个列表并不完整,因为二维数组是复杂的结构。
在选择实现时,我们必须考虑它对我们的需求的利弊。