【问题标题】:Deleting array element in awk while looping through it: always safe?在 awk 中循环删除数组元素:总是安全的?
【发布时间】:2019-10-10 19:05:04
【问题描述】:

这是一个 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] = $1 } 
     END { for (k in names)
             if (substr(names[k], 1, 1) == "A") delete names[k];
           for (k in names) print names[k] }'

【问题讨论】:

    标签: arrays loops awk semantics


    【解决方案1】:

    是的,不是的。删除一个条目是“安全的”,因为该条目在删除后将不存在,但假设在循环迭代时删除它后不会点击该索引是不安全的。

    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
    

    是一个非常常见的习语,用于查找哪些集合值在其中,如果该方法在该上下文中不“安全”,则该方法将不起作用。

    但是这并不意味着您可以假设数组索引在被删除后不会被命中在循环期间,因为 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] 的零或空性质,但在这种情况下实际上并不重要,因为我们正在打印无论如何它都是“”。

    因此,无论您使用哪个 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] 存在但在循环后包含零或空值:

    $ 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
    

    要了解为什么在开始循环之前预先确定将要访问的索引的 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,输出和最终结果都是可预测的,并且符合预期:

    $ 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
    

    使用 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
    

    这显然是混乱的,因为它试图访问在循环中添加的索引,但成功有限(可能是由于哈希表中新索引的顺序与先前访问的哈希表条目的顺序),这是幸运的否则我们会陷入无限循环。

    要从 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
    

    哦,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
    

    【讨论】:

    • 感谢@Ed Morton 非常有见地和详尽的回答!已验证! :)
    【解决方案2】:

    看起来应该是安全的:

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

    8.4 删除语句 要删除数组的单个元素,请使用 delete 语句:

    delete array[index-expression] 
    

    一旦数组元素被删除, 该元素曾经拥有的任何值都不再可用。就好像 元素从未被引用或被赋予值。这 以下是删除数组中元素的示例:

    for (i in frequencies)
        delete frequencies[i]
    

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


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

    此语句访问数组元素的顺序由 awk 中数组元素的内部排列决定,在标准 awk 中无法控制或更改。如果通过循环体中的语句将新元素添加到数组中,这可能会导致问题;无法预测 for 循环是否会到达它们。同样,在循环内更改 var 可能会产生奇怪的结果。最好避免这样的事情。

    没有提到删除。

    【讨论】:

    • 是的 Corentin,我已经阅读了该手册,所以该元素肯定不再在数组中,但是删除对我们当前循环使用的迭代器有何影响? IE。当前迭代是否有可能因为删除而跳过数组中的一个元素?
    • 不过,你会得到 +1,因为在 Gawk 手册中仅存在该示例并删除了频率数组就意味着在手册中编写该示例的人清楚地认为这是无害的代码... :) 不过,当我看到一个明确的、权威的东西在 awk 中解释迭代器的语义时,我会更放心...谢谢!
    • @jaybee 我编辑并添加了 1 个其他链接。虽然仍然不是 100% 满意
    【解决方案3】:

    一般来说,在迭代数组/容器时修改数组/容器是不安全的并且被认为是不好的做法。 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] }'
    

    【讨论】:

    • 当然,Dudi Boy,一旦我们完成循环,为延迟删除指定数组元素总是安全的,谢谢你的提示,但这并不能回答我的问题。
    • 我们不知道 awk 对键的迭代器是否可以安全删除。这是特定于实现的,并且可能因编译的优化而异。在单线程实现中是安全的。
    猜你喜欢
    • 2022-11-28
    • 2012-02-15
    • 2013-05-04
    • 2010-12-29
    • 2013-05-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多