C# Basics Teil 3

Der dritte Teil handelt von den Preprozessor Direktiven, die von Leuten mit C/C++ Erfahrung ohne weiteres übersprungen werden können. Interessanter wird es bei den Iteratoren.

Preprozessor Direktiven

Die Preprozessor Direktiven sind alten C/C++ Hacker längstens bekannt. Es ist auch ein offenes Geheimnis, dass man damit allerlei Unfug im Code anstellen kann. Es sind schlichte Anweisungen an den Preprozessor, gewisse Dinge vor der Komplation ein-/abzuschalten. Sie werden mit dem Prefix # gekennzeichnet. Eine Direktive die sehr grosse Bekanntheit geniesst, ist das Paar #region / #endregion. Diese werden jedoch nur zur Gliederung verwendet.

Beispiel:

#define MYSYMBOL

#if DEBUG
public void Foo() { }
#elif MYSYMBOL
public void Foo(int a) { }
#else
public void Foo2() { }
#endif

In diesem Beispiel werden die Direktiven #define, #if, #elif und #endif dafür verwendet, eine Methode Foo() je nach dem in den Code zu kompilieren oder nicht. Im Debug-Modus gibt es die Methode Foo(). Wird bevor der Preprozessor diesen Code prozessiert ein #define MYSYMBOL gesetzt (und mit #undefine MYSYMBOL nicht wieder rückgängig gemacht) und nicht im Debug-Modus kompiliert, kann auf die Foo(int a) Methode zugegriffen werden. Im Else-Fall (z.B. Release-Modus) ohne definiertes MYSYMBOL gibt es nur die Foo2() Methode. Bereits bei dieser Beschreibung sollte es Auffallen, dass diese Direktiven nur in wenigen Situationen benutzt werden sollten. Um den Programm-Code zu beschleunigen, könnte man z.B. alle Ausgaben in einer Console nur in Debug-Kompilationen ermöglichen.

Pragma Direktiven

Die Pgragmas werden verwendet, um dem Kompiler zusätzliche Anweisungen zur Kompilation an einer bestimmten Stelle zu geben. Es gibt ein pragma, um Warnungen ein-/ausgeschalten zu können. Ein weiteres Pragma kann benutzt werden, um die checksum der Datei ins Debug-File (*.PDB) zu schreiben. Das #pragma pack(…) von C/C++ bekannt, um Typen, Strukturen das Verhalten im Stack zu manipulieren, wurde bis zum jetztigen Zeitpunkt zum Glück nicht implementiert.

Beispiel:

#pragma warning disable 414
static string Message = „Hello“;
#pragma warning restore 414

Dieses Beispiel schaltet für eine Programmzeile die Warnung „…Message is assigned but its value is never used“ aus.

Was es sonst noch für Direktiven gibt, kann in der MSDN nachgelesen werden.

Die Semantik von Iteratoren

Ein Iterator wird benutzt, um eine Menge von Elementen mit einer Schleife zu durchlaufen. Um eine eigene Klasse mit einer Iterationsmöglichkeit zu bestücken, muss IEnumerable<T> oder IEnumerator<T> implementiert werden. Die jeweiligen nicht generischen Interface-Brüder IEnumerable und IEnumerator müssen auch implementiert werden, weil die generischen Schnittstellen sich davon ableiten. Ein grosser Vorteil von Iteratoren ist die Kapselung der Logik in eine andere Klasse. Zudem kann jede Klasse die das IEnumerable<int> implementiert für foreach-Schleifen benutzt werden. Das Konstrukt gibt uns zusätzlich die Möglichkeit, die internen Werte eines privates Feld nach aussen zugänglich zu machen, ohne die Sichtbarkeit des Feldes selber zu verändern.

Beispiel:

public class Foo : IEnumerable<int>
{
// Internes Feld
private int[] _dump = new int[] { 1, 2, 3 };

// Implementation der generischen Schnittstelle
public IEnumerator<int> GetEnumerator()
{
for (int i = 0; i < _dump.Length; i++)
yield return _dump[i];
}

// Explizite Implementation der „alten“ Schnittstelle.
// Mit einer einfachen Weiterleitung ist dies bereits erledigt.

IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
}

Man beachte das Keyword yield return. Es ist ein ganz spezielles Konstrukt. Der Kompiler merkt sich die Position des Programm Counters, wenn er mit yield return die Methode verlässt. Beim nächsten Aufruf springt er direkt wieder an die zuvor verlassene Stelle und setzt dort das Programm fort. Mit dieser Implementation kann man nun in diversen Arten über die Elemente im dump iterieren:

Foo f = new Foo();

// iteration mit foreach
foreach (int i in f) Console.Write(i);

// iteration mit einem enumerator
IEnumerator<int> e = f.GetEnumerator();
while (e.MoveNext()) Console.Write(e.Current);

// iteration mit linq
var v = from n in f select n;
foreach (int i in v) Console.Write(i);

Der eigene Enumerator

Möchte man nun einen eigenen Enumerator schreiben, um die Logik von der Haupklasse auszulagern, muss man das IEnumerator<T> implementieren. Natürlich kann man nun yield return nicht mehr verwenden und muss einen eigenen index führen.

Beispiel:

public class FooEnumerator : IEnumerator<int>
{
private int[] _dump = null;
private int currentIndex = -1;
internal FooEnumerator(int[] dump) { _dump = dump; }
public int Current { get { return _dump[currentIndex]; } }
public void Dispose() { /* Nicht benutzt */ }
object IEnumerator.Current { get { return Current; } }
public bool MoveNext() { return ++currentIndex < _dump.Length; }
public void Reset() { currentIndex = -1; }
}

Nun muss die Hauptklasse diesen Enumerator nur noch instanzieren und zurückgeben.

public class Foo : IEnumerable<int>
{
// … bisschen code …
public IEnumerator<int> GetEnumerator()
{
return new FooEnumerator(_dump);
}
// … bisschen code …
}