Was ist die Best Practice in C# für die Typumwandlung?

Was ist die Best Practice in C# für die Typumwandlung?

Zumindest gibt es zwei Möglichkeiten für das Casting, eine für die Typprüfung und eine Kombination aus beiden, die als Musterabgleich bezeichnet wird. Jeder hat seinen eigenen Zweck und hängt von der Situation ab:

Harte Besetzung

var myObject = (MyType)source;

Normalerweise tun Sie dies, wenn Sie absolut sicher sind, ob das angegebene Objekt von diesem Typ ist. Eine Situation, in der Sie es verwenden, wenn Sie einen Ereignishandler abonniert haben und das Senderobjekt in den richtigen Typ umwandeln, um daran zu arbeiten.

private void OnButtonClick(object sender, EventArgs e)
{
    var button = (Button)sender;

    button.Text = "Disabled";
    button.Enabled = false;
}

Weicher Guss

var myObject = source as MyType;

if (myObject != null)
    // Do Something

Dies wird normalerweise verwendet, wenn Sie nicht wissen können, ob Sie wirklich diese Art von Typ haben. Versuchen Sie also einfach, es zu wirken, und wenn es nicht möglich ist, geben Sie einfach eine Null zurück. Ein allgemeines Beispiel wäre, wenn Sie etwas nur tun müssen, wenn eine Schnittstelle erfüllt ist:

var disposable = source as IDisposable;

if(disposable != null)
    disposable.Dispose();

Auch der as Der Operator kann nicht auf einem struct verwendet werden . Dies liegt einfach daran, dass der Operator einen null zurückgeben möchte falls die Umwandlung fehlschlägt und ein struct kann niemals null sein .

Typprüfung

var isMyType = source is MyType;

Dies wird selten richtig verwendet. Diese Typprüfung ist nur nützlich, wenn Sie nur wissen müssen, ob etwas von einem bestimmten Typ ist, aber Sie müssen dieses Objekt nicht verwenden.

if(source is MyType)
   DoSomething();
else
   DoSomethingElse();

Musterabgleich

if (source is MyType myType)
    DoSomething(myType);

Der Musterabgleich ist die neueste Funktion im dotnet-Framework, die für Umwandlungen relevant ist. Aber Sie können auch kompliziertere Fälle behandeln, indem Sie die switch-Anweisung und die when-Klausel verwenden:

switch (source)
{
    case SpecialType s when s.SpecialValue > 5
        DoSomething(s);
    case AnotherType a when a.Foo == "Hello"
        SomethingElse(a);
}

Ich denke, das ist eine gute Frage, die eine ernsthafte und ausführliche Antwort verdient. Typumwandlungen in C# sind eigentlich viele verschiedene Dinge.

Im Gegensatz zu C# sind Sprachen wie C++ diesbezüglich sehr streng, daher verwende ich die dortige Benennung als Referenz. Ich denke immer, dass es am besten ist, zu verstehen, wie die Dinge funktionieren, also werde ich alles hier für Sie mit den Details aufschlüsseln. Hier geht's:

Dynamische Umwandlungen und statische Umwandlungen

C# hat Werttypen und Referenztypen. Referenztypen folgen immer einer Vererbungskette, beginnend mit Object.

Grundsätzlich, wenn Sie (Foo)myObject tun , führen Sie tatsächlich eine dynamische Umwandlung durch , und wenn Sie (object)myFoo machen (oder einfach object o = myFoo ) führen Sie eine statische Umwandlung durch .

Eine dynamische Umwandlung erfordert, dass Sie eine Typprüfung durchführen, d. h. die Laufzeit prüft, ob das Objekt, in das Sie umwandeln, den Typ hat. Schließlich verwerfen Sie den Vererbungsbaum, also können Sie genauso gut auf etwas ganz anderes umwerfen. In diesem Fall erhalten Sie am Ende InvalidCastException . Aus diesem Grund erfordern dynamische Umwandlungen Informationen zum Laufzeittyp (z. B. muss die Laufzeit wissen, welches Objekt welchen Typ hat).

Eine statische Umwandlung erfordert keine Typenprüfung. In diesem Fall werfen wir im Vererbungsbaum nach oben, also wissen wir es bereits dass die Typumwandlung erfolgreich sein wird. Es wird niemals eine Ausnahme ausgelöst.

Werttypumwandlungen sind eine spezielle Art von Cast, die verschiedene Werttypen umwandelt (z. B. von Float nach Int). Darauf gehe ich später ein.

Wie es ist, gegossen

In IL werden nur castclass unterstützt (cast) und isinst (wie). Die is Operator ist als as implementiert mit Nullprüfung und ist nichts weiter als eine praktische Kurzschreibweise für die Kombination aus beidem. In C# könnten Sie is schreiben als:(myObject as MyFoo) != null .

as überprüft einfach, ob ein Objekt von einem bestimmten Typ ist, und gibt null zurück, wenn dies nicht der Fall ist. Für die statische Umwandlung In diesem Fall können wir diese Kompilierzeit für den dynamischen Cast bestimmen In diesem Fall müssen wir dies zur Laufzeit überprüfen.

(...) Umwandlungen überprüfen erneut, ob der Typ korrekt ist, und lösen eine Ausnahme aus, wenn dies nicht der Fall ist. Es ist im Grunde dasselbe wie as , aber mit einem throw statt einem null Ergebnis. Sie fragen sich vielleicht, warum as ist nicht als Ausnahmehandler implementiert - nun, das liegt wahrscheinlich daran, dass Ausnahmen relativ langsam sind.

Boxen

Eine besondere Art der Umwandlung findet statt, wenn Sie box ein Werttyp in ein Objekt. Was im Grunde passiert, ist, dass die .NET-Laufzeit Ihren Werttyp auf den Heap kopiert (mit einigen Typinformationen) und die Adresse als Referenztyp zurückgibt. Mit anderen Worten:Es wandelt einen Werttyp in einen Referenztyp um.

Dies passiert, wenn Sie Code wie diesen haben:

int n = 5;
object o = n; // boxes n
int m = (int)o; // unboxes o

Beim Unboxing müssen Sie einen Typ angeben. Während des Unboxing-Vorgangs wird der Typ überprüft (wie beim dynamischen Cast Fall, aber es ist viel einfacher, weil die Vererbungskette eines Werttyps trivial ist) und wenn der Typ übereinstimmt, wird der Wert zurück auf den Stack kopiert.

Sie könnten erwarten, dass Werttypumwandlungen für das Boxen implizit sind - nun, aufgrund des oben Gesagten sind sie es nicht. Der einzige zulässige Unboxing-Vorgang ist das Unboxing auf den genauen Werttyp. Mit anderen Worten:

sbyte m2 = (sbyte)o; // throws an error

Werttypumwandlungen

Wenn Sie einen float senden zu einem int , konvertieren Sie im Grunde der Wert. Für die Basistypen (IntPtr, (u)int 8/16/32/64, float, double) sind diese Konvertierungen in IL als conv_* vordefiniert Anweisungen, die Bitcasts (int8 -> int16), Kürzung (int16 -> int8) und Umwandlung (float -> int32) entsprechen.

Übrigens gehen hier einige lustige Dinge vor sich. Die Laufzeit scheint mit einer Vielzahl von 32-Bit-Werten auf dem Stapel zu arbeiten, sodass Sie Konvertierungen auch an Stellen benötigen, an denen Sie sie nicht erwarten würden. Betrachten Sie zum Beispiel:

sbyte sum = (sbyte)(sbyte1 + sbyte2); // requires a cast. Return type is int32!
int sum = int1 + int2; // no cast required, return type is int32.

Die Zeichenerweiterung kann schwierig zu verstehen sein. Computer speichern vorzeichenbehaftete ganzzahlige Werte als 1er-Komplemente. In Hex-Schreibweise, int8, bedeutet dies, dass der Wert -1 0xFF ist. Was passiert also, wenn wir es in ein int32 umwandeln? Auch hier ist der 1-Komplement-Wert von -1 0xFFFFFFFF - also müssen wir das höchstwertige Bit an den Rest der "hinzugefügten" Bits weitergeben. Wenn wir unsignierte Erweiterungen verwenden, müssen wir Nullen weitergeben.

Um diesen Punkt zu veranschaulichen, hier ein einfacher Testfall:

byte b1 = 0xFF;
sbyte b2 = (sbyte)b1;
Console.WriteLine((int)b1);
Console.WriteLine((int)b2);
Console.ReadLine();

Die erste Umwandlung in int ist hier nullerweitert, die zweite Umwandlung in int ist vorzeichenerweitert. Vielleicht möchten Sie auch mit dem "x8"-Format-String spielen, um die Hex-Ausgabe zu erhalten.

Für den genauen Unterschied zwischen Bitcasts, Trunkierung und Konvertierung verweise ich auf die LLVM-Dokumentation, die die Unterschiede erklärt. Suchen Sie nach sext /zext /bitcast /fptosi und alle Varianten.

Implizite Typumwandlung

Eine weitere Kategorie bleibt übrig, und das sind die Konvertierungsoperatoren. MSDN beschreibt, wie Sie die Konvertierungsoperatoren überladen können. Grundsätzlich können Sie Ihre eigene Konvertierung implementieren, indem Sie einen Operator überladen. Wenn Sie möchten, dass der Benutzer explizit angibt, dass Sie eine Übertragung beabsichtigen, fügen Sie den explicit hinzu Stichwort; Wenn Sie möchten, dass implizite Konvertierungen automatisch erfolgen, fügen Sie implicit hinzu . Grundsätzlich erhalten Sie:

public static implicit operator byte(Digit d)  // implicit digit to byte conversion operator
{
    return d.value;  // implicit conversion
}

... danach können Sie Sachen wie

machen
Digit d = new Digit(123);
byte b = d;

Best Practices

Verstehen Sie zunächst die Unterschiede, was bedeutet, dass Sie kleine Testprogramme implementieren, bis Sie den Unterschied zwischen allen oben genannten verstanden haben. Es gibt keinen Ersatz für das Verständnis von How Stuff Works.

Dann würde ich mich an diese Praktiken halten:

  • Die Abkürzungen gibt es aus einem bestimmten Grund. Verwenden Sie die kürzeste Notation, wahrscheinlich die beste.
  • Verwenden Sie Casts nicht für statische Casts; Verwenden Sie Umwandlungen nur für dynamische Umwandlungen.
  • Benutze Boxen nur, wenn du es brauchst. Die Details dazu gehen weit über diese Antwort hinaus; Im Grunde sage ich:Verwenden Sie den richtigen Typ, wickeln Sie nicht alles ein.
  • Beachten Sie Compiler-Warnungen zu impliziten Konvertierungen (z. B. unsigned/signed) und immer lösen Sie sie mit expliziten Umwandlungen auf. Sie wollen keine Überraschungen mit seltsamen Werten aufgrund der Vorzeichen/Null-Erweiterung erleben.
  • Meiner Meinung nach ist es am besten, wenn Sie nicht genau wissen, was Sie tun, einfach die implizite/explizite Konvertierung zu vermeiden – ein einfacher Methodenaufruf ist normalerweise besser. Der Grund dafür ist, dass Sie am Ende mit einer Ausnahme auf freiem Fuß landen könnten, die Sie nicht kommen sahen.