为什么 Bash 关联数组不保持索引顺序?

Why don't Bash associative arrays maintain index order?

我正在创建关联数组以在 for 循环中处理,但我在索引顺序中得到了一些奇怪的结果。请查看此示例脚本:

#!/bin/bash
declare -A test1=(
    [d]=1w45
    [e]=2dfg
    [m]=3df
    [o]=4df
)

declare -A test2=(
    [d1]=1w45
    [e2]=2dfg
    [m3]=3df
    [o4]=4df
)

declare -A test3=(
    [1d]=1w45
    [2e]=2dfg
    [3m]=3df
    [4o]=4df
)

echo ${!test1[@]}
echo ${!test2[@]}
echo ${!test3[@]}

输出将是

$ ./test 
d e m o
o4 m3 e2 d1
3m 4o 1d 2e

为什么项目的顺序会改变?以及如何绕过这种行为?提前致谢!

根据评论,可以绕过此行为。

order=(d1 e2 m3 o4)
declare -A test2=(
    [d1]=1w45
    [e2]=2dfg
    [m3]=3df
    [o4]=4df
)
for key in ${order[@]}; { echo $key ${test2[$key]}; }

d1 1w45
e2 2dfg
m3 3df
o4 4df

或者那个

declare -A test3=(
    [order]="1d 2e 3m 4o"
    [1d]=1w45
    [2e]=2dfg
    [3m]=3df
    [4o]=4df
)
for key in ${test3[order]}; { echo $key ${test3[$key]}; }

1d 1w45
2e 2dfg
3m 3df
4o 4df

有没有更好的方法?

更新,根据公认的答案,如果你需要在 for 循环中严格排序,关联数组不是正确的选择,最好使用这样的东西:

key=(d1   e2   m3  o4 ) 
val=(1w45 2dfg 3df 4df)

for i in ${!key[@]}; {
    echo ${key[$i]} ${val[$i]}
}

或者这个

key_val=(
    "d1 1w45"
    "e2 2dfg"
    "m3 3df"
    "o4 4df")  

for item in "${key_val[@]}"; {
    sub=($item)
    echo ${sub[0]} ${sub[1]}
}

或者那个

keys=(d1      e2      m3     o4    )
      d1=1w45 e2=2dfg m3=3df o4=4df

for key in ${keys[@]}; {
    echo $key ${!key}
}

Why order of items is changing?

因为通常关联数组不会自然地维护插入顺序:基于树的数组使用自然(排序)排序,而哈希映射使用它们的哈希函数登陆键的任何地方(这可以出于安全原因,每个进程甚至每个地图都是随机的。

后者还解释了为什么在您添加新项目时项目的顺序甚至会发生变化:不仅可以在现有项目之间插入新项目,而且当必须调整 hashmap 的大小时,整个序列将 "reshuffled" 因为条目被重新散列并移动到它们的新位置。

有些语言会显式添加排序作为一项功能(通常使用双向链表),或者使用自然排序 hashmap,在这种情况下插入顺序会保持不变,但是你不能假设这个 属性 成立,除非语言保证它。 bash 没有。

Why don't bash associative arrays maintain index order?

因为它们的设计目的不是这样做。

Why order of items is changing?

Bashassociative array implementation uses a hash library and stores hashes of indexes. These hashes are stored in buckets with 128 default number of buckets. The hash is calculated with the function hash_string() using a simple multiplication and a bitwise XOR. The keys of the associative array are listed in the order buckets appear. Bucket number is calculated通过key的hash值与桶数减1associative array implementation uses a hash library and stores hashes of indexes. These hashes are stored in buckets with 128 default number of buckets. The hash is calculated with the function hash_string() using a simple multiplication and a bitwise XOR. The keys of the associative array are listed in the order buckets appear. Bucket number is calculated

我编译了 bash commit 6c6454cb18d7cd30b3b26d5ba6479431e599f3ed 并且你的脚本输出对我来说:

$ ./test 
o m e d
d1 e2 m3 o4
1d 3m 2e 4o

所以我复制了hash_string()函数,写了一个小的C程序,输出key的bucket号,编译执行:

#include <stdio.h>

#define FNV_OFFSET 2166136261
#define FNV_PRIME 16777619

unsigned int
hash_string (s)
     const char *s;
{
  register unsigned int i;

  for (i = FNV_OFFSET; *s; s++)
    {
      i *= FNV_PRIME;
      i ^= *s;
    }

  return i;
}

int main() {
    const char *s[] = {
        "o", "m", "e", "d",
        "d1", "e2", "m3", "o4",
        "1d", "3m", "2e", "4",
    };
    for (int i = 0;  i < sizeof(s)/sizeof(*s); ++i) {
        printf("%3s %3d\n",
            s[i], 
            hash_string(s[i]) & (128 - 1));
    }
}

程序输出两列,键和键的桶号(添加了额外的空行):

  o 112
  m 114
  e 122
  d 123

 d1  16
 e2  60
 m3  69
 o4 100

 1d  14
 3m  41
 2e  50
 4o  94

输出键的顺序是使用它们所在的散列table中的桶顺序排序的,因此它们按该顺序输出。这就是项目顺序发生变化的原因。

也就是说,您应该 依赖此行为,因为如果 bash 的作者决定更改散列函数或进行任何其他更改。

And how to bypass this behavior?

没有办法绕过这个。 Bash 数组使用散列 table 来存储散列。键的插入顺序不存储在任何地方。

当然,您可以通过修补 bash 来绕过此行为,以实现您请求的此类功能。

也就是说,我只使用两个数组:

keys=(d1 e2 m3 o4)
elements=(1w45 2dfg 3df 4df)
declare -A test2
for ((i=0;i<${#keys[@]};++i)); do
    test2[${keys[$i]}]="${elements[$i]}"
done
# or maybe something along:
declare -A test2=($(paste -zd <(printf "[%s]=[=13=]" "${keys[@]}") <(printf "%q [=13=]" "${elements[@]}"))

这样您就可以按照在单独的 keys 数组中插入键的顺序遍历键。

可以简单地解释为:

  • 整数索引数组具有隐式自动递增索引,并允许显式分配索引以创建稀疏数组。
    如果将具有显式索引的记录添加到索引数组;索引可能不再反映插入顺序

鉴于:

  • 关联数组只有一个明确的字符串索引。

总结一下:

一个隐式增量索引反映了插入顺序;而显式命令式索引则不会。