[Anmerkung: Diese Frage hatte den Originaltitel " C (ish) style union in C # ", aber wie Jeffs Kommentar mir mitteilte, wird diese Struktur anscheinend als "diskriminierte Union" bezeichnet.]
Entschuldigen Sie die Ausführlichkeit dieser Frage.
Es gibt einige ähnlich klingende Fragen zu meinen bereits in SO, aber sie scheinen sich auf die speichersparenden Vorteile der Gewerkschaft zu konzentrieren oder sie für Interop zu verwenden. Hier ist ein Beispiel für eine solche Frage .
Mein Wunsch nach einer gewerkschaftlichen Sache ist etwas anders.
Ich schreibe gerade Code, der Objekte generiert, die ein bisschen so aussehen
public class ValueWrapper
{
public DateTime ValueCreationDate;
// ... other meta data about the value
public object ValueA;
public object ValueB;
}
Ziemlich kompliziertes Zeug, ich denke du wirst mir zustimmen. Die Sache ist, dass ValueA
es nur wenige bestimmte Typen geben kann (sagen wir string
, int
und Foo
(was eine Klasse ist) und ValueB
eine andere kleine Gruppe von Typen sein kann. Ich mag es nicht, diese Werte als Objekte zu behandeln (ich möchte das warme, gemütliche Gefühl von Codierung mit ein wenig Typensicherheit).
Also habe ich darüber nachgedacht, eine triviale kleine Wrapper-Klasse zu schreiben, um die Tatsache auszudrücken, dass ValueA logisch eine Referenz auf einen bestimmten Typ ist. Ich rief die Klasse an, Union
weil mich das, was ich erreichen wollte, an das Gewerkschaftskonzept in C erinnerte.
public class Union<A, B, C>
{
private readonly Type type;
public readonly A a;
public readonly B b;
public readonly C c;
public A A{get {return a;}}
public B B{get {return b;}}
public C C{get {return c;}}
public Union(A a)
{
type = typeof(A);
this.a = a;
}
public Union(B b)
{
type = typeof(B);
this.b = b;
}
public Union(C c)
{
type = typeof(C);
this.c = c;
}
/// <summary>
/// Returns true if the union contains a value of type T
/// </summary>
/// <remarks>The type of T must exactly match the type</remarks>
public bool Is<T>()
{
return typeof(T) == type;
}
/// <summary>
/// Returns the union value cast to the given type.
/// </summary>
/// <remarks>If the type of T does not exactly match either X or Y, then the value <c>default(T)</c> is returned.</remarks>
public T As<T>()
{
if(Is<A>())
{
return (T)(object)a; // Is this boxing and unboxing unavoidable if I want the union to hold value types and reference types?
//return (T)x; // This will not compile: Error = "Cannot cast expression of type 'X' to 'T'."
}
if(Is<B>())
{
return (T)(object)b;
}
if(Is<C>())
{
return (T)(object)c;
}
return default(T);
}
}
Die Verwendung dieser Klasse ValueWrapper sieht jetzt so aus
public class ValueWrapper2
{
public DateTime ValueCreationDate;
public Union<int, string, Foo> ValueA;
public Union<double, Bar, Foo> ValueB;
}
Das ist ungefähr das, was ich erreichen wollte, aber mir fehlt ein ziemlich wichtiges Element - die vom Compiler erzwungene Typprüfung beim Aufrufen der Is- und As-Funktionen, wie der folgende Code zeigt
public void DoSomething()
{
if(ValueA.Is<string>())
{
var s = ValueA.As<string>();
// .... do somethng
}
if(ValueA.Is<char>()) // I would really like this to be a compile error
{
char c = ValueA.As<char>();
}
}
IMO Es ist nicht gültig, ValueA zu fragen, ob es sich um eine handelt, char
da die Definition eindeutig besagt, dass dies nicht der Fall ist. Dies ist ein Programmierfehler, und ich möchte, dass der Compiler dies aufgreift . [Auch wenn ich das richtig machen könnte, würde ich (hoffentlich) auch Intellisense bekommen - was ein Segen wäre.]
Um dies zu erreichen, möchte ich dem Compiler mitteilen, dass der Typ T
A, B oder C sein kann
public bool Is<T>() where T : A
or T : B // Yes I know this is not legal!
or T : C
{
return typeof(T) == type;
}
Hat jemand eine Idee, ob das, was ich erreichen möchte, möglich ist? Oder bin ich einfach nur dumm, diese Klasse überhaupt zu schreiben?
Danke im Voraus.
quelle
StructLayout(LayoutKind.Explicit)
und implementiert werdenFieldOffset
. Dies ist natürlich nicht mit Referenztypen möglich. Was Sie tun, ist überhaupt nicht wie eine C-Union.Antworten:
Ich mag die oben bereitgestellten Lösungen zur Typprüfung und Typumwandlung nicht wirklich. Hier ist also eine 100% typsichere Vereinigung, die Kompilierungsfehler auslöst, wenn Sie versuchen, den falschen Datentyp zu verwenden:
quelle
match
, und das ist so gut wie jeder andere .type Result = Success of int | Error of int
Ich mag die Richtung der akzeptierten Lösung, aber sie lässt sich nicht gut für Gewerkschaften mit mehr als drei Elementen skalieren (z. B. würde eine Vereinigung von 9 Elementen 9 Klassendefinitionen erfordern).
Hier ist ein weiterer Ansatz, der zur Kompilierungszeit ebenfalls zu 100% typsicher ist, der sich jedoch leicht auf große Gewerkschaften ausweiten lässt.
quelle
dynamic
& generics inUnionBase<A>
und der Vererbungskette erscheint unnötig. Machen SieUnionBase<A>
nicht generisch, töten Sie den Konstruktor, der ein nimmtA
, und machen Sievalue
einobject
(was es sowieso ist; es gibt keinen zusätzlichen Vorteil, wenn Sie es deklarierendynamic
). Leiten Sie dann jedeUnion<…>
Klasse direkt von abUnionBase
. Dies hat den Vorteil, dass nur die richtigeMatch<T>(…)
Methode verfügbar gemacht wird. (So wie es jetzt ist, z. B.Union<A, B>
stellt es eine Überlastung dar,Match<T>(Func<A, T> fa)
die garantiert eine AusnahmeA
Ich habe einige Blog-Beiträge zu diesem Thema geschrieben, die nützlich sein könnten:
Angenommen, Sie haben ein Warenkorbszenario mit drei Status: "Leer", "Aktiv" und "Bezahlt" mit jeweils unterschiedlichem Verhalten.
ICartState
Schnittstelle, die alle Zustände gemeinsam haben (und es könnte sich nur um eine leere Markierungsschnittstelle handeln).Sie könnten die F # -Runtime von C # verwenden, aber als leichtere Alternative habe ich eine kleine T4-Vorlage zum Generieren von Code wie diesem geschrieben.
Hier ist die Schnittstelle:
Und hier ist die Implementierung:
Angenommen, Sie erweitern das
CartStateEmpty
undCartStateActive
mit einerAddItem
Methode, die nicht von implementiert istCartStatePaid
.Und sagen wir auch, das
CartStateActive
hat einePay
Methode gibt, die die anderen Staaten nicht haben.Dann ist hier ein Code, der zeigt, wie er verwendet wird - zwei Artikel hinzufügen und dann für den Warenkorb bezahlen:
Beachten Sie, dass dieser Code vollständig typsicher ist - keine Casting- oder Bedingungsbedingungen und Compilerfehler, wenn Sie beispielsweise versuchen, für einen leeren Einkaufswagen zu bezahlen.
quelle
Ich habe dafür eine Bibliothek unter https://github.com/mcintyre321/OneOf geschrieben
Es enthält die generischen Typen für DUs, z. B.
OneOf<T0, T1>
bis zuOneOf<T0, ..., T9>
. Jeder von diesen hat ein.Match
und ein.Switch
Anweisung, die Sie für ein compilersicheres typisiertes Verhalten verwenden können, z.`` `
`` `
quelle
Ich bin nicht sicher, ob ich Ihr Ziel vollständig verstehe. In C ist eine Vereinigung eine Struktur, die dieselben Speicherorte für mehr als ein Feld verwendet. Beispielsweise:
Die
floatOrScalar
Union kann als Float oder Int verwendet werden, aber beide belegen denselben Speicherplatz. Das Ändern eines ändert das andere. Sie können dasselbe mit einer Struktur in C # erreichen:Die obige Struktur verwendet insgesamt 32 Bit anstelle von 64 Bit. Dies ist nur mit einer Struktur möglich. Ihr Beispiel oben ist eine Klasse und gibt angesichts der Art der CLR keine Garantie für die Speichereffizienz. Wenn Sie einen
Union<A, B, C>
von einem Typ in einen anderen ändern, wird der Speicher nicht unbedingt wiederverwendet. Höchstwahrscheinlich weisen Sie dem Heap einen neuen Typ zu und lassen einen anderen Zeiger im Hintergrundfeld fallenobject
. Im Gegensatz zu einer echten Union kann Ihr Ansatz tatsächlich mehr Heap-Thrashing verursachen, als Sie sonst erhalten würden, wenn Sie Ihren Union-Typ nicht verwenden würden.quelle
Dies führt zu einer Warnung, nicht zu einem Fehler. Wenn Sie suchen, dass Ihre
Is
undAs
Funktionen für die C # -Operatoren analog sind, sollten Sie sie sowieso nicht auf diese Weise einschränken.quelle
Wenn Sie mehrere Typen zulassen, können Sie keine Typensicherheit erreichen (es sei denn, die Typen sind verwandt).
Sie können und werden keine Typensicherheit erreichen, Sie können nur mit FieldOffset Byte-Wert-Sicherheit erreichen.
Es wäre viel sinnvoller, ein Generikum
ValueWrapper<T1, T2>
mitT1 ValueA
undT2 ValueB
, ...PS: Wenn ich über Typensicherheit spreche, meine ich Typensicherheit zur Kompilierungszeit.
Wenn Sie einen Code-Wrapper benötigen (Durchführen einer Geschäftslogik für Änderungen), können Sie Folgendes verwenden:
Für einen einfachen Ausweg könnten Sie verwenden (es hat Leistungsprobleme, aber es ist sehr einfach):
quelle
Hier ist mein Versuch. Es überprüft die Kompilierungszeit von Typen unter Verwendung generischer Typeinschränkungen.
Es könnte etwas hübsches gebrauchen. Insbesondere konnte ich nicht herausfinden, wie die Typparameter in As / Is / Set entfernt werden können (gibt es keine Möglichkeit, einen Typparameter anzugeben und C # den anderen berechnen zu lassen?)
quelle
Ich bin also schon oft auf dasselbe Problem gestoßen und habe gerade eine Lösung gefunden, die die gewünschte Syntax erhält (auf Kosten einer gewissen Hässlichkeit bei der Implementierung des Union-Typs).
Um es noch einmal zusammenzufassen: Wir möchten diese Art der Nutzung an der Anrufstelle.
Wir möchten jedoch, dass die folgenden Beispiele nicht kompiliert werden können, damit wir ein Mindestmaß an Typensicherheit erhalten.
Nehmen wir für zusätzliche Gutschriften auch nicht mehr Platz ein als unbedingt erforderlich.
Nach alledem ist hier meine Implementierung für zwei generische Typparameter. Die Implementierung für Typparameter für drei, vier usw. ist unkompliziert.
quelle
Und mein Versuch einer minimalen, aber erweiterbaren Lösung mit Verschachtelung vom Typ Union / Beide . Auch die Verwendung von Standardparametern in der Match-Methode aktiviert natürlich das Szenario "Entweder X oder Standard".
quelle
Sie können Ausnahmen auslösen, wenn versucht wird, auf Variablen zuzugreifen, die nicht initialisiert wurden. Wenn sie also mit einem A-Parameter erstellt wurden und später versucht wird, auf B oder C zuzugreifen, kann beispielsweise UnsupportedOperationException ausgelöst werden. Sie würden einen Getter brauchen, damit es funktioniert.
quelle
Sie können eine Pseudomuster-Übereinstimmungsfunktion exportieren, wie ich sie für den Typ "Beide" in meiner Sasa-Bibliothek verwende . Derzeit gibt es Laufzeit-Overhead, aber ich plane schließlich, eine CIL-Analyse hinzuzufügen, um alle Delegierten in eine echte case-Anweisung zu integrieren.
quelle
Es ist nicht möglich, genau die Syntax zu verwenden, die Sie verwendet haben, aber mit etwas mehr Ausführlichkeit und Kopieren / Einfügen ist es einfach, die Überlastungsauflösung für Sie zu erledigen:
Inzwischen sollte es ziemlich offensichtlich sein, wie es implementiert werden soll:
Es gibt keine Überprüfungen zum Extrahieren des Werts des falschen Typs, z.
In solchen Fällen können Sie die erforderlichen Überprüfungen hinzufügen und Ausnahmen auslösen.
quelle
Ich benutze eigene Union Type.
Betrachten Sie ein Beispiel, um es klarer zu machen.
Stellen Sie sich vor, wir haben eine Kontaktklasse:
Diese sind alle als einfache Zeichenfolgen definiert, aber sind sie wirklich nur Zeichenfolgen? Natürlich nicht. Der Name kann aus Vorname und Nachname bestehen. Oder ist eine E-Mail nur eine Reihe von Symbolen? Ich weiß, dass es zumindest @ enthalten sollte und es ist notwendig.
Lassen Sie uns das Domain-Modell verbessern
In diesen Klassen werden Validierungen während der Erstellung und wir werden schließlich gültige Modelle haben. Für Consturctor in der PersonaName-Klasse sind Vorname und Nachname gleichzeitig erforderlich. Dies bedeutet, dass es nach der Erstellung keinen ungültigen Status haben kann.
Und Kontaktklasse jeweils
In diesem Fall haben wir das gleiche Problem. Das Objekt der Kontaktklasse befindet sich möglicherweise in einem ungültigen Zustand. Ich meine, es hat vielleicht EmailAddress, aber keinen Namen
Lassen Sie es uns beheben und eine Kontaktklasse mit einem Konstruktor erstellen, für den PersonalName, EmailAddress und PostalAddress erforderlich sind:
Aber hier haben wir ein anderes Problem. Was ist, wenn die Person nur eine E-Mail-Adresse und keine Postanschrift hat?
Wenn wir dort darüber nachdenken, erkennen wir, dass es drei Möglichkeiten für einen gültigen Status des Kontaktklassenobjekts gibt:
Schreiben wir Domain-Modelle auf. Zu Beginn erstellen wir eine Kontaktinfo-Klasse, deren Status den oben genannten Fällen entspricht.
Und Kontaktklasse:
Versuchen wir es mal:
Fügen wir die Match-Methode in die ContactInfo-Klasse ein
Erstellen wir eine Hilfsklasse, damit nicht jedes Mal so viel Code geschrieben wird.
Lassen Sie uns die
ContactInfo
Klasse umschreiben :Das ist alles. Ich hoffe, dass Sie Spaß hatten.
Beispiel aus der Site F # für Spaß und Gewinn
quelle
Das C # Language Design Team diskutierte im Januar 2017 diskriminierte Gewerkschaften unter https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-01-10.md#discriminated-unions-via-closed-types
Sie können für die Funktionsanfrage unter https://github.com/dotnet/csharplang/issues/113 abstimmen
quelle