Veröffentlicht: 13. Jan 2003 | Aktualisiert: 09. Nov 2004
Von Peter Monadjemi und Ralf Westphal
Klassen und Objekte sind bei Visual Basic .NET keine Option mehr sondern ein fester Teil der Sprache. Da das.NET Framework vollständig objektorientiert ist, müssen sich Visual-Baisc-.NET-Programmierer, die das .NET Framework nicht nur benutzen, sondern auch verstehen wollen, ausgiebig mit der Klassenprogrammierung beschäftigen. In unserem Chat nahmen die Teilnehmer die Möglichkeit zum Fragenstellen rege in Anspruch.
Auf dieser Seite
Interne Prüfung auf doppelte Strings
String-Performance
Zeichenketten umkehren
Timer()-Funktion fehlt
Windows Forms Schaltflächen kurzzeitig hervorheben
Unterschied zwischen WebServices und .NET Remoting
Problem mit FileGet()-Parameter
Datentransport in verteilten Anwendungen
Interne Prüfung auf doppelte Strings
Frage: In VB .NET wird bei jedem neuen String geprüft ob er schon mal vorhanden ist. Wenn ja, wird auf die gleiche Referenz (intern) verwiesen. Wie effektiv ist dieses Suchen bei vielen Strings (>10000)?
Antwort: Das ist so nicht ganz richtig, wenn damit gemeint ist, dass eine solche Prüfung für jeden zur Laufzeit erzeugten String durchgeführt wird.
Die Frage spielt aber auf einen tatsächlich existierenden Mechanismus an, das sog. Interning von Strings. Das findet jedoch auf IL-Ebene statt und hat nichts mit VB.NET oder C# zu tun.
Während der JIT-Übersetzungszeit (!), also nur einmal pro Methode zur Laufzeit, prüft die CLR für jeden festen String im IL-Code (String-Literal), ob er schon in einer internen, prozessweiten Hash-Tabelle registriert ist. Ist das nicht der Fall, so hinterlegt sie ihn in dieser Tabelle. Bei zukünftigen IL-Übersetzungen im selben Prozess ist er dann dort vorhanden.
Sobald ein String-Literal in der internen Tabelle steht, trägt die CLR im erzeugten Maschinencode dessen Adresse im Speicher der Hash-Tabelle ein - z.B. um sie als Zeiger einer String-Variablen zuzuweisen.
Strings werden also an zwei Orten gespeichert: Literale, d.h. Zeichenketten, die zur Übersetzungszeit bekannt sind, stehen in der internen Hash-Tabelle. Alle anderen Zeichenketten werden auf dem Heap wie jedes andere Objekt allokiert.
Der Gewinn liegt darin, dass mehrfach im IL-Code (d.h. also auch im Quelltext) vorkommende Zeichenketten Assembly-übergreifend pro Prozess nur einmal im Hauptspeicher gehalten werden müssen. Das spart Platz und Zeit (weil diese Strings nicht der Garbage Collection unterliegen).
Normalerweise merken Sie von dem ganzen Vorgang nichts. Ein kleines Experiment macht das Interning jedoch sichtbar:
Dim s1 As String = "hello"
Dim s2 As String = "hello"
Console.WriteLine(Object.ReferenceEquals(s1, s2))
Dieser Code gibt True aus, denn obwohl im Quelltext zwei (allerdings gleichlautende) Strings zugewiesen werden, enthalten beide Objektvariablen dieselbe Referenz auf den String "hello" in der internen Hash-Tabelle.
Dass diese Gleichheit nichts mit dem Inhalt eines String zu tun hat, sondern damit, dass Interning zur JIT-Zeit und nicht zur Laufzeit stattfindet, wird deutlich, wenn Sie anschließend einer weiteren Variablen denselben String zuweisen:
Dim s3 As String
s3 = "hel"
s3 &= "lo"
Console.WriteLine(Object.ReferenceEquals(s1, s3))
In s3 steht ebenfalls ("hello"), allerdings als Ergebnis einer Stringverknüpfung. Der Vergleich s1=s3 würde also True liefern. Der Vergleich der Referenzen jedoch liefert nun False! Das Verknüpfungsresultat wurde nicht in der Interning-Tabelle gesucht, sondern sofort auf dem Heap abgelegt.
Der Unterschied zwischen zur Übersetzungszeit bekannten und zur Laufzeit erzeugten Zeichenketten wird deutlich, wenn Sie sich den IL-Code zum obigen Beispiel und den am Ende daraus erzeugten Maschinencode anschauen:
//000004: Dim s1 As String = "hello"
IL_0001: ldstr "hello"
IL_0006: stloc.0
.line 5:13
//000005: Dim s2 As String = "hello"
IL_0007: ldstr "hello"
IL_000c: stloc.1
.line 6:9
//000006: Console.WriteLine(Object.ReferenceEquals(s1, s2))
IL_000d: ldloc.0
IL_000e: ldloc.1
IL_000f: call bool
[mscorlib]System.Object::ReferenceEquals(object,
object)
IL_0014: call void
[mscorlib]System.Console::WriteLine(bool)
IL_0019: nop
.line 9:9
//000007:
//000008: Dim s3 As String
//000009: s3 = "hel"
IL_001a: ldstr "hel"
IL_001f: stloc.2
.line 10:9
//000010: s3 &= "lo"
IL_0020: ldloc.2
IL_0021: ldstr "lo"
IL_0026: call string
[mscorlib]System.String::Concat(string,
string)
IL_002b: stloc.2
.line 11:9
//000011: Console.WriteLine(Object.ReferenceEquals(s1, s3))
IL_002c: ldloc.0
IL_002d: ldloc.2
IL_002e: call bool
[mscorlib]System.Object::ReferenceEquals(object,
object)
IL_0033: call void
[mscorlib]System.Console::WriteLine(bool)
Im IL-Code ist noch keine "Gemeinsamkeit" der Zeichenketten zu erkennen. "hello" wird zweimal gelistet (Zeilen IL_0001 und IL_0007). (In der Assembly allerdings sind String-Literale auch schon an einem Ort zusammengefasst.)
Das Ergebnis der Verknüpfung von "hel" und "lo" allerdings ist kein Literal, sondern ein erst zur Laufzeit erzeugter Zeiger auf einen neuen String auf dem Heap, der in einer eigenen Variablen gespeichert wird (Zeile IL_002b).
Im Maschinencode hingegen stehen bei den Zuweisungen der String-Literale absolute Adressen:
Dim s1 As String = "hello"
00000015 mov eax,dword ptr ds:[01B207A0h]
0000001b mov edi,eax
Dim s2 As String = "hello"
0000001d mov eax,dword ptr ds:[01B207A0h]
00000023 mov dword ptr [ebp-8],eax
Console.WriteLine(Object.ReferenceEquals(s1, s2))
00000026 mov edx,dword ptr [ebp-8]
00000029 mov ecx,edi
0000002b call dword ptr ds:[00812188h]
00000031 movzx ebx,al
00000034 mov ecx,ebx
00000036 call dword ptr ds:[02E84054h]
0000003c nop
s3 = "hel"
0000003d mov eax,dword ptr ds:[01B207A4h]
00000043 mov esi,eax
s3 &= "lo"
00000045 mov edx,dword ptr ds:[01B207A8h]
0000004b mov ecx,esi
0000004d call FFE9BFD8
00000052 mov ebx,eax
00000054 mov esi,ebx
Console.WriteLine(Object.ReferenceEquals(s1, s3))
00000056 mov edx,esi
00000058 mov ecx,edi
Console.WriteLine(Object.ReferenceEquals(s1, s3))
0000005a call dword ptr ds:[00812188h]
00000060 movzx ebx,al
00000063 mov ecx,ebx
00000065 call dword ptr ds:[02E84054h]
Beide Zuweisungen von "hello" (Zeilen 15 und 1d) zeigen auf denselben Speicherbereich (10B207A0h): den Ort, wo der String "hello" in der internen Hash-Tabelle abgelegt ist.
Das Ergebnis der Verknüpfung von "hel" und "lo" wird jedoch weiterhin erst zur Laufzeit ermittelt (Zeile 4d).
Unter dem Strich bedeutet das: Interning macht die Übersetzung von IL-Code vielleicht ein klitzekleinwenig langsamer, weil String-Literale nicht einfach auf dem Heap abgelegt (alloziert) werden, sondern der JIT-Compiler sie zuerst in der Interning-Tabelle zu finden versucht. Verglichen mit der Gesamtdauer der JIT-Übersetzung, die ja auch nur einmal pro Methode stattfindet, ist dieser Aufwand jedoch vernachlässigbar winzig.
Für die Laufzeit einer Anwendung hingegen - und das war die Ausgangsfrage - bedeutet Interning einen deutlichen Performancevorteil (!) (vom geringeren Speicherverbrauch ganz zu schweigen), denn String-Literale müssen nicht mehr als String-Objekte erzeugt werden. Denn die sind durch das Interning schon vorhanden.
Ressourcen zur Frage:
F. Liger et al., Visual Basic .NET Text Manipulation Handbook, Wrox,
http://www.vbip.com/books/1861007302/chapter_7302_01.asp
J. Richter, String Interning, Wintellect,
http://www.wintellect.com/resources/newsletters/Jan2002.aspx
String-Performance
Frage: Ich bin momemtan mit der Performance von Stringoperationen (bis auf wenige Ausnahmen) in VB6 zufrieden, möchte aber bei einem Umstieg nach VB.NET keine bösen Überraschungen erleben. Wie sieht es mit der Performance von String-Operationen in VB.NET aus?
Antwort: Vom theoretischen wie vom praktischen Standpunkt aus lässt sich diese Frage recht leicht beantworten: Der Umgang mit Zeichenketten ist in VB.NET deutlich schneller als in VB6. Das hat drei Gründe:
-
Interning: Wie oben beschrieben werden String-Literale schon zur Übersetzungszeit im Speicher abgelegt. Sie müssen also weder auf den Stack (VB6) noch auf den Heap (VB.NET) gelegt werden, wenn Sie zur Laufzeit darauf zugreifen.
-
Speicherverwaltung: Die Speicherverwaltung der CLR funktioniert anders als die von VB6 oder COM. Freier Speicher für Objekte (bzw. variabel lange Zeichenketten) wird nicht in einer Liste gesucht, sondern blitzschnell einfach vom freien Heap-Speicher genommen.
VB6/COM gehen anders vor, um zu vermeiden, dass im Heap viele "Löcher" von ungenutztem (weil nicht mehr benötigtem) Speicher entstehen. Sie bemühen sich also, zuerst entstandene Löcher mit neuen Speicheranforderungen zu füllen. Das kostet Zeit.
VB.NET hingegen - d.h. der CLR und insofern gilt das für alle .NET-Sprachen - ist eine "Durchlöcherung" des Heap egal. Hier wird auf Performance bei der Objekterzeugung gesetzt; die entstehenden Löcher bereinigt im Hintergrund die Garbage Collection.
-
Stringbuilder: Die .NET Framework Base Class Library enthält eine spezielle Klasse für sehr schnelle String-Operationen. Sie arbeitet intern auf einem Puffer und bemüht daher für Verkettungen, Einfügungen usw. nicht den Heap. Indirekter Speicherzugriff und Zeichenkettenallokationen entfallen daher.
Soweit zur Theorie. Ob und wie Sie sich auf die Praxis auswirken, mag ein kleines Beispiel verdeutlichen. Nehmen Sie folgendes, in Web-Anwendungen recht typsiches Szenario: Eine HTML-Tabelle wird zur Laufzeit zusammengesetzt.
Dim s As Date
s = Now
Dim htmlTable As String
htmlTable = "<table>"
Dim r%, c%
For r = 1 To 100
htmlTable = htmlTable & vbCrLf & " <tr>
For c = 1 To 40
htmlTable = htmlTable & vbCrLf & " <td>"
htmlTable = htmlTable & r & "/" & c & "-" & Now
htmlTable = htmlTable & "</td>"
Next
htmlTable = htmlTable & vbCrLf & " </tr>"
Next
htmlTable = htmlTable & vbCrLf & "</table>"
Dim l As Long
l = Len(htmlTable)
MsgBox "len=" & l & ", time: " & DateDiff("s", s, Now) & "sec"
Auf einem Testrechner braucht dieser Code mit VB6 übersetzt ca. 25 sec, um den String von mehr als 160KB zusammenzusetzen.
Wenn Sie den Code 1:1 nach VB.NET übernehmen sinkt die Laufzeit sofort auf ca. 14 sec! VB.NET ist also ohne weiteres Zutun "einfach so" schon fast doppelt so schnell wie VB6. Und 50% Performance-Steigerung sind doch ein schönes Argument für eine neue Programmiersprache, oder?
Noch besser wird das Ergebnis aber, wenn Sie den Code nicht 1:1 übernehmen, sondern die HTML-Tabelle in einem StringBuilder-Objekt zusammenbauen:
Dim s As Date
s = Now
Dim htmlTable As New Text.StringBuilder("<table>")
Dim r%, c%
For r = 1 To 100
htmlTable.Append(vbCrLf)
htmlTable.Append(" <tr>")
For c = 1 To 40
htmlTable.Append(vbCrLf)
htmlTable.Append(" <td>")
htmlTable.Append(r)
htmlTable.Append("/")
htmlTable.Append(c)
htmlTable.Append("-")
htmlTable.Append(Now)
htmlTable.Append("</td>")
Next
htmlTable.Append(vbCrLf)
htmlTable.Append(" </tr>")
Next
htmlTable.Append(vbCrLf)
htmlTable.Append("</table>")
Dim l As Long
l = htmlTable.Length
MsgBox("len=" & l & ", time: " & DateDiff("s", s, Now) & "sec")
String-Verkettungen mit dem &-Operator werden dabei durch Aufrufe der Append()-Methode ersetzt. Aus
htmlTable = htmlTable & vbCrLf & "<tr>"
wird
htmlTable.Append(vbCrLf)
htmlTable.Append("<tr>")
Wenn Sie diesen Code ausführen wird als Ergebnis 0 sec angezeigt! Er benötigt für dasselbe Ergebnis also weniger als 1 sec! Damit ist er mindestens mehr als 20mal schneller als der VB6-Code. (Andere Tests zeigen, dass StringBuilder eine bis zu 100mal höhere Performance im Vergleich zu VB6 bringt.)
Unter dem Strich bedeutet das: Wenn es um String-Operationen geht, erleben Sie mit VB.NET nur sehr positive Überraschungen.
Ressourcen zur Frage:
J. Richter, Microsoft .NET Framework Programmierung, ISBN 3860636502, MSPress 2002
http://www.fawcette.com/vsm/2002_05/magazine/columns/gettingstarted/default_pf.asp
http://www.vb2themax.com/Item.asp?PageID=TipBank&Cat=121&ID=463
Zeichenketten umkehren
Frage: Wie kann ich aus "gninneh" wieder meinen Namen "Henning" machen, die Zeichenkette also umkehren?
Antwort: Eine Funktion zum Umkehren von Zeichenketten (z.B. Reverse()) auf der Klasse String gibt es leider nicht. Sie müssen sich also eine solche Funktion selbst schreiben, z.B. wie folgt:
Public Function Reverse(ByVal s As String) As String
Dim sDest As New Text.StringBuilder()
Dim i%
For i = s.Length - 1 To 0 Step -1
sDest.Append(s.Chars(i))
Next
Return sDest.ToString
End Function
Auf Stringverknüpfungen mit dem &-Operator verzichten Sie dabei am besten, weil sie vergleichsweise langsam sind (s.o.). Die Funktion nutzt daher ein StringBuilder-Objekt, um die umgekehrte Zeichenkette zusammenzusetzen. Für den lesenden Zugriff auf die Quellzeichen genügt jedoch die Chars-Eigenschaft von String.
Der vorstehende Code ist zwar einfach & geradlinig - aber trotz des StringBuilder-Objekts nicht sehr schnell und verbraucht recht viel Speicherplatz. Es lohnt sich also, nach einer alternativen Möglichkeit zu suchen.
Zwar gibt es keine Umkehrfunktion für Zeichenketten, aber eine für Felder: Array.Reverse()! Wenn Sie sich also die obige Umkehrschleife sparen wollen, können Sie das String-Reich verlassen und die Quellzeichenkette in ein Char-Feld wandeln, um die Feld-Umkehrfunktion zu nutzen:
Public Function Reverse(ByVal s As String) As String
Dim b() As Char = s.ToCharArray()
Array.Reverse(b)
Return New String(b)
End Function
Das Schöne daran ist nicht nur, dass Sie weniger Code schreiben müssen, sondern dass diese Funktion sowohl schnelle als die vorhergehende ist (ca. doppelte Geschwindigkeit) wie auch deutlich weniger Speicherplatz verbraucht, weil interne Speicherallokationen des StringBuilder-Objektes entfallen.
Mit weniger Codeaufwand können Sie eine Umkehrfunktion kaum mehr schreiben. Geht es aber nicht vielleicht noch schneller? Allerdings! Dafür müssen Sie jedoch die schöne Welt der sicheren Programmierung mit VB.NET verlassen, in der es keine Speicherfehler gibt. Sie müssen sich in die Welt des unsafe-Code mit C# begeben:
unsafe void Reverse(string s)
{
fixed(char* ps = s)
{
char* cStart = ps;
char* cEnd = cStart + s.Length - 1;
for (int i=0;
i < (s.Length / 2);
cStart++, cEnd--, i++)
{
char temp = *cStart;
*cStart = *cEnd;
*cEnd = temp;
}
}
}
Dort können Sie wie in C mit Zeigern arbeiten und das ermöglicht einen hochperformanten Algorithmus: Sie ermitteln einen Char-Zeiger auf den Anfang der umzukehrenden Zeichenkette (cStart), berechnen daraus einen Zeiger auf das Ende der Zeichenkette (cEnd) und vertauschen in einer Schleife die Zeichen an den Positionen, auf die diese Zeiger weisen (*cStart, *cEnd). Nach jedem Schleifendurchlauf rücken die Zeiger um ein Zeichen weiter auf einander zu, so dass sie bei der hälfte der Zeichenkette alle Zeichen vertauscht haben.
Im Vergleich zur ersten Umkehrfunktion ist diese Version fast 10 Mal schneller und verbraucht noch mal deutlich weniger Speicherplatz als die vorhergehende, weil hier überhaupt kein zusätzlicher Speicher belegt werden muss. Die Stringumkehrung findet im (!) Original statt.
Insgesamt ist es zwar ein wenig lästig (und vielleicht unverständlich), dass das .NET Framework keine Umkehrfunktion für Zeichenketten bietet, aber der Aufwand, sie zu programmieren ist sehr gering, wenn Sie einen String als einen Spezialfall eines Feldes betrachten. Und für mehr Performance können Sie auf eine eher low-level Programmierung mit C# ausweichen.
Timer()-Funktion fehlt
Frage: Wenn ich die Timer-Funktion benutzen will, bekomme ich folgende Meldung: "Funktion Timer wird nicht erkannt". Fehlt die VB6 Timer-Funktion in VB.NET?
Antwort: Die Timer-Funktion ist weiterhin vorhanden, muss aber über den Kompatibilitätsnamensraum Microsoft.VisualBasic angesprochen werden, z.B.
Console.WriteLine(Microsoft.VisualBasic.Timer())
Noch einfacher geht es, wenn Sie den Namespace am Anfang einer Quelldatei importieren. Dann stehen Ihnen auch die anderen darin definierten Kompatibilitätsfunktionen und -klassen zur Verfügung:
Imports Micorosft.VisualBasic
...
Console.WriteLine(Timer())
Windows Forms Schaltflächen kurzzeitig hervorheben
Frage: Wenn man die Farbe von Buttons ändern will und zwar kurzzeitig, damit der User sieht, dass der Button gedrückt war, wie macht man das?
Antwort: Wenn man eine Schaltfläche (oder ein anderes Steuerelement) vorübergehend hervorheben will, indem man eine Eigenschaft verändert (z.B. Hintergrundfarbe), dann gibt es dafür zwei Ansätze.
Zum einen können Sie Eigenschaften für die Dauer verändern, während der die Maus sich über dem Steuerelement befindet. Implementieren Sie einfach Ereignisbehandlungsroutinen für die MouseEnter- und MouseLeave-Events, z.B.:
Private _buttonDefaultBackColor As Color
Private Sub Button1_MouseEnter(ByVal sender As Object, _
ByVal e As System.EventArgs) _
Handles Button1.MouseEnter
_buttonDefaultBackColor = Button1.BackColor
Button1.BackColor = Color.Green
End Sub
Private Sub Button1_MouseLeave(ByVal sender As Object, _
ByVal e As System.EventArgs) _
Handles Button1.MouseLeave
Button1.BackColor = _buttonDefaultBackColor
End Sub
Damit ist eine Schaltfläche jedoch nicht nach einem Anklicken hervorgehoben, sondern nur beim Darüberbewegen der Maus. Für eine kurzzeitige (!) Markierung nach dem Anklicken ist ein Timer-Steuerelement oder ein Timer-Objekt ideal:
Imports System.Threading
.
Private Sub Button1_Click(.) Handles Button1.Click
_highlightTimer = New Timer(
New TimerCallback(AddressOf ResetButtonBackColor), _
New Object(){Button1, _
Button1.BackColor}, _
500, Timeout.Infinite)
Button1.BackColor = Color.Red
End Sub
Private Sub ResetButtonBackColor(ByVal state As Object)
Dim params() As Object = CType(state, Object())
CType(params(0), Button).BackColor = CType(params(1), Color)
End Sub
Einem Timer-Objekt übergeben Sie eine Zeit, wann es das erste Mal feuern soll (hier: 500 msec), eine Zeitspanne, wann es danach wieder rhythmisch feuern soll (hier: Timeout.Infinite, d.h. kein weiteres Feuern), einen Delegate für die Methode, die beim Feuern aufgerufen werden soll (hier: ResetButtonBackColor()) und ein Object als Parameter für diese Methode.
Der obige Code richtet also einen Timer ein, der nach einer halben Sekunde die Farbe des Buttons wieder zurücksetzt. Auf welche Farbe der Hintergrund welchen Buttons gesetzt werden soll, ist in ein ad hoc Object-Feld verpackt, das die Ereignisbehandlungsroutine ausliest.
Unterschied zwischen WebServices und .NET Remoting
Frage: Mich würde einmal interessieren, wo der Unterschied zwischen WebServices und Remoting liegt.
Antwort: WebServices - oder genauer: XML Web Services - sind ein Sonderfall des .NET Remoting. (Diese Aussage bezieht sich natürlich wie die Frage auf die .NET-Framework-Welt der Programmierung. WebServices als SOAP-basierte Dienstleistungen gibt es aber auch außerhalb davon auf anderen Plattformen.)
.NET Remoting ist die Infrastruktur zur Kommunikation in verteilten Anwendungen. Sie besteht aus mehreren Komponenten in verschiedenen Ausprägungen, u.a. sind das:
Host-Anwendung: Um entfernten Clients Funktionalität anbieten zu können, müssen die Typen (Klassen), deren Methoden aufgerufen werden sollen, von einer Host-Anwendung publiziert werden. Sie können schon mit einer Zeile Code jedes .NET-Programm zu einem solchen Host machen.
Kommunikationskanal: Die Kommunikation zwischen Clients und Host findet über Kanäle statt, über den die zu einem Methodenaufruf gehörenden Daten geschickt werden. Das .NET Framework bietet von sich aus einen TCP-Kanal und einen HTTP-Kanal.
Formatierer: Die Daten, die zwischen Clients und Host über den gewählten Kanal fließen, müssen serialisiert werden. In welchem Format das geschieht, legt die Wahl eines Formatierers fest. Das .NET Framework bietet von sich aus einen Binärformatierer und einen SOAP-Formatierer.
Von einem XML Web Service (auf dem .NET Framework) im engeren Sinn spricht man, wenn man meint, dass die Host-Anwendung die Microsoft Internet Information Server (IIS) sind und via HTTP-Kanal SOAP-formatierte Daten ausgetauscht werden. Im weiteren Sinn liegt ein WebService aber auch dann vor, wenn der Host nicht die IIS sind, sondern eine beliebige andere Anwendung mit eingestelltem HTTP-Kanal und SOAP-Formatierer arbeitet.
XML Web Services sind also sozusagen die schwergewichtigste Form des .NET Remoting, da sie neben dem .NET Framework die IIS als Infrastruktur voraussetzen und per default mit XML als Datenformat Verbindungen relativ stark belasten.
Auf der anderen Seite bieten XML Web Services gerade durch die IIS als Host Vorteile, die "normale" .NET Remoting Hosts fehlen:
Verschlüsseltete Datenübertragung: Die IIS sind SSL-fähig und XML Web Services können daher auch per HTTPS aufgerufen werden.
Autorisierung: Über verschiedene Mechanismen kann der Zugriff auf XML Web Services schon heute recht einfach reglementiert werden, wenn Client und Server auf dem .NET Framework laufen (Stichwort: SOAP Extensions). Die WS-Security Spezifikation tut ein Übriges und wird Standards hierfür definieren; der zugehörige WS SDK läuft jedoch nur mit den IIS.
Skalierbarkeit: Die IIS sind auf hohe Lasten ausgelegt, "normale" .NET Remoting Hosts hingegen sind zur Beantwortung von Anfragen auf den Threadpool ihres Prozesses beschränkt.
Die höchste Performance beim .NET Remoting bieteen übrigens die IIS als Host mit dem HTTP-Kanal und dem Binärformatierer.
Ressourcen zur Frage:
I. Rammer, Advanced .NET Remoting, ISBN 1590590252, APress 2002
C. Weyer, XML Web Service-Anwendungen mit Microsoft .NET, ISBN 3827318912, Addison-Wesley 2002
Problem mit FileGet()-Parameter
Frage: Ich habe eine Structure Temp definiert. Wenn Option Strict On geschaltet ist, kann ich folgendes nicht machen: FileGet(1, t) (mit t as Temp). Wie kommt das?
Antwort: Die kurze Antwort auf diese Frage lautet, dass VB.NET mit Option Strict On keinen automatischen Upcast mehr erlaubt und deshalb t als aktueller Parameter von FileGet() nicht funktionieren kann.
Die lange Antwort inkl. Lösung erfordert ein wenig Ausholen:
Upcast und Downcast
Ein Upcast versucht ein Objekt von einer niedrigeren Ebene in einer Vererbungshierarchie auf eine höhere zu heben. Beispiel:
Class Säugetier
...
End Class
Class Katze
Inherits Säugetier
...
End Class
Class Hund
Inherits Säugetier
...
End Class
...
Dim s As Säugetier
Dim k As Katze
Dim h As Hund
k = new Katze()
s = k
h = s
Die Zuweisung h=s erfordert einen Upcast, denn s mit dem Typ Säugetier steht in der Vererbungshierarchie auf einer tieferen Ebene als (d.h. ist eine Basisklasse von) Hund. ("Höher" und "tiefer" beziehen sich auf einen Vererbungsbaum, dessen Wurzel unten gedacht ist und der nach oben wächst.)
Umgekehrt wird die Zuweisung s = k als Downcast bezeichnet, weil typmäßig von einer höheren auf eine tiefere Ebene hinabgestiegen wird.
Ein Downcast ist in VB.NET mit Option Strict On und auch in C# schon durch den Compiler problemlos prüfbar und durchführbar, weil er sehr einfach feststellen kann, ob der Typ auf der linken Seite einer Zuweisung eine Basisklasse des Typs auf der rechten darstellt. Wenn ja, dann ist die Zuweisung erlaubt, weil nur eine Informationsreduktion stattfindet. Über die Zielobjektvariable sind anschließend einfach nicht mehr alle "Features" des Objektes erreichbar, weil sie zu Klassen gehören, die auf einer höheren Ebene stehen.
Downcasts sind Verallgemeinerungen. Säugetier ist ein allgemeinerer Begriff als Katze oder Hund. Insofern sind Katzen und Hunde zwar Säugetiere und die objektorientierte Programmierung erlaubt sowohl
s = k
als auch
s = h
aber Sie können dann eben nicht erwarten, dass ein Säugetier immer bellen kann oder nachts gut sieht und auch nicht, dass s all die "Features" besitzt, die k oder h besitzen.
Verallgemeinerungen sind also kein Problem. Ein Upcast als Spezialisierung ist hingegen ein Problem, weil sozusagen Informationen hinzugefügt werden müssten, wo vielleicht keine sind. Wenn Sie ein Säugetier in einer Schachtel bekommen und nicht hineinschauen dürfen, ist es müßig, es zu mehr als zu dem motivieren zu wollen, was Sie von allen Säugetieren erwarten können (z.B. fressen, schlafen).
Bei der Zuweisung h = s befindet sich der Compiler nun genau in derselben Position. Er kann s nicht ansehen, was in ihm steckt. Wird s zur Laufzeit nur auf Objekte vom Typ Hund (oder einer davon abgeleiteten Klasse) zeigen? Oder referenziert s eben womöglich auch Katze-Objekte?
In Ermangelung dieses Wissens über die Laufzeit, verweigert VB.NET mit Option Strict On die Übersetzung eines Upcast. Die Fehlermeldung in dem Fall lautet: Option Strict On disallows implicit conversions from '....Säugetier' to '....Hund'.
ByRef als Ursache für Upcast
Wo aber nun tritt ein Upcast bei Aufruf von FileGet() überhaupt auf? Vor allem weil die Fehlermeldung des Compilers ganz anders lautet: Overload resolution failed because no accessible 'FileGet' can be called with these arguments: ...
Die Ursache liegt in der Signatur der Funktion:
Public Sub FileGet( _
ByVal filenumber As Integer, _
ByRef value As System.ValueType, ...)
Aber zunächst etwas Beispielcode, um die Problemsituation realer zu machen. Der folgende Code liest in einer Schleife einfach alle Bytes einer Datei:
Public Structure TBuffer
Public byte1 As Byte
End Structure
...
FileOpen(1, "textfile1.txt", OpenMode.Binary, OpenAccess.Read)
Dim b As TBuffer
While Not EOF(1)
FileGet(1, b)
...
End While
FileClose(New Integer() {1})
Die Struktur TBuffer ist denkbar simpel gewählt und hat mit ihrem konkreten Aufbau auch keinen Einfluss auf das Problem.
Sobald Sie nun Option Strict On setzen, meldet der VB.NET Editor (bzw. der Compiler) den vorstehenden Fehler. Ihm "schmeckt" dann FileGet(1, b) nicht mehr.
Das ist umso merkwürdiger, als dass die Definition Structure TBuffer eigentlich bedeutet:
Public Class TBuffer
Inherits System.ValueType
...
End Class
Es scheint somit doch alles in bester Ordnung zu sein, denn der Typ des formalen Parameters value von FileGet() ist dann die Basisklasse von TBuffer und die Übergabe von b als aktuellem Parameter entspricht einem Downcast.
Dort liegt aber nicht das Problem. Und auch nicht im Basistyp System.ValueType, der aus TBuffer einen Werttyp macht, dessen Werte nicht als Objekte auf dem Heap gespeichert werden, sondern wie Integer-Werte auf dem Stack bzw. innerhalb von Objekten. Dadurch ist es zwar nötig, b beim Aufruf zuerst in ein "echtes" Objekt und nach Rückkehr wieder in eine Struktur zu wandeln (Boxing/Unboxing), weil value kein Wert-, sondern ein Referenztyp ist - aber auch das hat nichts mit dem vorliegenden Problem zu tun.
Die Ursache ist vielmehr, dass value als ByRef-Parameter definiert ist. Das bedeutet, es geht nicht nur um die Zuweisung value = b bei Aufruf von FileGet().ByRef bedeutet ja vielmehr, dass Änderungen am formalen Parameter (hier: value) innerhalb der Funktion immer auch Änderungen am aktuellen Parameter (hier: b) am Aufrufort sind.
Zur Verdeutlichung nochmal ein Beispiel aus der Tierwelt:
Sub TueEtwas(ByVal s As Säugetier)
...
End Sub
...
Dim k As New Katze()
TueEtwas(k)
Solange TueEtwas() den Parameter s als ByVal definiert, kann es kein Problem geben. Was innerhalb von TueEtwas() mit s passiert und was aus dessen ursprünglichen Wert wird, der von k stammt, ist am Aufrufort egal. Der Compiler muss sich also nur um den Downcast beim Aufruf kümmern.
Wenn TueEtwas() aber wie folgt aussieht, wird das Problem deutlich:
Sub TueEtwas(ByRef s As Säugetier)
...
s = New Hund()
End Sub
Innerhalb von TueEtwas() ist die Zuweisung als Downcast völlig in Ordnung. Ihr Effekt ist aber eben nicht lokal begrenzt, da s als ByRef-Parameter nur ein "Platzhalter" für k am Aufrufort ist. Tatsächlich würde TueEtwas() also die Zuweisung k = New Hund() ausführen - und das kann und darf nicht sein.
Um diese potenzielle Fehlerquelle von vornherein auszuschließen, kann ein Compiler also keinen Downcast bei aktuellen Parametern eines ByRef-Parameters erlauben. Deshalb muss der Versuch des Aufrufs von FileGet() wie oben bei Option Strict On fehlschlagen.
Dass die Fehlermeldung in diesem Fall leider nicht zur Lokalisierung des Problems beiträgt, ist bedauerlich, aber nicht zu ändern.
Explizites Casting als Problemlösung
Für eine Lösung des Problems dürfen Sie die Verweigerung des Compilers nicht so verstehen, dass das, was Sie wollen, wirklich nicht funktioniert. Vielmehr sagt der Compiler lediglich, er fühle sich jenseits seiner Kompetenz zur Einschätzung der Situation und entscheide konservativ, d.h. er übersetzt nicht, was er nicht beurteilen kann.
Sie können die Situation retten, indem Sie Ihr Vertrauen in die Richtigkeit Ihres Tuns explizit ausdrücken:
Dim b As TBuffer
Dim svtb As System.ValueType = b
While Not EOF(1)
FileGet(1, svtb)
b = CType(svtb, TBuffer)
Console.WriteLine(b.byte1)
End While
Bei Aufruf von FileGet() stellen Sie zunächst den Compiler zufrieden, indem Sie einen aktuellen Parameter (svtb) vom Typ System.ValueType übergeben.
svtb ist ein Objekt und enthält (!) durch Boxing bei der Zuweisung eine TBuffer-Struktur. Dieses Boxing übernimmt VB.NET in anderen Fällen gern, Sie müssen es hier jedoch explizit tun, weil VB.NET das automatische Unboxing verweigert, da es einen Upcast darstellt.
Der spannende Punkt liegt also nach der Rückkehr von FileGet(). Dort müssen Sie den Inhalt der Struktur innerhalb des Objekts von svtb, in welche FileGet() den Dateiinhalt gelesen hat, wieder in eine TBuffer-Struktur herauslesen. Einfach wäre es, wenn Sie schreiben könnten: b = svtb. Das aber wäre ein Upcast und der Compiler würde streiken.
Wenn Sie ihm aber explizit sagen, Sie wüssten, was Sie tun, dann ist er zufrieden und gibt die Verantwortung an Sie ab. Wenn Sie also eine explizite Typumwandlung vornehmen, ist alles in Ordnung:
b = Ctype(svtb, TBuffer)
Das Ausgangsproblem ist damit gelöst. Sie haben die Typumwandlungsschritte, die VB.NET automatisch in beide Richtungen vornimmt, wenn Sie Option Strict Off einstellen, von Hand implementiert.
Datentransport in verteilten Anwendungen
Frage: Wenn ich Daten in einer verteilten Anwendung (WebService oder Remoting) aus einer Datenbank übertrage, sende ich diese als Dataset zurück?
Antwort: Ja, es ist am einfachsten, die Daten in ein DataSet zu laden, um sie zwischen Client und Server zu transportieren, z.B.
<WebMethod()> _
Public Function GetCustomers(ByVal companyname As String) _
As DataSet
Dim conn As New SqlConnection(".;database=northwind")
Dim ds As New DataSet()
Dim adap As New SqlDataAdapter("select customerid, companyname " & _
"from customers where companyname like @name", conn)
adap.SelectCommand.Parameters.Add("@name", _
SqlDbType.VarChar).Value = companyname
adap.Fill(ds, "customers")
Return ds
End Function
Allerdings erfolgt die Übertragung eines DataSet immer als DiffGram, d.h. in einem XML-Format! Das kann in zweierlei Hinsicht ein Nachteil sein:
-
Falls der Empfänger der Daten nicht auf dem .NET Framework läuft (z.B. ein Java Client), kann es schwer für ihn sein, die Daten weiterzuverarbeiten. Der grundsätzliche Umgang mit XML-Daten mag zwar einfach sein, aber ein DiffGram ist keine ganz einfache Datenstruktur und so sollte man dessen Verarbeitung einer Komponente übertragen können, wie sie ein DataSet ist.
-
Der Umfang des nach XML serialisierten DataSet-Inhalts kann deutlich größer sein, als die in ihm gehaltenen Daten vermuten lassen - allemal, wenn es sich eher um Zahlen denn Texte handelt.
Falls einer der beiden Nachteile relevant in einem WebService/Remoting-Szenario ist, sollten Sie alternative Parametertypen evaluieren. Z.B. könnten Sie die Daten in ein Feld von Strukturen serialisieren:
<Serializable()> _
Public Structure Customer
Public customerid As String
Public companyname As String
Public Sub New(ByVal customerid As String, _
ByVal companyname As String)
Me.customerid = customerid
Me.companyname = companyname
End Sub
End Structure
<WebMethod()> _
Public Function GetCustomers(ByVal companyname As String) _
As Customer()
Dim conn As New SqlConnection(".;database=northwind")
conn.Open()
Try
Dim cmd As New SqlCommand("select customerid, " & _
"companyname from customers " & _
"where companyname like @name", conn)
cmd.Parameters.Add("@name", _
SqlDbType.VarChar).Value = companyname
Dim dr As SqlDataReader = cmd.ExecuteReader
Try
Dim custs As New ArrayList()
While dr.Read
custs.Add(New Customer(dr("customerid"), _
dr("companyname")))
End While
GetCustomers = custs.ToArray(GetType(Customer))
Finally
dr.Close()
End Try
Finally
conn.Close()
End Try
End Function
Sie sparen sich damit die Serialisierung von Metainformationen (z.B. Spaltennamen und -Typen) und im Falle eines eingestellten Binärformatierers auch die Umwandlung nach XML.
Unter dem Strich: Wenn Client und Server .NET-Framework-Anwendungen sind, können Sie beliebige serialisierbare Datenstrukturen für den Datentransport benutzen. Es geht dann im Wesentlichen um die Balance zwischen Performance und Bequemlichkeit in der Programmierung.
Ist der Client jedoch auf einer anderen Plattform implementiert (und sei es auch nur VB6 mit dem SOAP Toolkit), dann müssen Sie genau hinschauen, welche Datentypen er wirklich versteht. Ein DataSet wird es wahrscheinlich nicht sein.