Удаление элемента массива в awk во время его цикла: всегда безопасно? - PullRequest
2 голосов
/ 24 мая 2019

Это вопрос awk: мне интересно, какова точная семантика итератора цикла for (k in array): я знаю, что у нас нет большого контроля над порядком сканирования элементов массива, и все же я хочу знать, если всегда безопасно (то есть гарантировано некоторой спецификацией POSIX) удалить элемент массива в теле такого цикла. Я имею в виду, гарантировано ли , что последующие итерации в цикле будут работать правильно, не пропуская ни одного элемента и не ударяя по удаленному элементу?

Минимальным примером является приведенный ниже, где мы опускаем все имена из ввода, которые начинаются с заглавной буквы "A". Кажется, он хорошо работает на моем GNU Awk 4.2.1, но я не уверен, является ли он полностью переносимым и безопасным во всех реализациях awk. Есть мысли по этому поводу? Thx!

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

Ответы [ 3 ]

1 голос
/ 24 мая 2019

Да и нет.Удаление записи безопасно, поскольку запись не будет существовать после ее удаления, но небезопасно предполагать, что вы не попадете в этот индекс после удаления, пока цикл повторяется.

Спецификация POSIX не может сказать:

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 определяет все индексы, которые он посетит до ввода цикла , поэтому вы получаете такое поведение, когда массив становится на 1 элемент короче после delete a[3], но удаленный индекс 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] фактически воссоздается во время первого цикла из-за доступа к a[i], когда i равен 3, нотогда случается delete a[3]g для каждого индекса - это то, что удаляет его снова.Если бы мы выполняли удаление только тогда, когда i равно 1, мы бы увидели, что 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

Oh и 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
1 голос
/ 24 мая 2019

Похоже, это должно быть безопасно:

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 внутри цикла может привести к странным результатам. Лучше всего избегать таких вещей.

Ничего об удалении не упоминается.

0 голосов
/ 24 мая 2019

Как правило, изменение массива / контейнера во время его итерации небезопасно и считается плохой практикой. Язык 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] }'
...