Wie ich hier schon geschrieben habe gibt es zwischen VB und C# einige Unterschiede bei der Handhabung von Datentypen. War beim letzten mal die dynamische Typisierung Thema, will ich mich diesmal mit Überläufen beschäftigen.
Fangen wir mit einem kleinen Beispiel an um zu verdeutlichen was ich meine:
1 byte b = 0; 2 3 for (int i = 0; i<300; i++) 4 { 5 b++; 6 Console.Write("{0}\t", b); 7 }
Ein Byte hat einen Wertebereich von 0 bis 255. Unsere Schleife produziert also einen Überlauf indem Sie 300x eine Erhöhung um 1 durchführt. Was passiert? Eine Exception? Wird weitergezählt?
Weder noch. Was wir sehen ist ein sogenannter Überlauf. Der Wert der Variablen nimmt wieder seinen Startwert an und beginnt dort wieder zu rechnen. Dasselbe passiert auch bei Casts großer Werte auf einen niedrigerwertigen Datentyp:
1 int i = 300; 2 byte b = (byte)i; 3 Console.WriteLine(b);
Der Wert in b ist hier 44 (300-256) und zeigt damit anschaulich den Überlauf.
In diesen beiden Beispielen noch recht offensichtlich, kann ein Überlauf durchaus auch "versehentlich" geschehen wenn Casts implizit geschehen. Vielen Programmierern ist nicht bewußt, daß häufig bei mathematischen Operationen oder Vergleichen auch Casts durchgeführt werden. So ist zum Beispiel das Ergebnis der Addition zweier Bytes ein Integer (interessanterweise ergeben aber zwei Integer addiert nicht etwa ein Long sondern wieder ein int...). Wenn der Compiler meckert ist die Versuchung arg groß, ihn durch einen Cast zum Schweigen zu bringen:
1 byte b1 = 200, b2 = 200, b3 = 0; 2 // b3 = b1 + b2; //compiliert nicht 3 b3 = (byte)(b1+b2); // compiliert, riskiert aber einen Overflow
Fatal wird das Ganze, wenn später auf Basis des Wertes in b3 eine Entscheidung getroffen wird. So werden arithmetische Überläufe schnell zu einem handfesten Security-Problem, denn ein potentieller Angreifer könnte unter Umständen durch geschickte Eingaben Überläufe produzieren, die den Programmverlauf unerwünscht ändern.
Was also tun?
In C# existieren gleich mehrere Optionen die unterschiedliches bewirken. Allen gemeinsam ist, daß sie eine Fähigkeit in der Runtime ausnutzen, nämlich die Möglichkeit, Operationen automatisch zu prüfen. In MSIL (Microsoft Intermediate Language) existieren sogar zwei verschiedene Instruction-sets für geprüfte und ungeprüfte Operationen.
Eine entsprechende Option gesetzt (/checked+) gesetzt bringt den Compiler dazu, Code zu erzeugen, der auf Überläufe geprüft wird. Die Folge: Immer wenn eine Variable überläuft wird eine OverflowException geworfen. Natürlich ist das mit einem gewissen Performanceverlust verbunden. Wer aber ein wenig rumtestet wird feststellen, daß der eigentlich nur in Algorithmen zum Tragen kommt, die eine große Anzahl Operationen durchführen. Sprich: Otto-Normal-Anwendungen dürften den Unterschied nicht bemerken.
Wer aber trotzdem Bedenken hat, für den ist vielleicht die zweite Variante interessant, nämlich feingranular zu steuern wo gechecled wird. Dafür existieren in C# zwei Schlüsselworte: checked {} und unchecked{}
1 byte b = 0; 2 3 for (int i = 0; i<300; i++) 4 { 5 checked 6 { 7 b++; // Jetzt wird hier eine Exception geworfen 8 } 9 Console.Write("{0}\t", b); 10 }
So deklarierte Blöcke können damit gezielt performant bzw. sicher gemacht werden. Das gilt natürlich auch für Casts, für die es dann aber auch noch eine dritte Möglichkeit gibt: Die Klasse Convert aus dem Framework:
1 byte b1 = 200, b2 = 200, b3 = 0; 2 // b3 = b1 + b2; //compiliert nicht 3 // b3 = (byte)(b1+b2); // compiliert, riskiert aber einen Overflow 4 b3 = Convert.ToByte(b1+b2); //Exception wenn Overflow
Und wo liegt jetzt der Unterschied zu VB.NET? VB.NET emittiert standardmäßig NUR checked Code, sprich: Hier wird immer sichergestellt, dass Variablen nicht überlaufen. Um das zu prüfen muß man nur entsprechende Beispiele probieren:
Dim b As Byte For i As Integer = 0 To 300 b += 1 ' Exception bei Überlauf Next
Was lernen wir daraus? Wenn wirklich das letzte Quentchen Performance gebraucht wird, mag C# die bessere Wahl für Algorithmen sein. Umgekehrt hat VB den etwas "sichereren" Standard voreingestellt.
IL-Instruktionen mit Überlauf-Prüfung:
1 add.ovf.<mit Vorzeichen> 2 conv.ovf.<auf Typ> 3 conv.ovf.<Zieltyp>.un 4 mul.ovf.<Typ> 5 sub.ovf.<Typ> 6 newarr 7