0

Безопасно ли удалять элементы из Set во время итерации с помощью for..of?

16

Указано ли, что вы можете удалять любые элементы из экземпляра Set во время итерации с помощью for..of, и что

  • вы не будете итерироваться более одного раза по одному и тому же элементу
  • вы не пропустите другие элементы, которые были в множестве в начале итерации, кроме тех, которые вы удалите

?

2 ответ(ов)

0

Да, совершенно нормально добавлять и удалять элементы из множества во время его итерации. Этот сценарий был учтен и поддерживается в JavaScript начиная с 2015 года (ES6). Итерация останется в согласованном состоянии. Обратите внимание, что это также относится к итерации с использованием forEach.

Интуитивно:

Алгоритм итерации по множеству выглядит примерно так:

Установить позицию на 0
Пока позиция < calculateLength() // обратите внимание, она рассчитывается при каждой итерации
    вернуть элемент из set.entryList[position]

Добавление элемента происходит следующим образом:

Если элемент не в множестве
   Добавить элемент в _конец_ множества

Таким образом, это не мешает существующим итерациям - они продолжат свою работу.

Удаление выглядит так:

Заменить все элементы, равные `element`, на специальное пустое значение

Замена на пустое значение вместо удаления гарантирует, что позиции итераторов не будут нарушены.


Формально

Добавление

Вот соответствующая часть спецификации из %SetIteratorPrototype%.next:

Повторять, пока индекс меньше общего числа элементов в entries. Количество элементов должно пересчитываться каждый раз, когда этот метод вызывается.

Итератор множества перебирает элементы один за другим.

Из Set.prototype.add:

Добавить значение как последний элемент entries.

Это гарантирует, что при добавлении элементов в список они будут перебраны до завершения итерации, так как для них всегда выделяется новое место в списке entries. Таким образом, все работает в соответствии со спецификацией.

Что касается удаления:

Заменить элемент в entries с значением e на элемент с пустым значением.

Замена элемента на пустой вместо его удаления гарантирует, что порядок итерации существующих итераторов не нарушится, и они продолжат корректно перебирать множество.

Пример кода

Вот короткий фрагмент кода, демонстрирующий эту возможность:

var set = new Set([1]);
for(let item of set){
   if(item < 10) set.add(item + 1);
   console.log(item);
}

Этот код выводит числа от 1 до 10. Вот версия без использования for...of, которую вы можете запустить в вашем браузере:

var set = new Set([1]);
for (var _i = set[Symbol.iterator](), next; !(next = _i.next()).done;) {
   var item = next.value;
   if (item < 10) set.add(item + 1);
   document.body.innerHTML += " " + item;
}
0

Да, мой ответ — да, если вас устраивает, что после удаления элемент в наборе продолжает выводиться в следующей итерации. Похоже, не имеет значения, на каком элементе множества вы находитесь в процессе итерации. Это довольно удобно!

Вот мой тестовый код:

s = new Set([{ a: 0 }, { a: 1 }, { a: 2 }, { a: 3 }]);
do {
  for (let x of s) {
    console.log(x.a);
    if (Math.random() < 0.2) {
      console.log('удалено ' + x.a);
      s.delete(x);
    }
  }
} while (s.size > 0);

В Firefox 75.0 это работает без проблем. Множества, как предполагается, сохраняют порядок вставки, и оно действительно выводит элементы в этом порядке в процессе итерации. Независимо от того, что удаляется, итерация продолжается по порядку вставки:

0
1
2
3
0
1
удалено 1
2
3
0
2
удалено 2
3
0
3
0
удалено 0
3
3
...
3
3
удалено 3

Я также протестировал аналогичный код, который не использует текущий экземпляр в процессе итерации:

sCopy = [{ a: 0 }, { a: 1 }, { a: 2 }, { a: 3 }];
s = new Set(sCopy);
do {
  for (let x of s) {
    console.log(x.a);
    if (Math.random() < 0.2) {
      let deleteMe = Math.floor(Math.random() * s.size);
      console.log('удалено ' + sCopy[deleteMe].a);
      s.delete(sCopy[deleteMe]);
      sCopy.splice(deleteMe, 1);
    }
  }
} while (s.size > 0);

Мне пришлось использовать соседний массив, потому что в Set нет способа получить случайный индекс для удаления случайного экземпляра. Так что я просто создал Set из массива, чтобы он использовал те же объекты.

Это также отлично работает, как вы можете видеть:

0
удалено 1
2
удалено 2
3
0
3
0
удалено 0
3
3
3
3
удалено 3

И да... Я даже протестировал вставку объектов случайным образом... То же самое, но на этот раз не буду публиковать вывод:

sCopy = [{ a: 0 }, { a: 1 }, { a: 2 }];
s = new Set(sCopy);
do {
  for (let x of s) {
    console.log(x.a);
    if (Math.random() < 0.1) {
      let newInstance = { a: Math.random() * 100 + 100 };
      console.log('добавлено ' + newInstance.a);
      s.add(newInstance);
      sCopy.push(newInstance);
    }
    if (Math.random() < 0.2) {
      let deleteMe = Math.floor(Math.random() * s.size);
      console.log('удалено ' + sCopy[deleteMe].a);
      s.delete(sCopy[deleteMe]);
      sCopy.splice(deleteMe, 1);
    }
  }
} while (s.size > 0);
Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь