Der Iterator ist ein Entwurfsmuster aus dem Bereich der Softwareentwicklung, das zur Kategorie der Verhaltensmuster (englisch behavioral design patterns) gehört. Das Muster ist eines der sogenannten GoF-Muster (siehe Viererbande). Es stellt Möglichkeiten zur Verfügung, auf Elemente einer aggregierten Struktur sequenziell zuzugreifen, ohne die Struktur zu enthüllen.[1]
Das Muster ist auch als Cursor bekannt. Mitunter wird durch Wahl des einen oder anderen Begriffes ein Unterschied im Verhalten ausgedrückt.
In der Praxis werden häufig Objekte zu einer Sammlung zusammengefasst. Auf die Elemente solch einer Sammlung soll möglichst generisch und ohne Rücksicht auf die Implementierungsdetails zugegriffen werden können.
Das Diagramm ist nur als grobes Beispiel zu sehen. Die konkrete Realisierung kann stark abweichen:
Zurueck()
oder IstFertig()
sind nicht immer realisierbar.Die Implementierung der zugrunde liegenden Datenstruktur bleibt verborgen.
Je nach Variante der Implementierung können sich Nachteile durch erhöhte Laufzeit- und Speicherkosten ergeben.
Aufgrund einiger Designentscheidungen ergeben sich Iterator-Varianten mit verschiedenen Eigenschaften:
Bei einem externen Iterator steuert der Klient die Iteration. Der Klient muss dafür sorgen, dass der Iterator weiterrückt.
Ein interner Iterator tut dies selbst. Dazu muss ihm die Operation übergeben werden, die er auf das aktuelle Element anwenden soll. Interne Iteratoren werden oft gar nicht als solche erkannt oder bezeichnet, da die Iteration nicht sichtbar ist oder aber über externe Iteratoren realisiert ist.
Die Vorteile des externen Iterationsmodells liegen darin, dass der Aufrufer mehr Kontrolle über die Iteration hat. Der Iterator kann nur teilweise oder zu verschiedenen Zeiten im Programmcode, etwa bei einer Nutzerinteraktion, fortgeschritten werden. Dies ist beim internen Iterator nicht der Fall. Dafür kann dieser einfacher zu implementieren sein, unter anderem, weil eine rekursive Implementation möglich ist.[2]
Grady Booch nennt den externen Iterator aktiv und den internen passiv.
Der Traversionsalgorithmus kann durch den Iterator oder das Aggregat vorgegeben werden. Im letzteren Fall wird oft von einem Cursor gesprochen.
Wenn das Aggregat während der Traversion verändert wird, kann das zu falschen Ergebnissen oder gar zum Programmabsturz führen. Ein robuster Iterator ist gegen Modifikationen des Aggregats gesichert. Typischerweise werden dazu die Iteratoren beim Aggregat registriert. Das führt zu höheren Kosten beim Erzeugen der Iteratoren, aber auch beim Ändern des Aggregates.
Polymorphe Iteratoren bieten eine hohe Flexibilität. Da Iteratoren meist in Schleifen verwendet werden, sind die Kosten dafür allerdings sehr hoch.
Je nach Implementation kann es, in Analogie zum Null-Zeiger, einen Nulliterator geben. Dieser signalisiert das Ende einer Iteration oder einen ungültigen Iterator. Das ist recht bequem in der Benutzung, da man einem Iterator seine Gültigkeit „ansieht“, aber die Implementation wird dadurch u. U. komplizierter.
Daher wurde z. B. in der C++-Standardbibliothek diese Alternative gewählt. Dabei besitzt jedes Aggregat seinen eigenen Nulliterator, mit dem man dann den jeweiligen Iterator vergleichen muss.
Ein Iterator kann privilegierten Zugriff auf die Interna des Aggregates besitzen. Das ist bei komplexen Datenstrukturen teilweise nicht zu vermeiden. Allerdings wird dadurch die Implementation neuer Traversionsalgorithmen erschwert oder gar verhindert.
Diese C++11 Implementierung basiert auf dem Kapitel „Generalizing vector yet again“.[3]
#include <iostream>
#include <stdexcept>
#include <initializer_list>
class Vector {
public:
using iterator = double*;
iterator begin() { return elem; }
iterator end() { return elem + sz; }
Vector(std::initializer_list<double> lst) :elem(nullptr), sz(0) {
sz = lst.size();
elem = new double[sz];
double* p = elem;
for (auto i = lst.begin(); i != lst.end(); ++i, ++p) {
*p = *i;
}
}
~Vector() { delete[] elem; }
int size() const { return sz; }
double& operator[](int n) {
if (n < 0 || n >= sz) throw std::out_of_range("Vector::operator[]");
return elem[n];
}
Vector(const Vector&) = delete; // Dreierregel
Vector& operator=(const Vector&) = delete;
private:
double* elem;
int sz;
};
int main() {
Vector v = {1.1*1.1, 2.2*2.2};
for (const auto& x : v) {
std::cout << x << '\n';
}
for (auto i = v.begin(); i != v.end(); ++i) {
std::cout << *i << '\n';
}
for (auto i = 0; i <= v.size(); ++i) {
std::cout << v[i] << '\n';
}
}
Die Programmausgabe ist:
1.21
4.84
1.21
4.84
1.21
4.84
terminate called after throwing an instance of 'std::out_of_range'
what(): Vector::operator[]
Ein einfacher Fall eines externen Iterators wäre etwa (in Java):
class ObjectIterator {
private Object[] m_source;
private int m_current;
public ObjectIterator(Object[] source) {
m_source = source;
m_current = 0;
}
public boolean hasNext() {
return m_current < m_source.length;
}
public Object next() {
return m_source[m_current++];
}
}
Anwendung:
Object[] myList = new Object[] {new Integer(1), new Integer(2), new Integer(3)};
ObjectIterator iterator = new ObjectIterator(myList);
while(iterator.hasNext()) {
System.out.println(iterator.next());
}
Eine andere Variation dieses Entwurfsmusters wäre ein interner Iterator:
class MyObjectIterator extends ObjectIterator {
public MyObjectIterator(Object[] source) {
super(source);
}
public void print() {
while(hasNext()) {
System.out.println(next());
}
}
public void apply(ObjectHandler handler) {
while(hasNext()) {
handler.handle(next());
}
}
}
interface ObjectHandler {
void handle(Object o);
}
Anwendung:
class MyObjectHandler implements ObjectHandler {
public void handle(Object o) {
System.out.println(o);
}
}
Object[] myList = new Object[] {new Integer(1), new Integer(2), new Integer(3)};
MyObjectIterator iterator = new MyObjectIterator(myList);
iterator.print();
iterator.apply(new MyObjectHandler());
Ein interner Iterator kapselt die Iteration selber und macht sie so im gesamten Programm einfach und konsistent wiederverwendbar, ohne sie wie im Fall eines externen Iterators immer wieder neu programmieren zu müssen. Durch Anwendung des Strategie Entwurfsmusters lässt sich der Algorithmus auch einfach austauschen, wie in der letzten Beispielzeile gezeigt.
Obwohl sich diese Beispiele mit sequenziellem Zugriff begnügen, sind natürlich auch Iteratoren mit wahlfreiem Zugriff möglich. Viele Implementierungen bieten zusätzlich die Möglichkeit die iterierte Sammlung direkt zu verändern, etwa durch Entfernen des aktuellen Elementes.
Die STL enthält Iteratoren für ihre Container.