Is it safe to delete from an array inside each?

Is it safe to remove elements from an Array , iterating over it through each ? The first test looks promising:

 a = (1..4).to_a a.each { |i| a.delete(i) if i == 2 } # => [1, 3, 4] 

However, I could not find serious facts:

  • Is it safe (by design)
  • Which version of Ruby is safe with

In some cases in the past, it seems that it was not possible to do :

It does not work because Ruby terminates the .each when trying to delete something.

the documentation does not indicate anything about the possibility of deletion during iteration.

I am not looking for reject or delete_if . I want to do things with array elements, and sometimes also delete an element from the array (after I did other things with the specified element).

Update 1: I did not very clearly understand my definition of "safe", which I had in mind:

  • do not throw any exceptions
  • don't skip a single item in Array
+7
arrays ruby
source share
5 answers

You should not rely too much on unauthorized answers. The answer you quoted is incorrect, as stated in Kevin's commentary on him.

It is safe (from the very beginning of Ruby) to remove elements from the array, and each in the sense that Ruby will not cause an error for this and will give a decisive (i.e. not random) result.

However, you need to be careful, because when you delete an item, the items that follow it will be shifted, so the next item that was supposed to be repeated will be moved to the position of the deleted item that was repeated over already, and will be skipped.

+7
source share

To answer your question whether it is safe to do this, you first need to determine what you mean by "safe." You mean

  • Is this a runtime failure?
  • it does not raise a Exception ?
  • does he raise a Exception ?
  • behaves deterministically?
  • Does he do what you expect from this? (What do you expect from this?)

Unfortunately, the Ruby language specification is not entirely useful:

15.2.12.5.10 Array # each


each (& block)


Visibility : public

Behavior

  • If a block is specified:
    • For each recipient element in index order, a call block with the element as its only argument.
    • Return the receiver.

This, apparently, means that it is really completely safe in the sense of above 1., 2., 4. and 5.

The documentation says:

each { |item| block } each { |item| block } β†’ ary

Calls this block once for each element in self , passing this element as a parameter.

Again, this seems to imply the same thing as the spec.

Unfortunately, none of the existing Ruby implementations interpret the specification in this way.

What actually happens in MRI and YARV is the following: an array mutation, including any offset of elements and / or indices, becomes immediately visible, including in the internal implementation of iterator code, which is based on array indices. Thus, if you delete an element at or before the position in which you are currently iterating, you will skip the next element, whereas if you delete an element after the position at which you are currently iterating, you will skip that element. For each_with_index you will also notice that all elements after the deleted element have their indices shifted (or, conversely, vice versa: the indices remain, but the elements are shifted).

Thus, this behavior is β€œsafe” in the sense of 1., 2., and 4.

Other Ruby implementations basically copy this (undocumented) behavior, but being undocumented you cannot rely on it, and in fact I believe that at least one of them experimented briefly with raising a kind of ConcurrentModificationException .

+3
source share

I would say that it is safe based on the following:

 2.2.2 :035 > a = (1..4).to_a => [1, 2, 3, 4] 2.2.2 :036 > a.each { |i| a.delete(i+1) if i > 1 ; puts i } 1 2 4 => [1, 2, 4] 

I would deduce from this test that Ruby correctly recognizes iterations through the content that the β€œ3” element was deleted while processing the β€œ2” element, otherwise the β€œ4” element would also be deleted.

but

 2.2.2 :040 > a.each { |i| puts i; a.delete(i) if i > 1 ; puts i } 1 1 2 2 4 4 

This means that after deleting "2" the next processed element is what is now the third in the array, therefore the element that used to be in third place is not processed at all. Everyone seems to be reviewing the array to find the next item to process at each iteration.

I think that with this in mind, you should duplicate the array in your circumstances before processing.

+1
source share

It depends.

All .each does returns a counter that contains the collection and a pointer to its place. Example:

 a = [1,2,3] b = a.each # => #<Enumerator: [1, 2, 3]:each> b.next # => 1 a.delete(2) b.next # => 3 a.clear b.next # => StopIteration: iteration reached an end 

Each with block calls next until the iteration reaches the end. Therefore, as long as you do not modify any β€œfuture” massive entries, this should be safe.

However, there are so many useful methods in ruby Enumerable and Array that you really won't need to do this.

0
source share

You are right, in the past it was recommended not to delete items from the collection, iterating over it. In my tests and, at least, with version 1.9.3 in practice in an array, this does not cause problems even when deleting the previous or next elements.

I believe that you cannot do this yet. A clearer and safer approach is to reject elements and assign a new array.

 b = a.reject{ |i| i == 2 } #[1, 3, 4] 

If you want to reuse your array, which is also possible

 a = a.reject{ |i| i == 2 } #[1, 3, 4] 

which actually matches

 a.reject!{ |i| i == 2 } #[1, 3, 4] 

You say you do not want to use rejection because you want to delete other things with elements before deleting, but this is also possible.

 a.reject!{ |i| puts i if i == 2;i == 2 } # 2 #[1, 3, 4] 
0
source share

All Articles