Donnerstag, 26. Dezember 2013

C# - Misshandlung von 'as'

Es scheint eine Modeerscheinung zu sein, die sich derzeit rasant ausbreitet. Man kann seit geraumer Zeit sehr oft in Demo Codes folgende 'as' Verwendung sehen.

Sinnfreies Beispiel:

void SetText(object sender)
{
   (sender as TextBox).Text = "Hallo";
}

Auf den ersten Blick denkt man "interessant". Aber auf den zweiten Blick sollte jeder erfahrene Programmierer den Fehler erkennen.
Hier wird per 'as' gecastet ohne Typprüfung.

Durch die Verwendung von 'as'  (msdn) wird der Aufruf der Methode universell. D.h. ich kann jedes Objekt in die Methode reinwerfen. Irgendwann gibt es eine NullPointerException.
Aber wann gibt es diese Exception?

Hier mal ein paar Testimplementierungen:
SetText(new TextBox())
SetText("Hallo"); // NullPointerException
SetText(null); // NullPointerException

Wenn man sich nun mal vorstellt, dass man gar nicht weiß wie die Methode SetText implementiert ist würde man bei einer NullPointerException erstmal stutzig werden. Der string "Hallo" ist doch gar kein null-Wert, aber ich bekomme eine solche Exception? Sehr verwirrend. Was habe ich falsch gemacht? Oder was muss ich tun, damit bei "Hallo" keine Exception kommt?

Genau hier liegt das Problem. Eigentlich sollte eine InvalidCastException geworfen werden, um mir zu signalisieren, dass es ein Typproblem gibt.

Durch die fehlerhafte Verwendung von 'as' hat man sich selber ein Bein gestellt.

Eine korrekte Implementierung der Methode sollte in etwa so aussehen:
void SetText(object sender)
{
   var textBox = (TextBox) sender; // InvalidCastException
   textBox.Text = "Hallo"; // NullPointerException

Hier werden nun die richtigen Exceptions geworfen. 
Der Vorteil ist, dass man nun den möglichen Programmfehler (Exception) besser bewerten kann und nach entsprechenden Objekten zur Laufzeit suchen kann (debuggen). Dies spart Zeit und Geld.

Hinweis:

Mir ist klar, dass man bereits in der Parameterdefinition den Typ TextBox angeben könnte. Dann wäre per Definition "Hallo" als Parameterwert nicht mehr möglich gewesen. Ein cast mit 'as' aber auch nicht :-) Deshalb hier nochmal, das ist nur ein einfaches Beispiel. Komplexere Beispiele mit Deserialisierungen o.ä. habe ich an dieser Stelle vermieden.

Hintergrund

'as' "[...] entspricht dem folgenden Ausdruck, mit der Ausnahme, dass expression nur ein Mal ausgewertet wird." (msdn):
expression is type ? (type)expression : (type)null

Der einzige Grund für ein as-cast ist die Performance. Da es keine InvalidCastException geben kann (Exceptions kosten Zeit).

Hier ein kleines Beispiel:
var name = dataRow["Name"] as string;
var id = dataRow["Id"] as int?;
Beim Abrufen des Namens aus der DataRow erwarte ich einen String, sei er null oder vorhanden. Manch einer möchte aber besonders sicher gehen und prüfen, ob da nicht doch noch ein anderes Objekt enthalten ist, statt eines Strings. Dann kann man natürlich auch einen expliziten Cast wählen. Verbaut sich aber zusätzlich die Möglichkeit Standardwerte relativ einfach zu definieren.
var name = dataRow["Name"] as string
           ?? "keiner vorhanden"; // Standardwert
var id = dataRow["Id"] as int?
         ?? -1; // Standardwert
dataRow["Name"] könnte als null Wert auch ein DBNull-Objekt enthalten. Dieses würde automatisch durch ein as-cast in null umgewandelt.


Ein weiteres Beispiel wäre ein Filter auf Personen in einer Liste mit Objekten:
foreach(object item in list)
{
   var person = item as person;
 
   if (person != null)
      person.Delete();
}

In diesem Beispiel werden alle Person-Objekte als gelöscht markiert. Man filtert mit dem as-cast also die Personen aus der Liste heraus.

Sicher kann man auch hier einen expliziten cast durchführen, aber hierbei würde das Objekt in der Variablen item 2 mal ausgewertet. Einmal bei der 'is' Prüfung und dann noch einmal beim cast. Dies kann dann etwas Performance kosten und ist eigentlich unnötig.


Wann 'as'


Das Schlüsselwort 'as' ist auf jeden Fall optisch leichter zu lesen, da man sich Text erspart, den man schreiben und später lesen muss. Und alles was dem schnelleren Verstehen dienlich ist ist gut.

Wann man nun 'as' verwendet hängt davon ab, wie der Kontext definiert wurde. Erwarte ich beim Aufruf keine InvalidCastException, dann sollte man 'as' verwenden.

Besteht aber die Möglichkeit einer solchen Exception, dann sollte man lieber zu einem expliziten cast greifen. Allerdings sollte man das Nachprogrammieren des as-Schlüsselwortes vermeiden. :-)
'is' sollte man nur verwenden, wenn man auf unterschiedliche Typen unterschiedlich reagieren möchte.

Negativ-Beispiel


Ich habe mal gelesen, dass jemand meinte, sobald er eine event handler Methode implementiert könne er davon ausgehen, dass in der sender-Variable immer das Objekt enthalten ist, das den event bereitstellt, und er somit 'as' verwenden darf. Er wisse ja was er tut.

Dem kann ich nicht zustimmen. Wäre dies wirklich so, dann würde die Parameterdefinition nicht (object sender, EventArgs e) sein, sondern einen speziellen Typen beim sender Parameter angeben.

Somit wäre folgende Implementierung falsch:

new Button().Click += 
   (sender,e) => (sender as Button).Text = "geklickt"; 

Man kann nicht davon ausgehen, dass heute und für alle Zeit sich in der Variable 'sender' immer ein Button-Objekt befindet. Das würde dann mit hellseherischen Fähigkeiten zu tun haben.

Viel wichtiger ist die Aussage, dass der Programmierer in der Variable 'sender' einen Button erwartet. Das heißt, wenn in 'sender' kein Button-Objekt enthalten ist muss das Programm mir das sagen, indem eine InvalidCastException geworfen wird. Schließlich ist das Programmverhalten auf ein Button-Objekt ausgelegt. 
Diese Definition kann mit einem as-cast nicht sicher gestellt werden. Da hier das Programm nur meldet, dass gar kein Objekt vorhanden wäre.


Fazit


Ein as-cast ist eine tolle Sache, wenn man es an den richtigen Stellen verwendet. Richtige Stellen sind die, an denen keine InvalidCastException zur Programmablaufsicherung benötigt wird.
Im Programmcode sollte man immer darauf achten, dass man die Definitionen die man aufstellt nicht mit einem as-cast wieder aushebelt.