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)

    End With

 

    Console.Read()

  End Sub

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:

    1 .method public static void Main() cil managed

    2 {

    3     .custom instance void [mscorlib]System.STAThreadAttribute::.ctor()

    4     .entrypoint

    5     .maxstack 3

    6     .locals init (

    7         [0] class WithTest.Person p,

    8         [1] class WithTest.Person VB$t_ref$L0)

    9     L_0000: nop

   10     L_0001: newobj instance void WithTest.Person::.ctor()

   11     L_0006: stloc.0

   12     L_0007: ldloc.0

   13     L_0008: stloc.1

   14     L_0009: ldloc.1

   15     L_000a: ldstr "Mueller"

   16     L_000f: callvirt instance void WithTest.Person::set_Name(object)

   17     L_0014: nop

   18     L_0015: ldloc.1

   19     L_0016: ldc.i4.s 0x1d

   20     L_0018: box int32

   21     L_001d: callvirt instance void WithTest.Person::set_Age(object)

   22     L_0022: nop

   23     L_0023: newobj instance void WithTest.Person::.ctor()

   24     L_0028: stloc.0

   25     L_0029: ldloc.0

   26     L_002a: ldstr "Meier"

   27     L_002f: callvirt instance void WithTest.Person::set_Name(object)

   28     L_0034: nop

   29     L_0035: ldloc.0

   30     L_0036: ldc.i4.s 0x15

   31     L_0038: box int32

   32     L_003d: callvirt instance void WithTest.Person::set_Age(object)

   33     L_0042: nop

   34     L_0043: ldstr "Name:{0}, Alter:{1}"

   35     L_0048: ldloc.1

   36     L_0049: callvirt instance object WithTest.Person::get_Name()

   37     L_004e: call object [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::GetObjectValue(object)

   38     L_0053: ldloc.1

   39     L_0054: callvirt instance object WithTest.Person::get_Age()

   40     L_0059: call object [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::GetObjectValue(object)

   41     L_005e: call void [mscorlib]System.Console::WriteLine(string, object, object)

   42     L_0063: nop

   43     L_0064: ldstr "Name:{0}, Alter:{1}"

   44     L_0069: ldloc.0

   45     L_006a: callvirt instance object WithTest.Person::get_Name()

   46     L_006f: call object [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::GetObjectValue(object)

   47     L_0074: ldloc.0

   48     L_0075: callvirt instance object WithTest.Person::get_Age()

   49     L_007a: call object [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::GetObjectValue(object)

   50     L_007f: call void [mscorlib]System.Console::WriteLine(string, object, object)

   51     L_0084: nop

   52     L_0085: ldnull

   53     L_0086: stloc.1

   54     L_0087: call int32 [mscorlib]System.Console::Read()

   55     L_008c: pop

   56     L_008d: nop

   57     L_008e: ret

   58 }

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 :-)


Kick it on dotnet-kicks.de
 
7/17/2008 - 07:35 AM | Comments [2] | Categories: VB.NET
© Andre Kraemer | RSS/Subscribe Feed your aggregator (RSS 2.0)

Thursday, July 17, 2008 10:17:51 AM (Mitteleuropäische Sommerzeit, UTC+02:00)
Sorry, aber wer so den with- Block verwendet, der gehört wirklich mit Fehlern bestraft ;). Weder in VB 6 noch in .NET sollte man jeweils innerhalb des with-Blocks, außer der Zuweisung von Werten des angegeben Objektes im with Block, Code- Zeilen stellen.
Tom Inm
Saturday, July 19, 2008 8:43:35 PM (Mitteleuropäische Sommerzeit, UTC+02:00)
Da gebe ich dir uneingeschränkt recht. Wenn man den With-Block nutzt, dann nur in der von dir beschriebenen Art :-)
Comments are closed.