Als ich nach Informationen gesucht habe um mein Post zum Thema Exceptions und „as“ zu untermauern, bin ich in der MSDN auf zwei weitere Schlüsselworte gestoßen die ich noch nicht kannte: implicit und explicit
Diese Entdeckung hat mich schon etwas verwundert, sind beide doch seit .Net 2.0 Teil des Frameworks aber mir in den vergangenen Jahren noch nie über den Weg gelaufen. Woran das wohl liegt?
Bevor ich diese Frage beantworte, ist erst einmal zu klären was das Duo wirklich tut. Hierbei passt es sehr gut in den Kontext des bereits erwähnten Artikels, denn sie dienen der Definition von Konvertierungs- und Cast-Operationen.
Um das genauer zu verstehen, betrachten wir die Struktur Bit. Also ein Wertetyp der entweder eine 1 oder eine 0 darstellt. Intern hält er sich dafür nur ein unsigned integer, welches bei einem Cast in den Zieldatentyp gewandelt werden soll.
public struct Bit { private uint value; public uint Value { get { return this.value; } set { if(value > 1 ) throw new InvalidDataException("Value is larger than 1!"); this.value = value; } } }
Versucht man nun diese Struktur einem unsigned Integer zuzuweisen kann man nicht kompilieren, da sie weder in einer Vererbungshierarchie liegen noch eine geeignete Konvertierungsmethode vorhanden ist und sie somit inkompatibel sind.
Bit bit = new Bit(){Value = 1}; uint i = bit;
Wir müssen also genau diese Konvertierugnsmethode angeben. Hierzu brauchen wir die Schlüsselwörter implict und operator. Damit definieren wir uns sozusagen eine Überladung des Zuweisungsoperators, der normalerweise nicht überladen werden kann!
public static implicit operator uint(Bit value) { return value.Value; }
Ein Problem hat unsere aktuelle Lösung noch, denn wenn man ein Bit einem uint zuweisen kann, sollte man ein uint auch einem Bit zuweisen können. Dieses Problem kann sehr einfach gelöst werden, indem man Bit einfach einen weiteren Operator hinzufügt. Dieser muss also nicht am Standartdatentyp definiert werden.
public static implicit operator Bit(uint value) { var bit = new Bit { Value = value }; return bit; }
Nun funktionieren zwar Zuweisungen, es wird aber nicht offensichtlich, dass es sich dabei um einen Cast handelt. Dies erreicht man wiederum über das Schlüsselwort explicit.
public static explicit operator int(Bit bitValue) { return (int)bitValue.Value; }
Durch explicit wird eine explizite Angabe des Typs erforderlich und dadurch wiederrum für jeden auch offensichtlich, dass es sich um einen Cast handelt. Folgender Code zeigt das gesamte Spektrum der nun möglichen Operationen.
Bit bit = 1; uint i = bit; int j = (int)bit;
Vor allem die einfache Zuweisung in der ersten Zeile finde ich sehr reizvoll, da auf diese Weise das elende new verschwindet welches mich im Zusammenhang mit struct immer etwas stört. Sehr interessant ist daran auch, dass die angegebene 1 automatisch als uint interpretiert wird und bei einer -1 nicht einmal der Compiler anspringt. Immerhin hatten wir für int ja keinen impliziten Cast erlaubt.
Ganz ungefährlich ist die Sache aber nicht, weshalb in der MSDN auch diverse Spielregeln zur Verwendung von implicit und explicit beschrieben sind:
- Keine unerwarteten Konvertierungsfunktionen
Hier gegen verstößt das Beispiel bereits. Immerhin ist nicht klar ersichtlich, dass uint nicht größer als 1 sein darf. Auch die Konvertierung von int ist aufgrund möglicher negativer Zahlen äußerst gewagt. - Konvertierungen nicht außerhalb der Domäne des Typen
Im Grunde eine Verschärfung der ersten Regel. Da gegen würden wir verstoßen wenn wir nach DateTime, Point usw. konvertieren. Immerhin dienen solche Typen nicht vorrangig der Verarbeitung von Zahlen. - implicit sollte nicht bei Konvertierungen eingesetzt werden bei denen Daten verloren gehen können
Ups, die nächste Regel die wir verletzt haben… Negative Zahlen gehen uns ja genzlich verloren und alle größer 1 werden eher schlecht als recht behandelt. - Implizite Konvertierungen dürfen keine Exceptions werfen
Und noch eine verletzte Regel. Der Grund dafür ist klar, woher soll der Nutzer auch wissen, dass es sich bei einer Zuweisung letztendlich um eine Konvertierung handelt? Debugging wird damit zumindest nicht vereinfacht. - Es ist eine InvalidCastException zu werfen wenn eine Konvertierung mit Datenverlust geschieht
Mit der InvalidDataException war ich also gar nicht so verkehrt, richtig ist es dennoch nicht…
Oh weia, 4 von 5 Punkten falsch. Das ist alles andere als ein guter Schnitt und zeigt uns, dass implicit und explicit zurecht ein Schattendasein fristen, immerhin kann man sich bei ihrer Verwendung schnell in die Nesseln setzen. Darüber hinaus ergibt sich mit Punkt eins der Regeln ein eher eingeschränkter Wirkungsbereich des Duos.
Mir ist im ersten Moment zum Beispiel der Konvertierung von Daten unterschiedlicher Transferobjekte eingefallen. Bei näherer Betrachtung sehe ich das aber als eher kritisch. In diesem Fall müssen dann die Datentypen untereinander bekannt sein und genau das will man eigentlich nicht. Viel eher sind implicit und explicit bei der Schaffung neuer Basisdatentypen sinnvoll, aber wie häufig macht man das?
Als Gründe für die geringe Verbreitung der Schlüsselwörter bleibt demnach fest zu halten: Das Anwendungsspektrum ist eher gering, man kann viele Fehler machen und vorallem mit der guten Absicht den Code lesbarer zu gestallt das genaue Gegenteil erreichen. Dazu kommt noch, dass die beiden Schlüsselwörter nur von C# unterstützt werden.
Kommentare