【问题标题】:Why don't Bash associative arrays maintain index order?为什么 Bash 关联数组不维护索引顺序?
【发布时间】:2020-06-09 07:09:38
【问题描述】:

我正在创建关联数组以在 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

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

【问题讨论】:

  • 这就是关联数组的工作方式。如果您需要有序键,请将有序键存储在数组中。
  • 顺序由键的哈希值决定;如果你不知道散列函数(你也没有必要知道),你就无法真正预测这个顺序是什么。
  • 您使用的是哪个 bash 版本?我无法复制3m 4o 1d 2e,我总是得到1d 3m 2e 4o
  • 这根本不是 bash 特定的。大多数散列或映射类型都以这种方式运行;例如,最近 Python 将其 dict 类型更改为具有明确的顺序,而 Go 故意将其随机化以避免利用故意低效的分桶进行拒绝服务攻击。

标签: arrays bash associative-array


【解决方案1】:

可以简单解释为:

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

鉴于:

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

总结一下:

隐式增量索引反映插入顺序;而显式的命令式索引则没有。

【讨论】:

    【解决方案2】:

    为什么 bash 关联数组不维护索引顺序?

    因为它们被设计为不这样做。

    为什么项目的顺序会发生变化?

    Bash associative array implementation 使用 hash library 并存储索引的哈希值。这些哈希存储在buckets 和128 default number of buckets。哈希是使用函数hash_string() 使用简单的乘法和按位异或来计算的。关联数组的键在in the order buckets appear 中列出。 Bucket number is calculated通过key的hash值和bucket的个数进行按位与运算减少1。

    我编译了 bash commit 6c6454cb18d7cd30b3b26d5ba6479431e599f3ed 并为我编译了你的脚本输出:

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

    于是我复制了hash_string()函数并编写了一个小C程序,该程序将输出键的桶数并编译并执行:

    #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));
        }
    }
    

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

      o 112
      m 114
      e 122
      d 123
    
     d1  16
     e2  60
     m3  69
     o4 100
    
     1d  14
     3m  41
     2e  50
     4o  94
    

    输出的keys的顺序是按照它们在hash表中bucket的顺序来排序的,所以就是按照这个顺序输出的。这就是项目顺序发生变化的原因。

    也就是说,您应该依赖这种行为,因为如果 bash 的作者决定更改散列函数或进行任何其他更改,键的输出顺序可能会发生变化。

    以及如何绕过这种行为?

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

    当然,您可以通过修补 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]=\0" "${keys[@]}") <(printf "%q \0" "${elements[@]}"))
    

    这样,您可以按照将键插入单独的keys 数组的顺序对键进行迭代。

    【讨论】:

      【解决方案3】:

      根据 cmets 这可以绕过此行为。

      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}
      }
      

      【讨论】:

      • 没有更好的方法。其他语言具有保持插入顺序的哈希类型数据结构,但 bash 没有。
      【解决方案4】:

      为什么项目的顺序会发生变化?

      因为通常关联数组不会自然地维护插入顺序:基于树的数组使用自然(排序)排序,而哈希图使用其哈希函数到达键的任何位置(可以按进程随机分配)出于安全原因,甚至是每个地图)。

      后者还解释了为什么在添加新项目时项目的顺序甚至会发生变化:不仅可以在现有项目之间插入新项目,当哈希图必须调整大小时,整个序列将作为条目“重新洗牌”被重新散列并移动到新位置。

      有些语言要么明确添加排序作为一项功能(通常使用双向链表),要么使用自然排序 hashmap,在这种情况下插入顺序得到维护,但你不能假设这个属性成立,除非语言保证它。哪个 bash 没有。

      【讨论】:

        猜你喜欢
        • 2011-04-18
        • 1970-01-01
        • 2019-08-17
        • 2015-04-28
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多