Training - Beratung - Projektarbeiten

www.David-Tielke.de

Wie funktioniert LINQ? - Teil 1: IEnumerable<T>, IEnumerator<T> und yield

Wenn man die Arbeitsweise von LINQ verstehen möchte, kommt man nicht darum herum, sich mit den Interfaces IEnumerable<T> und IEnumerator<T> zu beschäftigen. Beide gibt es bereits seit .NET 2.0 und werden seit jeh her genutzt, um aus einer Datensammlung alle Elemente der Reihe nach zu bearbeiten. Im ersten Beitrag zur Serie "Wie funktioniert LINQ?", schauen wir uns diese beiden Interfaces einmal genauer an.

Wie enumeriert man eine Klasse?

Beim Enumerieren wird eine Datensammlung Element für Element durchlaufen.Da die Elemente einzeln nacheinander abgefragt werden, muss an einer Stelle gespeichert werden, welches das letzte Element war, das durchlaufen wurde. Solch einen Status wird normalerweise von einem Zustandsautomaten (eng. Statemachine) festgehalten. Die Implementierung ist relativ aufwändig, wir schauen uns das mal an einer einfachen Klasse an:

class Collection<T>
{
	private T[] _data;
	private int _current;

	public Collection(int capacity)
	{
		_data = new T[capacity];
		_current = -1;
	}

	public T this[int index]
	{
		get { return _data[index]; }
		set { _data[index] = value; }
	}

	public T Current
	{
		get
		{
			return _data[_current];
		}
	}

	public bool MoveNext()
	{
		if (_current >= _data.Length-1)
		{
			return false;
		}
		else
		{
			_current++;
			return true;
		}
	}
}
Listing 1: Eine enumerierbare Collection-Klasse mit gemerktem Zustand
Die Klasse hat intern ein Array vom Typ T um Daten zu speichern (Zeile 3) und das Feld _current (Zeile 3) um das zuletzt zurückgegebene Element zu "merken". Beim Aufruf von der Methode MoveNext() wird der interne Merker auf das nächste Element gesetzt (sofern vorhanden) und true oder false zurückgegeben, je nachdem ob ein weiteres Element vorhanden war, oder nicht. Damit wir später einfacher mit einer while-Schleife über die Collection enumerieren können, startet _current bei -1. Beim Aufruf der Eigenschaft Current wird das Element zurückgegeben, das gerade an der Reihe ist, also auf das unser Merker "zeigt".
Mit dieser Klasse können wir nun mit einer while-Schleife die einzelnen Elemente der Reihe nach ausgeben.
Collection<int> numbers = new Collection<int>(3);
numbers[0] = 1;
numbers[1] = 2;
numbers[2] = 3;

while (numbers.MoveNext())
{
   Console.WriteLine(numbers.Current);
}
Listing 2: Das Enumerieren der Klasse mit einer while-Schleife
Unsere erste Überlegung war richtig, wir können über die Collection enumerieren. Aber wir sind noch nicht fertig, was passiert wenn wir auf unterschiedliche Arten über die Collection iterieren möchten? Stellen Sie sich vor, in Thread A geben wir die Elemente 1,2 aus, möchten dann mit Thread B die Elemente 1,2,3 und am Ende mit Thread A noch Element 3 ausgeben. Mit unserer derzeitigen Lösung funktioniert das nicht auf einem gemeinsamen Collection-Objekt, da der Status unsere Enumeration pro Klasse gespeichert wird. Um dieses Problem zu umgehen, erzeugen wir eine Iterator-Klasse.
class Collection<T>
{
	private T[] _data;

	public Collection(int capacity)
	{
		_data = new T[capacity];
	}

	public T this[int index]
	{
		get { return _data[index]; }
		set { _data[index] = value; }
	}

	public Iterator<T> GetIterator()
	{
		return new Iterator<T>(this);
	}
	
	public class Iterator<T>
	{
		private Collection<T> _collection;
		private int _current;

		public Iterator(Collection<T> collection)
		{
			_collection = collection;
			_current = -1;
		}

		public T Current
		{
			get
			{
				return _collection._data[_current];
			}
		}

		public bool MoveNext()
		{
			if (_current >= _collection._data.Length - 1)
			{
				return false;
			}
			else
			{
				_current++;
				return true;
			}
		}
	}
}
Listing 3: Eine enumerierbare Collection-Klasse mit einer nested Iterator-Klasse.

Die Iterator-Klasse ist nested deklariert und übernimmt jetzt die Funktion der Statemachine und bietet die Methode MoveNext und die Eigenschaft Current an. Um eine Iterator-Instanz zu bekommen, ruft man auf das Collection-Objekt die GetIterator() - Methode auf. Dadurch ist gewährleistet, das nun unabhängig über diese Collection enumeriert werden kann:

Collection<int> numbers = new Collection<int>(3);
numbers[0] = 1;
numbers[1] = 2;
numbers[2] = 3;

var it1 = numbers.GetIterator();
var it2 = numbers.GetIterator();

it1.MoveNext();
it2.MoveNext();

Console.WriteLine(it1.Current);
Console.WriteLine(it2.Current);
Listing 4: Mit der Iterator-Klasse kann nun unabhängig enumiert werden.

An diesem einfachen Beispiel kann man bereits erkennen das die Implementierung einer enumerierbaren Klasse keine einfache Aufgabe ist und viel zusätzlicher Code nötig ist. Später schauen wir uns noch ein C#-Konzept an, um das alles vom Kompiler erzeugen zu lassen.

Enumierierbar mit IEnumerable<T>

In unserem Beispiel hat die Klasse Collection die GetIterator()-Methode, um die Referenz auf ein Objekt zu bekomme, mit dem ich über die Collection enumerieren kann. Würde man dieses Muster in anderen Klassen so implementieren, könnte man sagen, das alle Klassen welche die Methode GetIterator() anieten, sind enumerierbar. Dann könnte ich diese "Fähigkeit" theoretisch auch durch ein Interface deutlich machen, oder? Genau das macht das Interface IEnumerable<T>, welches im .NET-Framework bereits implementiert ist. In diesem Interface ist nur eine Methode GetEnumerator() deklariert (Gleiche Idee wie GetIterator() aus dem Beispiel), die dem Aufrufer einen Enumerator zurückgibt, mit dem über diese Elemente enumeriert werden kann.

public interface IEnumerable<out T> : IEnumerable
{
     IEnumerator<T> GetEnumerator();
}
Listing 5: Das Interface IEnumerable<T>
Dieses Interface wird von allen gängigen Collections wie List, Dictionary usw. implementiert, das heßt also, alle diese Klassen sind enumerierbar. Darüber hinaus gibt es auch noch eine nicht generische Variante des Interfaces, allerdings spielt das für unsere Betrachtungen keine Rolle.

Enumerieren mit IEnumerator<T>

Wenn man auf ein Objekt einer Klasse, welches das Interface IEnumerable<T> implementiert, die GetEnumerator()-Metode aufruft, bekommt man ein Enumerator-Objekt zurückgeliefert. Dieses Objekt nutzt das selbe Konzept wie die Iterator-Klasse aus unserem Beispiel, implementiert allerdings zusätzlich das Interface IEnumerator<T>.

public interface IEnumerator
{
	object Current { get; }
	bool MoveNext();
	void Reset();
}

public interface IEnumerator<out T> : IDisposable, IEnumerator
{
	T Current { get; }
}
Listing 6: Die Interfaces IEnumerator<T> und IEnumerator

Das Interface leitet von IEnumerator ab und so verpflichten sich Klassen, die IEnumerable<T> implementieren, die Methoden MoveNext(), Reset() und die Eigenschaft Current zu implementieren. Auch das entspricht unserer Strategie aus dem Beispiel.

Enumerierbarkeit im .NET-Framework

Um enumerierbare Klassen im .NET-Framework zu nutzen, gibt es zahlreiche Methoden und Sprachkonstrukte, wie z.B: die foreach-Schleife. Damit enumerierbare Klassen eine gemeinsame Schnittstelle haben, wurden die Interfaces IEnumerable<T> und IEnumerator<T> eingeführt. Um das Ganze zu verdeutlichen, schauen wir uns die Metadaten der Klasse List<T> an (unwichtige Bereiche wurden entfernt):

public class List<T> : IEnumerable<T>, IEnumerable
{
	public List<T>.Enumerator GetEnumerator();

	public struct Enumerator : IEnumerator<T>, IEnumerator
	{
		public T Current { get; }
		public bool MoveNext();
	}
}
Listing 7: Die Metdaten der Klasse List<T>

Analog zu unsem Beispiel wurde von den Entwicklern des .NET-Frameworks das Interface IEnumerable<T> implementiert, um eine gemeinsame Schnittstelle für das Erzeugen eines Enumerators zu haben. Weiterhin wurde eine nested Struktur definiert (bei uns war es eine Klasse), um einen Unabhängigen Enumerator zu erzeugen.

 

.NET DevCon 2011 in Nürnberg

image

Vom 6.-7. Juni war ich als Referent auf der .NET DevCon in Nürnberg zu Gast. Die .NET DevCon, welche von dem Verlag der dotnetpro veranstaltet wird,  fand zum ersten Mal statt und hat sich zur Aufgabe gemacht, aktuell verwendete Technologien zu vermitteln und weniger auf Zukunftsperspektiven ausgelegt zu sein. Im Development- und VisualStudio-Track war ich dabei mit drei Vorträgen vertreten.

C# 2.0
Immer wieder trifft man auf Entwickler die zwar sehr gut entwickeln können, aber meist viele der Spracherweiterungen aus C# 2.0 noch nicht richtig beherrschen und daher manche Aufgaben unnötig kompliziert lösen und dabei viel Zeit sparen könnten. In dieser Session ging es um die wichtigsten Neuerungen Generics, Nullable Value Types, Anonyme Methoden und Iteratoren.

LINQ it all together!
Eine der wohl faszinierendsten Technologien ist LINQ. Um diese Technologie zu ermöglichen, wurde die Sprache C# in der Version 3.0 abermals um einige Sprachkonstrukte erweitert. Welches diese Sprachkonstrukte sind, wie und wo sie eingesetzt werden und wie LINQtoObjects unter der Haube funktioniert, war Bestandteil dieser Session.

Debugging 2.0
Jeder Entwickler beherrscht den Debugger – sagen Sie. Natürlich, der Debugger in Visual Studio ist so intuitiv bedienbar, dass jeder Entwickler mit wenig Einarbeitungszeit schnell produktiv nach Fehlern suchen kann. Aber wissen Sie wie man entfernte Programme debuggt? Oder wie man auch Quellcode aus dem .NET-Framework beim debuggen berücksichtigen kann? Oder wie man herausfindet, was passiert ist, bevor das Programm angehalten wurde? In dieser Session ging es neben den normalen Features des Debuggers besonders um die Tricks und Kniffe und die neue Technologie IntelliTrace aus Visual Studio 2010 Ultimate.

Meine Sessions waren dabei alle sehr gut Besucht und das Teilnehmerfeedback war ausgezeichnet. Ich möchte mich an dieser Stelle noch einmal bei allen Teilnehmern und natürlich auch bei dem Veranstalter bedanken und hoffe auf ein Wiedersehen bei der nächsten .NET DevCon.

Neuer Blog

Lange hat es gedauert, aber nach den Ganzen Konferenzen und Workshops der letzten Zeit, habe ich heute endlich meinen neuen Blog auf Basis von BlogEngine.NET nebst Design fertiggestellt. Leider gibt es derzeit noch einen Bug beim importieren von BlogML-Dateien und so kann ich derzeit leider meine alten Blogeinträge nicht migrieren, ich hoffe aber das der Fehler mit der nächsten Version behoben wird. Ich wünsche allen Besuchern viel Spaß :)