Wie in meinem letzten Blog-Post geschrieben, arbeite ich derzeit nach langer Abstinenz wieder an einigen VB.NET Projekten. Dabei stieß ich auch auf den “With-Block”, einen alten Bekannten aus VB 6 Zeiten, von dem in diesen Projekten intensiver Gebrauch gemacht wurde. Ein solches Konstrukt erlaubt es innerhalb eines Blocks ein Objekt anzugeben, auf das alle Statements ausgeführt werden, die nicht näher qualifiziert werden. So kann man einiges an Tipp-Arbeit sparen. Folgendes Beispiel soll dies verdeutlichen:
Sub Main()
Dim p As Person = New Person
With p
.Name = "Mueller"
.Age = 29
Console.WriteLine("Name:{0}, Alter:{1}", .Name, .Age)
EndWith
Console.Read()
EndSub
So gerne ich das With
… End With
auch unter VB 6 genutzt hatte, so zuwider war mir nach nun mehr als sechs Jahren C# die Nutzung unter VB.NET. Ich hatte das Gefühl, dass mein Code durch den Einsatz des “With-Blocks” Struktur und Lesbarkeit verlor und eine Hintertür für unnötige Fehlerquellen geöffnet wird.
Bestätigt wurde mein Gefühl als ich Zeilen in folgender Art las:
Sub Main()
Dim p As Person = New Person
With p
.Name = "Mueller"
.Age = 29
p = New Person
p.Age = 21
p.Name = "Meier"
Console.WriteLine("Name:{0}, Alter:{1}", .Name, .Age) ' Wird hier nun Mueller oder Meier ausgegeben?
Console.WriteLine("Name:{0}, Alter:{1}", p.Name, p.Age) ' Gleicht die Ausgabe dieser Zeile der vorherigen?
End With
Console.Read()
End Sub
An dieser Stelle war sich das gesamte Team unsicher, welchen Effekt die Zeile p = new Person innerhalb des Blocks haben wird. Es herrschte rege Diskussion, ob nur die Anweisungen, die über “p.” qualifiziert werden das neue Objekt verändern, oder ob auch die Anweisungen die nur über den “.” qualifiziert werden das neue Objekt verändern.
Um die Antwort vorweg zu nehmen: Wir haben nun zwei Objekte von der Klasse Person im Speicher, die beide referenziert werden. Das ursprüngliche Objekt wird durch “.” angesprochen, dass neu erstelle Objekt durch “p.”.
Sehr schön sieht man das ganze auch wenn man sich den zugehörigen IL-Code ansieht:
.method publicstatic void Main() cil managed
{
.custom instance void [mscorlib]System.STAThreadAttribute::.ctor() .entrypoint .maxstack 3 .locals init ( [0] class WithTest.Person p, [1] class WithTest.Person VB$t_ref$L0) L_0000: nop L_0001: newobj instance void WithTest.Person::.ctor() L_0006: stloc.0 L_0007: ldloc.0 L_0008: stloc.1 L_0009: ldloc.1 L_000a: ldstr "Mueller" L_000f: callvirt instance void WithTest.Person::set_Name(object) L_0014: nop L_0015: ldloc.1 L_0016: ldc.i4.s 0x1d L_0018: box int32 L_001d: callvirt instance void WithTest.Person::set_Age(object) L_0022: nop L_0023: newobj instance void WithTest.Person::.ctor() L_0028: stloc.0 L_0029: ldloc.0 L_002a: ldstr "Meier" L_002f: callvirt instance void WithTest.Person::set_Name(object) L_0034: nop L_0035: ldloc.0 L_0036: ldc.i4.s 0x15 L_0038: box int32 L_003d: callvirt instance void WithTest.Person::set_Age(object) L_0042: nop L_0043: ldstr "Name:{0}, Alter:{1}" L_0048: ldloc.1 L_0049: callvirt instance object WithTest.Person::get_Name() L_004e: callobject [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::GetObjectValue(object) L_0053: ldloc.1 L_0054: callvirt instance object WithTest.Person::get_Age() L_0059: callobject [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::GetObjectValue(object) L_005e: call void [mscorlib]System.Console::WriteLine(string, object, object) L_0063: nop L_0064: ldstr "Name:{0}, Alter:{1}" L_0069: ldloc.0 L_006a: callvirt instance object WithTest.Person::get_Name() L_006f: callobject [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::GetObjectValue(object) L_0074: ldloc.0 L_0075: callvirt instance object WithTest.Person::get_Age() L_007a: callobject [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::GetObjectValue(object) L_007f: call void [mscorlib]System.Console::WriteLine(string, object, object) L_0084: nop L_0085: ldnull L_0086: stloc.1 L_0087: call int32 [mscorlib]System.Console::Read() L_008c: pop L_008d: nop L_008e: ret }
Wie man in den Zeilen 6 - 8 sieht werden zunächst zwei Variablen vom Typ Person auf dem Stack abgelegt. Die in unserem VB.NET SourceCode deklarierte Variable p wird an der Index Position 0 des Stacks abgelegt (Zeile 7) und zusätzlich wird eine Variable VB$t_ref$L0 an der Position 1 abgelegt. Letzte Variable wird später zur Umsetzung unseres “With-Blocks” genutzt. In der Zeile 10 wird nun ein neues Objekt der Klasse Person erstellt und auf dem Evaluationsstack abgelegt.
In der Zeile 11 wird dieses Objekt der lokalen Variablen am Index 0, also unserer Variablen p, zugewiesen. In der Zeile 12 wird der Inhalt der Variablen am Index 0, also unser Personen Objekt, wieder auf den Evaluationsstack geladen, um in Zeile 13 schließlich der Variablen am Index 1 (VB$t_ref$L0) zugewiesen zu werden.
In den Zeilen 15 - 22 werden anschließend die Werte für den Namen und das Alter auf dem Stack abgelegt und die entsprechenden Setter-Methoden des durch die Variable am Index 1 (VB$t_ref$L0) referenzierten Objektes aufgerufen.
Bis hier hin ist die Welt noch in Ordnung. Wir haben zwei Variablen vom Typ Person, die beide das selbe Objekt referenzieren. Die Freude währt jedoch nur bis zur Zeile 23. In dieser Zeile wird nämlich ein neues Objekt vom Typ Person erstellt und in den folgenden Zeilen der lokalen Variablen am Index 0 (p) zugewiesen. Ab diesem Zeitpunkt greifen die Anweisungen, die über “.” und die, die über “p.” qualifizieren auf verschiedene Objekte zu!
Welche Fehler sich in solch einem Szenario bei kleinen Unachtsamkeiten einschleichen können, liegt auf der Hand.
Daher lautet mein persönliches Fazit:
Die Zeit, die ich durchs Tippen beim Einsatz des “With-Blocks” spare, büße ich während der Fehlersuche unter Umständen mehrfach wieder ein. Daher lautet meine Devise für die Zukunft: Finger weg von “with… end with”.
Kommentare willkommen :-)
Es gibt 2 Kommentare