简体   繁体   中英

Why does ruby array.delete_at inside an array.each fail?

I'm new to ruby, and clearly see and find online that the following fails:

arr = [10, 20, 30, 40]     
arr.each.with_index do |elmt, i|
  print "#{elmt}, #{i}, "
  arr.delete_at(i) if elmt == 20
  puts arr.length
end

Clearly the delete_at is interacting with the iterator, but I cannot find a clear description of how the iterator and delete_at work such that this is so. (BTW, I understand solutions that work - I'm not looking for a correct way to do this, I'm trying to understand the semantics such that I know why this is not doing what's expected.) For completeness, here's the output

10, 0, 4
20, 1, 3
40, 2, 3
=> [10, 30, 40]

lightbulb!!

It seems clear the delete_at immediately adjusts the underlying data structure, left shifting all elements to the right of the deleted item. The array seems more like a linked list than an array. So the "next" item (here "30") is now arr[1] (which the iterator has already processed), and when the iterator increments to arr[2], it sees "40". arr[3] returns nil and puts seems to do nothing with nil.

look at: https://github.com/ruby/ruby/blob/ca6b174078fa15f33655be704d9409fdbc4f9929/include/ruby/intern.h

https://github.com/ruby/ruby/blob/ca6b174078fa15f33655be704d9409fdbc4f9929/enumerator.c

https://github.com/ruby/ruby/blob/ca6b174078fa15f33655be704d9409fdbc4f9929/array.c

each gives you an enumerator, and the with_index works with the enumerator.

when you reach element 20 you print it out, and after that you erase it, and at this point Ruby effectively shifts down all elements in the array. Now the enumerator which is backed by the array picks up the next element which is 40 because everything got shifted down (30 was copied over 20, 40 was copied over 30 and array was resized)

take a look at: https://github.com/ruby/ruby/blob/ca6b174078fa15f33655be704d9409fdbc4f9929/array.c#L3023 It's where the magic of moving the elements via a memmove happens.

Let's step through it:

arr = [10, 20, 30, 40]  
enum0 = arr.each
  #=> #<Enumerator: [10, 20, 30, 40]:each> 
enum1 = enum0.with_index
  #=> #<Enumerator: #<Enumerator: [10, 20, 30, 40]:each>:with_index> 

We can see the contents of enum1 by converting it to an array:

enum1.to_a
  #=> [[10, 0], [20, 1], [30, 2], [40, 3]] 

This tells us that Enumerator#each (which will invoke Array#each ) will pass the four elements of the enumerator enum1 into the block, assigning them in turn to the block variables. The first is:

elmt, i = enum1.next
  #=> [10, 0] 
puts elmt, i
  # 10
  #  0
elmt == 20
  #=> false 

so arr.delete_at(i) is not executed.

Neither arr nor enum1 have been altered:

arr
  #=> [10, 20, 30, 40]
enum1.to_a
  #=> [[10, 0], [20, 1], [30, 2], [40, 3]] 

each now passes the next element of enum1 into the block:

elmt, i = enum1.next
  #=> [20, 1] 
elmt == 20
  #=> true 

so we execute:

arr.delete_at(i)
  #=> [10, 20, 30, 40].delete_at(1)
  #=> 20 
arr
  #=> [10, 30, 40] 
enum1.to_a
  #=> [[10, 0], [30, 1], [40, 2]]

Ah! So the enumerator has been changed as well as arr . That makes perfect sense, because when the enumerator is created a reference to the original receiver is established, together with rules for what is to be be done with it. Changes to the receiver will therefore affect the enumerator.

We can use Enumerator#peek to see what will be the next element of enum1 that each will pass into the block:

enum1.peek
  #=> [40, 2]

So you see that each moves on to the next indexed position, oblivious to the fact that an earlier element has been removed, causing the later elements to each shift down by one position, causing each to skip [30,1] .

elmt, i = enum1.next
  #=> [40, 2] 
elmt == 20
  #=> false 
arr
  #=> [10, 30, 40] 
enum1.to_a
  #=> [[10, 0], [30, 1], [40, 2]] 

At this point each reaches the end of the enumerator, so it's job is finished. It therefore returns the original receiver, arr , but that has been modified, so we get:

[10, 30, 40] 

A better example might be:

arr = [10, 20, 20, 40]  

where:

[10, 20, 40] 

would be returned.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM