Tipps und Tricks

Veröffentlicht: 20. Mrz 2002 | Aktualisiert: 21. Jun 2004

Von Eric Gunnerson

In der letzten Folge haben wir uns mit der Verwendung von unsicherem Code für die Bildverarbeitung beschäftigt. In dieser Kolumne hätten wir eigentlich mehr Zeit auf die Untersuchung von unsicherem Code verwenden sollen, doch habe ich das Programm ein wenig geändert. Dies hat zwei Gründe: Erstens habe ich es noch nicht geschafft, den Code zu schreiben. Und zweitens würde ich gerne einige allgemeine Fragen beantworten, um nicht so viel Zeit darauf zu verwenden, sie an anderer Stelle zu beantworten. Nächsten Monat werden wir uns (wahrscheinlich) wieder mit unsicherem Code befassen.

Working with C#: Tips and Tricks
Auf dieser Seite

 Windows Forms und das Konsolenfenster
 IntelliSense, XML-Dokumentation und Bibliotheken
 Verbessern von "foreach"
 Sortieren von Hashtabellenschlüsseln
 "SubList"-Iteration
 Zufällige Iteration
 Hashtabellenwerte
 Zusammenfassung

Windows Forms und das Konsolenfenster

Unser erstes Thema beginnt mit einer Frage.

Was ist der Unterschied zwischen einem Windows-Forms-Projekt und einem Konsolenanwendungsprojekt?

Die recht überraschende Antwort ist ziemlich kurz. Der einzig echte Unterschied besteht darin, dass eine Konsolenanwendung eine Ausgabe an ein Konsolenfenster sendet, eine Windows-Forms-Anwendung jedoch nicht. Dies ist jedoch lediglich das Standardverhalten. Es gibt keinen Grund, warum eine Windows-Forms-Anwendung nicht über ein Konsolenfenster verfügen sollte.

Das Aktivieren des Konsolenfensters ist einfach. Klicken Sie in Microsoft Visual Studio mit der rechten Maustaste auf das Projekt, und wählen Sie Eigenschaften. Ändern Sie den Ausgabetyp von Windows-Anwendung in Konsolenanwendung. Alternativ dazu können Sie auch in der Befehlszeile

/target:exe

statt

/target:winexe 

verwenden.

Ich fand dies beim Debuggen meiner Anwendungen recht nützlich, und außerdem lässt sich die Konsolenanwendung anschließend wieder ganz leicht deaktivieren.

 

IntelliSense, XML-Dokumentation und Bibliotheken

Wenn Sie Ihren Code in C# schreiben und das XML-Dokumentationsfeature verwenden, zeigt die Visual Studio-IDE diese Informationen automatisch im Anweisungsvervollstänigungsfenster an. Ich könnte z.B. den folgenden Code schreiben:

class Vector 
{ 
   /// <summary> 
   /// Vektorlänge berechnen 
   /// </summary> 
   public double Length 
   { 
      get 
      { 
         return(Math.Sqrt(x * x + y * y)); 
      } 
   } 
}

Anschließend integriere ich diese Klasse in mein Projekt, deklariere einen Vector mit dem Namen v und gebe Folgendes ein:

double l = v.Length;

Sobald ich den Punkt eingegeben habe, wird ein Popupfenster mit den statischen Funktionen für Vector angezeigt. Wenn ich Length hervorhebe, wird der Text Vektorlänge berechnen zusammen mit dem Prototyp der Funktion angezeigt. Das ist wirklich ein tolles Feature.

Ich verwende die Vector-Klasse eine Weile. Da sie sich als recht nützlich erweist, beschließe ich, sie in einer separaten Klassenbibliothek zu platzieren, damit ich sie in verschiedenen Projekten einsetzen kann. Anschließend kann ich die Klassenbibliothek als Verweis auf mein Projekt verwenden.

In diesem Fall zeigt mir die Anweisungserstellung jedoch leider den Zusammenfassungstext nicht mehr an. Damit dies wieder funktioniert, muss eine separate Dokumentationsdatei generiert werden. Klicken Sie dazu mit der rechten Maustaste auf das Projekt, wählen Sie Eigenschaften und dann Konfigurationseigenschaften, und geben Sie einen Dateinamen für die XML-Dokumentationsdatei ein. Der gewählte Dateiname sollte dem der Klassenbibliothek entsprechen, muss jedoch statt der Erweiterung .dll die Erweiterung .xml erhalten. Ansonsten können Sie auch die /doc-Option mit dem Befehlszeilencompiler verwenden.

Nachdem Sie die XML-Datei erstellt haben, legen Sie sie an demselben Speicherort ab wie die Assembly. Auf diese Wiese funktioniert alles einwandfrei. Dieses Verfahren bietet einen weiteren Vorteil: Der Compiler führt eine Überprüfung durch, um sicherzustellen, dass Sie alle Mitglieder der Klasse dokumentiert haben.

 

Verbessern von "foreach"

Wie ich bereits vor einiger Zeit erwähnte, habe ich eine ganze Menge Code in Perl geschrieben. Perl verfügt über einige integrierte Auflistungsoperationen, die äußerst nützlich sein können. Wenn ich den Inhalt einer Hashtabelle ausschreibe, kann ich z.B. folgenden Code verwenden:

my %animals; 
$animals{"Hund"} = 45; 
$animals{"Katze"} = 47; 
$animals{"Erdferkel"} = 3; 
$animals{"Wal"} = 0; 
foreach my $key (keys(%animals)) 
{ 
   print "$key $animals{$key}\n"; 
}

Das lässt sich ziemlich einfach in C# schreiben. Der Code würde folgendermaßen aussehen:

foreach (string s in animals.Keys)

Wenn Sie diesen Code ausführen, werden Sie feststellen, dass die Schlüssel in einer beliebigen Reihenfolge aufgelistet werden. Ich möchte sie gerne alphabetisch sortieren. In Perl können Sie z.B. folgenden Code schreiben und die integrierte sort()-Funktion verwenden, um die Schlüssel zu sortieren:

foreach my $key (sort(keys(%animals)))

Da es hier keine einfache Möglichkeit zu geben scheint, dies in C# zu bewerkstelligen, würde ich die Schlüssel in einer ArrayList platzieren und diese dann in foreach verwenden.

Isolation

Einige Monate später las ich einen Beitrag in einer Newsgroup, in dem sich jemand mit dem Löschen von Elementen aus einer Auflistung in einer foreach-Schleife befasste. Der Benutzer wollte ungefähr folgenden Code schreiben:

foreach (string key in hashtable.Keys) 
{ 
   int value = hashtable[key]; 
   if (value == 0) 
   { 
      hashtable.Delete(key); 
   } 
}

Wenn dieser Code ausgeführt wird, generiert er eine Exception, da es nicht möglich ist, eine Auflistung zu ändern, während eine Enumeration für sie ausgeführt wird. Das ist eine ziemlich gute Idee, da Sie gewöhnlich vermeiden möchten, dass Auflistungen ohne Ihr Zutun geändert werden. Manchmal kann es jedoch auch störend sein.

Dies brachte mich zum Nachdenken, und ich erkannte, dass hier eine Klasse benötigt wird, die die
Aufzählung von der Auflistung isoliert. Diese Klasse würde eine volle Aufzählung für die Auflistung durchführen und die Daten speichern, um ihr eigenes Enumerationsobjekt offen zu legen. Es wäre in Ordnung, die Auflistung bei Verwendung dieses Objekts zu ändern.

Ich schrieb daher den Code für die folgende Klasse:

public class IterIsolate: IEnumerable 
{ 
   internal class IterIsolateEnumerator: IEnumerator 
   { 
      ArrayList items = new ArrayList(); 
      int currentItem; 
      internal IterIsolateEnumerator(IEnumerator enumerator) 
      { 
         while (enumerator.MoveNext() != false) 
         { 
            items.Add(enumerator.Current); 
         } 
         IDisposable disposable = enumerator as IDisposable; 
         if (disposable != null) 
         { 
            disposable.Dispose(); 
         } 
         currentItem = -1; 
      } 
      public void Reset() 
      { 
         currentItem = -1; 
      } 
      public bool MoveNext() 
      { 
         currentItem++; 
         if (currentItem == items.Count) 
            return false; 
         return true; 
      } 
      public object Current 
      { 
         get 
         { 
            return items[currentItem]; 
         } 
      } 
   } 
   public IterIsolate(IEnumerable enumerable) 
   { 
      this.enumerable = enumerable; 
   } 
   public IEnumerator GetEnumerator() 
   { 
      return new IterIsolateEnumerator(enumerable.GetEnumerator()); 
   } 
   IEnumerable enumerable; 
}

Dieses Codesegment enthält zwei Klassen: IterIsolate wird verwendet, damit wir etwas übergeben können, das IEnumerable in eine foreach-Anweisung implementiert. Sie speichert alles, was das IEnumerable-Objekt in den Konstruktor übergibt, und wenn sie für eine Enumeration aufgerufen wird, gibt sie eine Instanz von IterIsolateEnumerator zurück.

IterIsolateEnumerator ist recht einfach. Diese Klasse enumeriert alle Elemente in der Auflistung, speichert sie in einer ArrayList und implementiert IEnumerator

Mit diesen Klassen kann ich nun den unten stehenden Code schreiben. Statt die Keys-Auflistung direkt an foreach zu übergeben, erstelle ich ein neues IterIsolate-Objekt aus der Auflistung und übergebe es an foreach. Wie Sie sehen können, wird es mir dadurch ermöglicht, die Hashtabelle in meinem foreach zu ändern.

public class IterIsolateTest 
{ 
   public static void Main() 
   { 
      Hashtable hash = new Hashtable(); 
      hash.Add("A", 1); 
      hash.Add("B", 0); 
      hash.Add("C", 1); 
      foreach (string s in new IterIsolate(hash.Keys)) 
      { 
         if ((int) hash[s] == 0) 
            hash.Remove(s); 
      } 
      foreach (string s in hash.Keys) 
      { 
         Console.WriteLine("Wert: {0}", s); 
      } 
   } 
}

Dies funktioniert recht gut, doch gibt es hier einige leistungsrelevante Nachteile, die berücksichtigt werden sollten. Beispiel:

  1. Es müssen Instanzen der Klassen

    IterIsolate 
    

    und

    IterIsolateEnumerator 
    

    erstellt werden.

  2. Alle Elemente in der ursprünglichen Auflistung müssen in ein temporäres Array platziert werden, was bei umfangreichen Auflistungen problematisch sein könnte.

  3. Wenn die Auflistung einen Werttyp über starke Typisierung * zurückgibt, erfolgt ein weiterer Boxing-Vorgang.

  4. Falls die Auflistung starke Typisierung verwendet, geht die Typsicherheit bei der Kompilierung verloren.

* Im stark typisierenden Dialekt: Anstelle der Aufzählung,
die IEnumerator implementiert und eine Current-Eigenschaft vom Typ Object besitzt, enthält die Current-Eigenschaft in dem stark typisierten Idiom dem tatsächlichen Typ in der Auflistung.
Sofern dies kein großes Problem darstellt, funktioniert dieses Verfahren recht gut.

Einige Tage später erkannte ich, dass ich dieses Idiom auch für andere Vorgänge verwenden könnte. Bei einer dieser Funktionen, die von Perl unterstützt werden, handelt es sich um Reverse(), welches das Umkehren der Listenfolge ermöglicht. Ich schrieb einen weiteren Iterator namens IterReverse, der genauso funktionierte wie IterIsolate, nur dass er die Liste in umgekehrter Reihenfolge durchlief. Allmählich wurde die Sache interessant.

 

Sortieren von Hashtabellenschlüsseln

Der Schritt vom Umkehren zum Sortieren ist einfach. Wenn das in der Auflistung enthaltene Objekt IComparable implementiert - und das sollte es, sofern die Sortierung sinnvoll ist -, müssen Sie lediglich IterIsolate ändern, um Sort()für die ArrayList aufzurufen, nachdem sie aufgefüllt wurde. Das ist ziemlich einfach, und ich kann nun Code schreiben, der genau dasselbe tut wie mein Perl-Code:

foreach (string s in new IterSort(hash.Keys)) 
{ 
   Console.WriteLine("{0} = {1}", s, hash[s]); 
}

Das Ganze lässt sich noch flexibler gestalten, da die IterSort-Klasse auch einen IComparer aufnehmen kann, so dass Sie bei Bedarf eine unterschiedliche Sortierung angeben können.

 

"SubList"-Iteration

Manchmal möchten Sie einige der Einträge durchlaufen, ohne alle anzuzeigen. Die IterSubList-Klasse ermöglicht dies. Der folgende Code durchläuft z.B. die Testauflistung und überspringt dabei ein Element am Anfang und eines am Ende der Liste.

foreach (string s in new IterSubList(test, 1, 1))

 

Zufällige Iteration

Ich habe eine Reihe von Kartenspielen für PCs geschrieben, bei denen es möglich sein musste, die Karten nach dem Zufallsprinzip zu geben. Dies kann mit einer Klasse namens IterRandom erreicht werden, die die Elemente in der Auflistung in zufälliger Reihenfolge verteilt.

 

Hashtabellenwerte

Ebenso kommt es beim Umgang mit Daten in einer Hashtabelle häufig vor, dass die Einträge basierend auf den Werten in der Hashtabelle sortiert werden sollen. Wir würden dazu gerne dasselbe IComparable-Verfahren verwenden wie zuvor, doch ist dies mit ein wenig mehr Arbeit verbunden. Was wir brauchen, ist eine Klasse mit einer IComparable-Implementierung, die die Hashtabellenwerte über die Schlüssel sucht und anschließend vergleicht.

Dazu wird eine weitere Klasse namens SortHashValueItem erstellt. Für jeden Schlüssel in der Hashtabelle wird eine Instanz dieser Klasse erstellt, wobei der Hashtabellenschlüssel, der Hashtabellenverweis und ein IComparer-Objekt (sofern anwendbar) gespeichert werden. Diese Klasse implementiert IComparable, und die CompareTo()-Funktion leitet den Vergleich an die Werte in der Hashtabelle oder den IComparer weiter.

Mit diesem Iterator kann ich nun genau das tun, was ich möchte:

Hashtable hash = new Hashtable(); 
hash.Add("Hund", 45); 
hash.Add("Katze", 46); 
hash.Add("Erdferkel", 3); 
hash.Add("Wal", 0); 
foreach (string s in new IterSortHashValue(hash)) 
{ 
   Console.WriteLine("{0} = {1}", s, hash[s]); 
}

Dieser Code liefert die folgende Ausgabe:

Wal = 0 
Erdferkel = 3 
Hund = 45 
Katze = 46

 

Zusammenfassung

Ich finde diesen Ansatz außerordentlich interessant, obwohl ich nicht der Ansicht bin, dass meine Ideen hier besonders originell sind. Wenn Sie andere Ideen für solche Iteratoren haben, schreiben Sie mir. Sie haben wahrscheinlich bereits erkannt, dass sich die Iteratoren durch folgenden Code aneinander reihen lassen:

foreach (string s in new IterReverse(new IterSort(new IterSubList(items, 1, 5))))

In meiner ersten Implementierung resultierte dies in der Erstellung von drei temporären ArrayList-Objekten, doch in der aktuellen Version teilen sich die Iteratoren eine einzelne ArrayList.