Jochens Weblog

ESCde Developer Blog

  Home :: Kontakt :: RSS Feed
  12 Posts :: 0 Artikel :: 4 Kommentare :: 0 Trackbacks

Archiv

Post Kategorien

ESCde

ESCde Blogger

Montag, 01. Oktober 2007 #

Sie suchen detailierte Informationen zum TFS? Dann sollten Sie mal unter

http://www.codeplex.com/TFSGuide nachsehen...


Sie haben in einer Variablen einen Verzeichnispfad, in einer anderen einen Dateinamen. Ziel: Den exakten Pfad zur Datei erzeugen. Das Problem hierbei ist, dass man in solchen Fällen zumeist umständlich prüfen muss, ob der Verzeichnispfad mit einem '\' aufhört oder nicht. Vergisst man die Problematik komplett, führt dies zwangsläufig zu Fehlern. Denkt man daran, entsteht häufig sehr unschöner Code.

Doch das .NET Framework kann hierbei helfen: Mittels der statischen Methode System.IO.Path.Combine() lässt sich dieser Test (sowie das einfügen eines '\', falls nötig) mit nur einem Aufruf erledigen:

string strPath = "C:\\MyFolder"
string strFileName = "MyFile.txt"
string strComplete = System.IO.Path.Combine(strPath, strFileName);


Mittwoch, 13. Juni 2007 #

Als erprobter C#-Entwickler kennt man natürlich das Konzept des Boxing/Unboxing: Struct-Instanzen liegen üblicherweise auf dem Stack. Möchte man allerdings

Methoden auf diesen Instanzen ausführen (Beispielsweise die "ToString()"-Methode), so muss man das Struct zunächst in einen Referenztyp verwandeln: Die

Struct-Instanz wird auf den Heap verschoben und es wird auf dem Stack eine Referenz darauf angelegt. Beispielsweise so:

struct MyStruct {}

MyStruct ms; // Instanz von MyStruct auf dem Stack erstellen
object obj = (object) ms; // ms wird auf den Heap kopiert, obj verweist auf dieses Objektms.ToString();
// nun kann ToString() aufgerufen werden

Soweit nichts neues. Allerdings gibt es auch Situationen, in denen das Geschehen nicht so offensichlich ist wie hier. Betrachten Sie die folgenden

CodeZeilen:

struct MyStruct
{
    private int i;
    public void SetI(int x) { i = x; };
}

public static void Main()
{
    MyStruct ms;
    ms.SetI(1);
    object obj = (object) ms;
    ((MyStruct)obj).SetI(5); // [1]
}

In diesem Codeausschnitt wird ein Element vom Typ MyStruct erzeugt, welches einen privaten Integer sowie eine öffentliche Methode hat, um diesen zu setzen.

In der Main-Methode wird dieser zuerst auf den Wert 1 gesetzt, anschließend wird das Objekt auf den Heap geboxt. Interessant wird es in der mit [1]

markierten Zeile: Hier konvertieren wir obj nach MyStruct, um anschließend mittels SetI() den Wert des structs zu ändern. Auf den ersten Blick wirkt es so,

als würde die Objektreferenz obj in eine MyStruct-Referenz konvertiert und SetI(8) somit auf dem Objekt auf dem Heap ausgeführt. Aber weit gefehlt: Da

MyStruct nach wie vor ein Wertetyp ist (und eben kein Referenz-Typ) wird in dieser Situation Unboxing durchgeführt: Das von obj referenzierte Objekt wird

zurück auf den Stack kopiert und SetI auf dieses Element durchgeführt. Anschließend wird dieses Objekt wieder gelöscht (da wir den Wert nicht in einer

Variablen abspeichern). Die auf dem Heap liegende MyStruct-Instanz, auf die obj verweist, hat also auch nach dem SetI(8)-Aufruf noch den Wert 1 in i

gespeichert.

Quintessenz des Ganzen ist, dass structs, die auf den Heap geboxt wurden nicht veränderbar sind. Bei einer Konversion in einen Struct-Datentyp wird immer

unboxing aufgerufen, die Objekte auf dem Heap werden hierbei nicht angerührt, auch dann nicht, wenn die Syntax zunächst anderes vermuten lässt.

Aber Vorsicht: eine Variante gibt es doch noch, um Struct-Elemente auf dem Heap zu bearbeiten: Interfaces. Betrachten Sie die folgende Variante des obigen

Codes:

Interface I { void SetI(int x); }

struct MyStruct : I
{
    private int i;
    public void SetI(int x) { i = x; };
}

public static void Main()
{
    MyStruct ms;
    ms.SetI(1);
    Object obj = (object) ms;
    ((I)obj).SetI(5); // [2]
}

Nun wird die SetI()-Methode von einem Interface angeboten. MyStruct implementiert dieses Interface. Wen wir nun wie in Zeile [2] unsere Objekt-Referenz nicht

nach MyStruct sondern nach I konvertieren, so "trifft" unser SetI(5)-Aufruf wirklich das Objekt auf dem Heap, ohne eine Kopie zu erstellen. Dies liegt daran,

dass Interfaces immer Referenztypen sind. Daher kommt der Cast von Object nach Interface auch ohne Unboxing aus.


Mittwoch, 25. April 2007 #

Das grundlegende Konzept der Referenz unter .NET sollte jedem klar sein: Eine Referenz identifiziert ein Objekt, ohne gleichzeitig die exakte Speicheradresse des Objektes zu bezeichnen. Solange der Entwickler eine Referenz auf ein Objekt hat, ist gewährleistet, dass dieses nicht vom Garbage Collector "zerstört" wird. Sobald der Entwickler die letzte Referenz eines Objektes auf "null" setzt, ist das Objekt unereichbar und darf vom Garbage Collector abgebaut werden. Diese Referenzen nennt man auch "starke Referenzen" ("strong reference").

Im Unterschied hierzu gibt es allerdings auch "schwache" Referenzen ("weak references"). Bei diesen hat der Entwickler noch eine Referenz auf sein Objekt, allerdings kann der Garbage Collector das Objekt trotzdem zerstören. Zunächst hört sich dieses Konzept völlig sinnfrei an, allerdings gibt es Situationen, in denen schwache Referenzen sehr hilfreich sein können: Stellen Sie sich vor, Sie schreiben eine Anwendung, die aus zwei relativ unabhängigen Bereichen besteht. Zunächst arbeitet der Anwender in Bereich A, wo sich sehr viele, große Objekte ansammeln, beispielsweise eine Liste aller Dateien des Rechners. Nun wechselt der Anwender in Bereich B. Ob er wieder in Teil A zurückkehrt, ist ungewiss. Falls der Garbage Collector nun Speicher frei machen muss, sollte er also prinzipiell unsere Dateiliste aus Teil A löschen können. Wenn wir hierzu unsere Referenzen alle auf "null" setzen, ermöglichen wir dem Garbage Collector diese Option. kehrt der User allerdings in Bereich A zurück (und der GC musste zuvor nicht aktiv werden), würde unsere Liste im Speicher zwar noch existieren, allerdings müssten wir sie neu aufbauen, da wir keine Referenz mehr haben. Genau für solche Fälle sind die schwachen Referenzen.

Und nun wollen wir uns mal ansehen, wie man solch eine schwache Referenz einsetzt:

using System; using System.Collections.Generic; using System.Text; namespace ConsoleApplication5 { class Program { class BigChunk { } static void Main(string[] args) { // das große Objekt BigChunk bc = new BigChunk(); // schwache Referenz auf das Objekt erzeugen, starke Referenz freigeben System.WeakReference wr = new WeakReference(bc); bc = null; // starke Referenz aus schwacher wiederherstellen: BigChunk bc2 = (BigChunk) wr.Target; if (bc2 == null) { // der Garbage Collector war schneller... bc2 = new BigChunk(); } } } }

Das Erzeugen einer schwachen Referenz ist sehr einfach, man muss lediglich eine starke Referenz auf das betroffene Objekt als Konstruktionsparameter übergeben. Allerdings muss man aufpassen, dass man auch auf jeden Fall die starke Referenz (beziehungsweise alle vorhandenen) auf null setzt. Sonst ist das Objekt trotz der erzeugten schwachen Referenz für den Garbage Collector unerreichbar.

Möchte man später wieder mit dem Objekt arbeiten, weißt man einfach die Target-Eigenschaft der schwachen Referenz einer starken Referenz zu. Hierbei muss man allerdings auf den Cast achten! Anschließend muss man die starke Referenz noch auf null hin überprüfen. Zeigt die Referenz auf null, so hat der Garbage Collector das Objekt in der Zwischenzeit gelöscht und man muss es erneut erzeugen.

Abschließend bleibt noch zu sagen, dass man schwache Referenzen nur dann einsetzen sollte, wenn sich in den betroffenen Objekten keine Daten befinden, die man nicht selbst wiederherstellen kann. Schwache Referenzen bieten also einen Ansatz zur besseren Ausnutzung des Speichers und einer schöneren Zusammenarbeit mit dem Garbage Collector. Allerdings sollten sie immer wohlbedacht angewendet werden!


Montag, 02. April 2007 #

Die beiden Schlüsselwörter is und as sollte jeder C#-Programmierer kennen: Mit is kann geprüft werden, ob ein Objekt Instanz eines bestimmten Typs ist. Das Ergebnis dieses Aufrufs ist ein Boolean:

if (a is B) { B b = (B)a; // b verwenden... }

In diesem Beispiel gehe ich davon aus, dass B eine von A abgeleitete Klasse ist. Die Verwendung ist nun recht offensichtlich. Alternativ kann man auch das as-Schlüsselwort verwenden. as gibt sofort das gecastete Objekt zurück, beziehungsweise null, falls der cast nicht durchgeführt werden kann.

B b = a as B; if (b != null) { // b verwenden }

Beide Varianten sehen recht schön aus und gehen schnell von der Hand. Nun stellt sich natürlich die Frage, welche Variante man (üblicherweise) verwenden sollte. Zunächst drängt sich natürlich ein Geschwindigkeitsvergleich auf. Man sollte annehmen, dass dieser Code vom Compiler in identischer Art und Weise übersetzt wird, da (oberflächlich betrachtet) ja eigentlich das Gleiche passiert. Aber weit gefehlt: Der entstehende IL-Code sieht nicht nur unterschiedlich aus, sondern es gibt auch einen klaren Gewinner bezüglich der Ausführungsgeschwindigkeit: as.

Die Begründung hierfür ist recht einfach: In der ersten Variante muss die Runtime beim is Befehl prüfen, ob es sich bei a um ein B-Objekt handelt. Dieser Aufwand ist nahezu identisch mit einem cast. Anschließend wird innerhalb des if-Blocks erneut gecastet. Beim as Befehl selbst wird ebenfalls ein "cast-artiger" Test durchgeführt. Im if-Block allerdings nicht. Daher muss bei der as Variante nur einmal der Typ geprüft werden, bei is zweimal. Der null-Test bei as ist so schnell, dass er kaum ins Gewicht fällt. Testet man dies mit der Stopwatch mal durch,  so sieht man dass as tatsächlich die besseren Ergebnisse liefert. Allerdings sind beide Operatoren sehr schnell, so dass man für alltägliche Anwendungen völlig bedenkenlos is verwenden kann. In einer Performance-kritischen Anwendung, beispielsweise in einem XNA-Projekt, kann ein konsequentes beachten solcher Details sich in der Summe schon lohnen.

Betrachtet man den IL-Code, so sieht man auch schon, dass der is-Code ein wenig länger ist. DIe kritische Anweisung ist hierbei der castclass IL-Befehl, der genau dem cast in der if-Abfrage entspricht und den Performance-Unterschied ausmacht.


Dienstag, 06. März 2007 #

Haben Sie sich schon mal Gedanken darüber gemacht, ob eine foreach-Schleife schneller als eine For-Schleife ist? Zugegeben, in den meisten Windows Forms-Anwendungen spielt dies kaum eine Rolle, da die Anzahl der Schleifendurchläufe einfach zu gering ist, als dass ein Unterschied überhaupt auffallen würde. Allerdings gibt es auch Situationen, in denen solche kleinen Optimierungen in der Summe dann wieder relevant werden können, beispielsweise bei der Spieleentwicklung unter XNA oder bei Anwendungen, die mit komplexen, Schleifen-intensiven Algorithmen große Datenmengen durchkauen ("Number-Crunching").

Vergleichen Sie die beiden folgenden Schleifen:

foreach (int i in int_list) { DoSomething(i); }

for (int i = 0; i < int_list.Count; i++) { DoSomething(int_list[i]); }

Diese beiden Schleifen machen semantisch exakt das gleiche, allerdings passiert bei der foreach-Schleife intern deutlich mehr. Implementieren Sie einfach mal das generische Interface IEnumerable, dann sehen Sie sofort den Unterschied.

Je nach Anzahl der Schleifendurchläufe (und natürlich auch abhängig von den Operationen in der Schleife) schwankt der Performance-Unterschied deutlich, allerdings lässt sich festhalten, dass eine for-Schleife gegenüber foreach eigentlich immer die Nase vorn hat. Das heißt natürlich nicht, dass foreach nicht nützlich wäre: foreach geht sehr leicht von der Hand und kann komplexe Schleifenkonstrukte sehr schön darstellen. Wenn allerdings Performance das ausschlaggebende Kriterium ist, lässt sich mit for ein Quentchen mehr Leistung aus dem Rechner pressen.

Aber es geht noch besser: selbst die soeben hochgelobte for-Schleife lässt sich noch ein wenig "tieferlegen". Sehen Sie sich die folgende Schleife an:

int len = int_list.Count; for (int i = 0; i < len; i++) { DoSomething(li[i]); }

Diese for-Schleife spart gegenüber der Version weiter oben wieder ein paar Taktzyklen ein. Wieso? Properties scheinen auf den ersten Blick einfach nur eine Variable auszulesen. In Wahrheit versteckt sich hinter ihnen allerdings ein Methodenaufruf. Also ein Sprung, ablegen einer Rücksprungadresse auf den Stack, etc, alles nicht so schlimm, aber bei seeehr vielen Schleifendurchläufen irgendwann spürbar. Deshalb sparen wir uns die ständige Springerei und speichern die (sich nicht verändernde) Anzahl der zu iterierenden Listenelemente in einer Variablen. Noch schneller geht's wohl nur in Assembler...


Mittwoch, 21. Februar 2007 #

Generics sind eine sehr hübsche und unglaublich nützliche Angelegenheit. Allerdings gibt es hierbei auch manche Dinge, die nicht wirklich Intuitiv gelöst sind. Ein kleines Beispiel:

Sie haben eine Klasse A, von der zwei Klassen B und C erben. Da Sie sowohl viele B als auch viele C Objekte benötigen, benutzen Sie 2 Listen:

List<B> list_b = new List<B>(); List<C> list_c = new List<C>();

Angenommen, Sie möchten nun eine Methode schreiben, die auf Mengen von A Objekten operiert. Was liegt näher als eine Funktion wie folgt zu deklarieren:

public void MyMethod(List<A> list_a) { foreach (A a in list_a) { Do_Something_Important(a); } }

Nun sollten die beiden folgenden Aufrufe doch eigentlich ordnungsgemäß ihren Dienst verrichten:

MyMethod(list_b); MyMethod(list_c);

Leider bekommen Sie hier bereits beim Kompilieren eine Fehlermeldung. Die Typen List<A> und List<B> sind nicht kompatibel. Obwohl sich eine Vererbungshierarchie zwischen beiden Mengen logisch geradezu aufdrängt, gibt es keine. Die beiden Typen haben rein gar nichts gemeinsam. Sollte man deshalb also genötigt sein, Code doppelt zu schreiben? Je eine Methode MyMethod für List<B> und List<C>? Wenn Ihre Vererbungshierarchie etwas komplexer ist, wird das Problem gar noch schlimmer. Aber es gibt eine Lösung, auch ohne exzessives Quellcode-Copy-And-Paste: Bekämpfen Sie Generics mit Generics:

 

public static void MyMethod<type>(List<type> list_t) where type : A { foreach (A a in list_t) { Do_Something_Important(a); } }

Diese Methode ist nun selbst generisch. Sie übernimmt einen Typ-Parameter (type) und erwartet als Argument eine Liste mit Elementen vom Typ type. Allerdings schränken wir den Typparameter insoweit ein, dass er von der Klasse A erben muss. Somit können wir in der Funktion ohne hässliche Casts oder Ähnliches auf die Elemente der Liste zugreifen und unsere beiden Methodenaufrufe von oben (mit list_b und list_c als Parameter) werden erfolgreich durchgeführt. Der Trick hierbei ist, dass für jeden Parametertyp (List<B> und List<C>) vom Compiler eine eigene Version von MyMethod<type> angelegt wird. Man hat zwar weiterhin keine "echte" Vererbung zwischen den verschiedenen Listen, allerdings hat man zumindest die mehrfache Implementierung von MyMethod an den Compiler delegiert.


Mittwoch, 14. Februar 2007 #

Nach längerer Blogging-Abstinenz gibt es nun endlich mal wieder einen neuen Eintrag. Außerdem bin ich voller Hoffnung, dass ich nun wieder öfters Zeit zum Bloggen finde ;-)

Heute möchte ich das Konzept des letzten Eintrags ("Objekte rechtzeitig erzeugen") ein wenig verbessern. In diesem Eintrag ging es darum, dass viele Algorithmen ganz schnell eine große Menge von Objekten eines bestimmten Typs für ihre Berechnungen brauchen. Um so schneller diese erzeugt werden, um so schneller läuft der Algorithmus. Optimal wäre es also, wenn die Objekte bereits existieren. Genau das macht die in diesem letzten Blog-Eintrag erstellte Klasse - sie erzeugt Objekte zu einem Zeitpunkt, zu dem die dafür benötigte Rechenleistung nicht ins Gewicht fällt.

Diese Klasse hat allerdings einen Nachteil: Alle benötigten Objekte werden auf einmal erzeugt. Das heißt, dass zu irgendeinem Zeitpunkt richtig viel Rechenpower verbraucht wird. Besser wäre es, wenn die Objekte alle schön nacheinander über einen größeren Zeitraum verteilt erzeugt werden. Während des normalen Programmablaufs einer WinForms-Anwendung würde es ja nicht stören, wenn einfach ein paar mal pro Sekunde einige wenige Objekte angelegt werden. Wird dann nach 5 Minuten der Rechen- (und Speicher- ) intensive Algorithmus gestartet, sind dann auch so die nötigen Objekte bereits vorhanden.

Die unten gezeigte ObjectCreator-Klasse implementiert solch ein Verhalten. Sie bedient sich hierzu der System.Windows.Timer-Klasse, welche in einem festen Intervall eine Methode von ObjectCreator aufruft. Zu diesen Zeitpunkten werden dann einige der verwalteten Objekte erstellt:

 

class ObjectCreator<TargetType> where TargetType : class, new() { // in diesem Stack werden die Objekte gespeichert protected Stack<TargetType> mObjects = new Stack<TargetType>(); // der Timer, um regelmäßig Objekte zu erzeugen protected Timer mTimer = null; // wie viele Objekte sollen in jedem Intervall erzeugt werden? protected int mNumObjPerTick = 1; // wie viele Objekte sollen insgesamt erzeugt werden? protected int mMaxObjs = 1000; // Ermöglicht dem User, ein Maximum an verwalteten Objekten zu setzen / lesen public int MaxObjects { get { return this.mMaxObjs; } set { if (value > 0) this.mMaxObjs = value; } } // Anzahl aktuell verfügbarer Objekte public int CountCurrentObjects { get { return this.mObjects.Count; } } // Konstruktor, initialisiert den Timer public ObjectCreator() { mTimer = new Timer(); mTimer.Enabled = false; mTimer.Tick += new EventHandler(mTimer_Tick); } // ein Objekt wird erstellt und auf dem Stack abgelegt public void CreateObject() { TargetType tt = new TargetType(); mObjects.Push(tt); } // gibt ein Objekt zurück und entfernt es aus dem Stack public TargetType GetObject() { if (mObjects.Count > 0) return mObjects.Pop(); else return new TargetType(); } // aktiviert das automatischen Erzeugen von // NumObjObjekten alle MilliSecs Millisekunden public void EnableAutoCreation(int MilliSecs, int NumObj) { if (MilliSecs > 0 && NumObj > 0 && this.mObjects.Count < this.mMaxObjs) { this.mTimer.Interval = MilliSecs; this.mNumObjPerTick = NumObj; this.mTimer.Enabled = true; } } // deaktiviert die automatische Erzeugung wieder public void DisableAutoCreation() { this.mTimer.Enabled = false; } // wird vom Timer alle MilliSecs Millisekunden aufgerufen void mTimer_Tick(object sender, EventArgs e) { for (int i = 0; i < this.mNumObjPerTick; i++) { this.CreateObject(); } } }

Diese Implementierung verwendet einen Stack, um die erzeugten Objekte zwischenzuspeichern. Denkbar sind auch Ansätze mit anderen Container-Klassen, gerade auch ein Array kann sehr sinnvoll sein, da man sich bei geschickter Implementierung die Abfrage von "Count" in GetObject() sparen kann.

Die Klasse ist außerdem generisch, kann also zum frühzeitigen erzeugen aller Datentypen verwendet werden, die selbst ein Referenztyp sind und über einen Konstruktor verfügen. Ausgeschlossen sind hiervon die integralen Datentypen wie beispielsweise int, float, etc. Hierfür müßte man die Implementierung minimal abändern.

Grundsätzlich sollte man solche Klassen allerdings nur dann verwenden, wenn man wirklich viele Objekte verwendet, die auch noch eine gewisse Größe aufweisen. Bitmaps, Texturen, große Strings aber auch Klassen, die viele Membervariablen haben wären sinnvolle Beispiele. Bei der Anwendung sollte man auf jeden Fall testen, ob sich ein Geschwindigkeitsvorteil ergibt. Ist das Problem zu "gering", kann der Versuch durchaus nach hinten gehen, da der Verwaltungsaufwand nicht unerheblich ist. Bei großen Datenmengen kann man allerdings eine Geschwindigkeitssteigerung von Faktor 10 oder mehr verbuchen.


Freitag, 08. September 2006 #

Oftmals kommt man als Entwickler in die Verlegenheit, dass große Mengen von Objekten in kurzer Zeit erstellt werden müssen. Ein gutes Beispiel hierfür sind Algorithmen, welche während ihrer Laufzeit viele Objekte benötigen um Zustände und Zwischenergebnisse zu speichern.

Üblicherweise werden diese Objekte genau zu dem Zeitpunkt erstellt, zu dem sie auch benötigt werden, also mitten im Algorithmus. Das Erzeugen dieser Objekte kann allerdings recht teuer sein, wenn man bedenkt, dass die .NET Runtime hierzu erst passenden Speicher finden und reservieren muss.
Die Anzahl der verwendeten Objekte steht meist in Relation zu der Problemgröße, beispielsweise der Anzahl an untersuchten Dateien, der Größe eines Eingabearrays oder Ähnlichem. Hierdurch hat man meist schon vor dem Start des Algorithmus eine Ahnung, wie viele Objekte wohl benötigt werden. Dieses Wissen möchten wir uns hier nun zu Nutze machen.

Weiß man nämlich bereits vorab, wie viele Objekte benötigt werden, kann man diese bereits vor dem Ablauf des Algorithmus anlegen und die eigentliche Rechenzeit des Algorithmus somit verkürzen. Nehmen wir einmal an, Ihr Algorithmus würde während seiner Laufzeit viele Objekte des folgenden Typs benötigen:

class MemoryChunk { public int[] iData = new int[100]; }

Diese Klasse ist zwar recht unkompliziert, verlangt der .NET-Runtime jedoch einiges ab. Für eine Anwendung wäre es ein deutlicher Vorteil, wenn das erstellen dieser „Speicherklumpen“ bereits vorbereitet werden könnte.

Zu diesem Zweck erstellen wir eine weitere Klasse, den ObjectCreator:

class ObjectCreator<TargetType> where TargetType : class , new() { private TargetType[] mObjects; int mIndex = 0; public ObjectCreator() { } public void CreateObjects(int iCount) { if (iCount > 0) { mIndex = iCount - 1; mObjects = new TargetType[iCount]; for (int i = 0; i < iCount; i++) { TargetType obj = new TargetType(); mObjects[i] = obj; } } } public TargetType NewObject() { if (mIndex > 0) { TargetType obj = this.mObjects[mIndex]; mIndex--; return obj; } else return null; // oder: else return new TargetType(); } }

Wie Sie sehen handelt es sich hierbei um eine generische Klasse. TargetType gibt hierbei den Typ der zu erstellenden Objekte an, beispielsweise MemoryChunk. Durch Aufruf der Methode CreateObjects(AnzahlObj) können Sie ObjectCreator dazu veranlassen, AnzahlObj Objekte anzulegen. Durch Aufruf der Methode NewObject wird nun nach und nach je eines dieser Objekte zurückgegeben.

Die Anwendung der Klasse liegt nun auf der Hand: Wissen Sie beispielsweise, dass Ihr (gleich startender) Algorithmus 800 MemoryChunk-Objekte benötigt, erstellen Sie eine Instanz der Klasse ObjectCreator und rufen CreateObjects auf:

ObjectCreator<MemoryChunk> Creator = new ObjectCreator<MemoryChunk>(); Creator.CreateObjects(800);

Alle Codezeilen, in denen Sie innerhalb des Algorithmus ein MemoryChunk-Objekt erstellen, z.B. durch:

MemoryChunk mem = new MemoryChunk();

können Sie nun durch den folgenden Aufruf ersetzen:

MemoryChunk mem = Creator.NewObject();

Da die hier zurückgegebenen Objekte bereits existieren, werden Sie durch eine deutlich beschleunigte Laufzeit belohnt.

Nun kann man richtigerweise einwenden, dass die Objekte so oder so erstellt werden müssen, in der Summe sollte der Aufwand daher gleich bleiben. Dies ist so sicher richtig, allerdings gibt es viele Situationen, in denen man die „gesammelte“ Objekterstellung in Zeiten des Leerlaufs unterbringen kann. Nehmen Sie beispielsweise an, der Anwender fügt Einträge in einer Liste ein, welche anschließend von einem Algorithmus verarbeitet werden sollen. Während der Anwender den Mauszeiger zum Menüpunkt des Algorithmus bewegt, besteht genug Zeit, die nötigen Objekte vorab zu erstellen und die Wartezeit aufgrund der Abarbeitung für den Anwender zu minimieren.


Seit Version 6.0 hat sich Visual Basic deutlich weiterentwickelt. Hierbei fällt einem vor allem die nun (endlich) richtige Objektorientierung auf.

Inzwischen dürften wohl die meisten Visual Basic (2005) Programmierer mit den grundlegenden Mechanismen der Objektorientierung - Kapselung, Vererbung und Polymorphie - vertraut sein. Aber es gibt noch einige wenig bekannte Mechanismen, welche in Ausnahmesituationen recht hilfreich sein können.

Ein solcher Mechanismus ist "Shadowing". Stellen Sie sich vor, Sie haben eine Klasse A, welche eine Methode "Method()" enthällt. Der Aufruf von Method() erzeugt folgende Ausgabe:

"Eine Nachricht von Klasse A!"

Der passende Code würde so aussehen:

Class A Public Overridable Sub MyMethod() Console.WriteLine("Eine Nachricht von Klasse A!") End Sub End Class

Nun schreiben Sie eine weitere Klasse namens B, welche von A erbt. Sie überschreiben die Methode "Method()", so dass diese nun folgende Ausgabe vornimmt:

"Eine Nachricht von Klasse B!"

Diese würde wie folgt aussehen:

Class B : Inherits A Public Overrides Sub MyMethod() Console.WriteLine("Eine Nachricht von Klasse B!") End Sub End Class

Nun könten Sie folgendes tun:

Dim a As New A a.Method() Dim b As New B b.Method() a = b a.Method()

Die Ausgabe wäre dank Polymorphie folgende:

"Eine Nachricht von Klasse A!"
"Eine Nachricht von Klasse B!"
"Eine Nachricht von Klasse B!"

Was aber, wenn Sie möchten, dass sich ein Objekt sich wie der Typ verhällt, den die darauf zeigende Referenz hat? Hierzu können Sie sich des "Shadows" Schlüsselwortes bedienen. Schreiben Sie hierzu die Klasse B wie folgt:

Class B : Inherits A Public Shadows Sub MyMethod() Console.WriteLine("Eine Nachricht von Klasse B!") End Sub End Class

Die Ausgabe des Testcodes sieht nun wie folgt aus:

"Eine Nachricht von Klasse A!"
"Eine Nachricht von Klasse B!"
"Eine Nachricht von Klasse A!"

Shadows bewirkt also, das bei Methodenaufrufen durch eine "A"-Referenz, die Methode der A-Klasse aufgerufen wird, während bei einer "B"-Referenz die Methode von B genommen wird. Man könnte Shadows sozusagen auch als "Antipolymorphie" bezeichnen.

Nun, wann braucht man Shadows? Eigentlich sollte man es gar nicht benötigen, wenn Sie Shadows ernsthaft nutzen deutet dies meist auf einen Design-Fehler hin.

Allerdings gibt es einige wenige Situationen, in denen es sich durchaus als nützlich erweist. Stellen Sie sich vor, Sie entwickeln ein Control, welches andere Entwickler via Visual Studio Designer auf ein Form ziehen können. Anschließend kann der Entwickler via Properties Window die Eigenschaften des Controls bearbeiten.
Nun kann es passieren, dass dieses Control im Properties Window Eigenschaften anbietet, welche es von einer Basisklasse geerbt hat. Sie möchten diese dem Entwickler in Ihrem Control aber nicht anbieten. Durch Shadows können Sie solche Eigenschaften "verstecken", indem Sie beispielsweise das [Browsable(false)] Attribut anhängen.

An diesem Beispiel erkennt man aber schon, dass Shadows eher in Randsituationen benötigt wird und nicht in einem ordentlichen, objektorientierten Entwurf. Hier sollten Sie wirklich Abstand von der "dunklen Seite der Polymorphie" halten...