Using and Abusing the Iterator Pattern
While Iterator is one of the most commonly used and useful patterns, it does have some pointy bits sticking out waiting to snag the unwary. The main danger is this: What happens if the aggregate object changes while you are iterating through it? Suppose you are sequencing through a list and just before you get to the third element, someone deletes that element from the list. What happens? Does the iterator show you the now-defunct third element anyway? Does the iterator quietly go on to the fourth element as though nothing has happened? Does it throw an exception?
Unfortunately, none of the iterators that we have built in this chapter so far react particularly well to change. Recall that our external ArrayIterator worked by holding on to the index of the current item. Deleting elements from the array that we have not seen yet is not a problem, but making modifications to the beginning of the array will wreak havoc with the indexing.
We can make our ArrayIterator resistant to changes to the underlying array by simply making a copy of the array in the iterator constructor:
class ChangeResistantArrayIterator def initialize(array) @array = Array.new(array) @index = 0 end ...
This new iterator makes a shallow copy of the array—the copy points to the original contents, which are not themselves copied—and sequences through the new array. Thanks to this new iterator, we have a change-resistant snapshot of the array and can iterate through that.
Internal iterators have exactly the same concurrent modification problems as external iterators. For example, it is probably a very bad idea to do the following:
array=['red', 'green', 'blue', 'purple'] array.each do | color | puts(color) if color == 'green' array.delete(color) end end
This code will print
red green purple
By deleting the 'green' entry, we managed to mess up the indexing of the iterator just enough to cause it to miss 'blue'.
Internal iterators can also defend against the crime of modifying while iterating by working on a separate copy of the aggregate, just as we did in our ChangeResistantArrayIterator class. This might look something like the following code:
def change_resistant_for_each_element(array) copy = Array.new(array) i = 0 while i < copy.length yield(copy[i]) i += 1 end end
Finally, a multithreaded program is a particularly dangerous home for iterators. You need to take all of the usual care to ensure that one thread does not rip the aggregate rug out from under your iterator.