在遍历 awk 时删除数组元素:总是安全的吗?

Deleting array element in awk while looping through it: always safe?

这是一个 awk 问题:我想知道循环迭代器的确切语义是什么 for (k in array):我知道我们对扫描数组元素的顺序没有太多控制,但是我想知道删除此类循环主体中的数组元素是否总是安全的(即由某些 POSIX 规范保证)。我的意思是,是否保证循环中的后续迭代将正常运行,既不会跳过任何元素,也不会命中已删除的元素?

下面是一个最小的示例,我们在其中省略了输入中所有以大写 "A" 开头的名称。它似乎在我的 GNU Awk 4.2.1 上运行良好,但我不确定它在所有 awk 实现上是否完全可移植和安全。对此有什么想法吗?谢谢!

echo -e "Alberto\n Adam\n Payne\n Kristell\n John\n\
   Arjuna\n Albert\n Me\n You\n Toto\n Auntie\n Terribel" | 
awk '{ names[NR] =  } 
     END { for (k in names)
             if (substr(names[k], 1, 1) == "A") delete names[k];
           for (k in names) print names[k] }'

看起来应该是安全的:

https://www.gnu.org/software/gawk/manual/html_node/Delete.html

8.4 The delete Statement To remove an individual element of an array, use the delete statement:

delete array[index-expression] 

Once an array element has been deleted, any value the element once had is no longer available. It is as if the element had never been referred to or been given a value. The following is an example of deleting elements in an array:

for (i in frequencies)
    delete frequencies[i]

如果使用遍历数组所有元素的循环删除数组中的所有元素是安全的,那么您的代码也应该是安全的。


这是 for 循环的另一个资源:https://www.gnu.org/software/gawk/manual/html_node/Scanning-an-Array.html#Scanning-an-Array

The order in which elements of the array are accessed by this statement is determined by the internal arrangement of the array elements within awk and in standard awk cannot be controlled or changed. This can lead to problems if new elements are added to array by statements in the loop body; it is not predictable whether the for loop will reach them. Similarly, changing var inside the loop may produce strange results. It is best to avoid such things.

没有提到删除。

一般来说,在迭代时修改 array/container 是不安全的,被认为是不好的做法。 Java 语言为此提供了特殊的例外。

一种更安全的方法是遍历数组并创建一个包含要删除的索引的数组。

像这样:

 for (k in names) 
     if (substr(names[k], 1, 1) == "A") deletions[++i] = k;
 for (k in deletions)
     delete names[deletions[k]];
 for (k in names) print names[k] }'

是也不是。删除一个条目是 "safe",因为条目在删除后将不存在,但是假设在循环迭代时删除它后不会命中该索引是不安全的。

The POSIX spec 不能说:

the following code deletes an entire array:

for (index in array)
    delete array[index]

如果这样做可能会跳过一个索引,这:

for (index in arrayA) {
    if (index in arrayB) {
        print "Both:", index
        delete arrayA[index]
        delete arrayB[index]
    }
}

for (index in arrayA)
    print "A only:", index

for (index in arrayB)
    print "B only:", index

是一个 非常 常见的习惯用语,用于查找哪些集合值在其中,如果该方法不是 "safe" 在那种情况下,那将不起作用。

但是 这并不意味着您可以假设数组索引在循环 被删除后不会被命中 因为awk 是否计算出在进入循环之前或执行期间将要访问的所有数组索引取决于实现。例如,GNU awk 在进入循环 之前确定它将访问的所有索引 所以你会得到这种行为,其中数组在 delete a[3] 之后短了 1 个元素,但删除了索引 3 仍然在之前被删除的循环中被访问:

$ gawk 'BEGIN{split("a b c d e",a);
    for (i in a) {print length(a), i, a[i]; delete a[3]} }'
5 1 a
4 2 b
4 3
4 4 d
4 5 e

但并非所有 awk 都这样做,例如BWK awk/nawk 没有,MacOS/BSD 也没有 awk:

$ awk 'BEGIN{split("a b c d e",a);
    for (i in a) {print length(a), i, a[i]; delete a[3]} }'
5 2 b
4 4 d
4 5 e
4 1 a

gawk 行为等同于上述其他 awk 中的行为:

$ awk 'BEGIN{split("a b c d e",a); for (i in a) b[i];
    for (i in b) { print length(a), i, (i in a ? a[i] : x); delete a[3]} }'
5 2 b
4 3
4 4 d
4 5 e
4 1 a

我在上面使用未分配的变量 x 而不是 "" 来准确描述删除后 a[3] 的 zero-or-null 性质,但这实际上并不重要在这种情况下,因为我们无论如何都将其打印为“”。

所以无论你使用哪个awk,一旦退出上面的循环,a[3]就会消失,例如再次使用 GNU awk:

$ gawk 'BEGIN{split("a b c d e",a);
    for (i in a) {print length(a), i, a[i]; delete a[3]}
    print "---";
    for (i in a) {print i, a[i]} }'
5 1 a
4 2 b
4 3
4 4 d
4 5 e
---
1 a
2 b
4 d
5 e

请注意,在上面的脚本中,a[3] 实际上是在第一个循环中重新创建的,因为当 i3 时访问 a[i],然后是 delete a[3] 发生在每个索引上的是再次删除它的原因。如果我们仅在 i1 时执行删除,那么我们会看到 a[3] 存在但在循环后包含 zero-or-null:

$ gawk 'BEGIN{split("a b c d e",a);
        for (i in a) {print length(a), i, a[i]; if (i==1) delete a[3]}
        print "---";
        for (i in a) {print i, a[i]} }'
5 1 a
4 2 b
4 3
5 4 d
5 5 e
---
1 a
2 b
3
4 d
5 e

要了解为什么 pre-determining 在开始循环之前将访问的索引的 gawk 方法比在循环时尝试动态确定它们更好,请考虑这段代码,它试图在循环内向数组添加 3 个新元素:

$ cat tst.awk
BEGIN {
    split("a b c",a)
    for (i in a) {
        j=i+100
        a[j] = "foo" j
        print length(a), i, a[i]
    }
    print "---"
    for (i in a) {
        print i, a[i]
    }
}

gawk 的输出和最终结果都是 predictable 和期望的:

$ gawk -f tst.awk
4 1 a
5 2 b
6 3 c
---
6 1 a
6 2 b
6 3 c
6 101 foo101
6 102 foo102
6 103 foo103

while with MacOS/BSD awk(忽略顺序,只看数组的长度和索引的值):

$ awk -f tst.awk
4 2 b
5 3 c
6 102 foo102
7 103 foo103
8 202 foo202
9 203 foo203
10 302 foo302
11 1 a
---
11 303 foo303
11 2 b
11 3 c
11 402 foo402
11 101 foo101
11 102 foo102
11 103 foo103
11 202 foo202
11 203 foo203
11 302 foo302
11 1 a

它显然是混乱的,因为它在循环时试图访问循环中添加的索引,但收效有限(可能是由于散列 table 中新索引的顺序与之前访问的散列中的顺序有关table 个条目)这是幸运的,否则我们将陷入无限循环。

要从 MacOS/BSD awk 等中获得有用的结果,您再次需要在循环之前将预定索引保存在新数组中,如上所示:

$ cat tst.awk
BEGIN {
    split("a b c",a)
    for (i in a) {
        b[i]
    }
    for (i in b) {
        j=i+100
        a[j] = "foo" j
        print length(a), i, a[i]
    }
    print "---"
    for (i in a) {
        print length(a), i, a[i]
    }
}

$ awk -f tst.awk
4 2 b
5 3 c
6 1 a
---
6 2 b
6 3 c
6 101 foo101
6 102 foo102
6 103 foo103
6 1 a

Oh and wrt I know we don't have much control on the order in which the array elements are scanned - 使用 GNU awk,您可以通过设置 PROCINFO["sorted_in"] 来精确控制它,请参阅 https://www.gnu.org/software/gawk/manual/gawk.html#Controlling-Scanning。例如:

$ gawk 'BEGIN{split("10 2 bob alf",a);
    PROCINFO["sorted_in"]="@ind_str_asc"; for (i in a) print i, a[i]}'
1 10
2 2
3 bob
4 alf

$ gawk 'BEGIN{split("10 2 bob alf",a);
    PROCINFO["sorted_in"]="@val_str_asc"; for (i in a) print i, a[i]}'
1 10
2 2
4 alf
3 bob

$ gawk 'BEGIN{split("10 2 bob alf",a);
    PROCINFO["sorted_in"]="@val_num_asc"; for (i in a) print i, a[i]}'
4 alf
3 bob
2 2
1 10