Hiding the implementation is a core principle of OOP and a good idea in all paradigms, but it especially important for iterators(or whatever they're called in that specific language) in languages that support lazy iteration.
The problem with exposing the concrete type of iterables - or even interfaces like
IList<T> - is not in the objects that expose them, but in the methods that use them. For example, let's say you have a function for printing a list of
void PrintFoos(IList<Foo> foos)
foreach (foo in foos)
You can only use that function to print lists of foo - but only if they implement
IList<Foo> foos = //.....
But what if you want to print every even-indexed item of the list? You'll need to create a new list:
IList<Foo> everySecondFoo = new List<T>();
bool isIndexEven = true;
foreach (foo; foos)
isIndexEven = !isIndexEven;
This is quite long, but with LINQ we can do it to a one-liner, which is actually more readable:
PrintFoos(foos.Where((foo, i) => i % 2 == 0).ToList());
Now, did you notice the
.ToList() in the end? This converts the lazy query into a list, so we can pass it to
PrintFoos. This requires an allocation of a second list, and two passes on the items(one on the first list to create the second list, and another on the second list to print it). Also, what if we have this:
void Print6Foos(IList<Foo> foos)
int counter = 0;
foreach (foo in foos)
if (6 < counter)
Print6Foos(foos.Where((foo, i) => i % 2 == 0).ToList());
foos has thousands of entries? We will have to go through all of them and to allocate a huge list just to print 6 of them!
Enter Enumerators - C#'s version of The Iterator Pattern. Instead of having our function accept a list, we make it accept
void Print6Foos(Enumerable<Foo> foos)
// everything else stays the same
Print6Foos(foos.Where((foo, i) => i % 2 == 0));
Print6Foos can lazily iterate over the first 6 items of the list, and we don't need to touch the rest of it.
Not exposing the internal representation is the key here. When
Print6Foos accepted a list, we had to give it a list - something that supports random access - and therefore we had to allocate a list, because the signature does not guarantee that it'll only iterate over it. By hiding the implementation, we can easily create a more efficient
Enumerable object that supports only what the function actually needs.