-
-
Notifications
You must be signed in to change notification settings - Fork 942
Description
Given:
class Stuff
include Enumerable
def initialize(stuff)
@stuff = stuff
end
def each(&block)
@stuff.each(&block)
end
end
original_stuff = [Object.new, Object.new, Object.new]
enumerable_stuff = Stuff.new(original_stuff)Expected Behavior
original_stuff[1].equal?(enumerable_stuff.to_a[1]) # => true
original_stuff[1].equal?(enumerable_stuff.drop(1).first) # => true
enumerable_stuff.to_a[1].equal?(enumerable_stuff.drop(1).first) # => trueAfter dropping an element from an Enumerable I expect that the remaining elements are the same as in the original Enumerable. Calling #drop should not modify the elements.
Actual Behavior
original_stuff[1].equal?(enumerable_stuff.to_a[1]) # => true
original_stuff[1].equal?(enumerable_stuff.drop(1).first) # => false
enumerable_stuff.to_a[1].equal?(enumerable_stuff.drop(1).first) # => falseCalling #drop on an Enumerable calls #dup on every non-dropped element. Besides the side-effect illustrated above it also expensive to do all that copying, and it doesn't work with objects whose #dup doesn't behave well, or objects that can't be #dup'ed.
I discovered this while writing a native extension with a class that was not allocatable. It took me a while to understand why my code failed with errors saying that my class wasn't allocatable – I wasn't trying to create instances.
It looks like this behaviour was introduced in 06f0441, but it seems like the fix for the problem described in the commit message was applied in the wrong place (it ended up in Enumerable instead of Enumerator).