Freigeben über


Gewerkschaften

Hinweis

Dieser Artikel ist eine Featurespezifikation. Die Spezifikation dient als Designdokument für das Feature. Es enthält vorgeschlagene Spezifikationsänderungen sowie Informationen, die während des Entwurfs und der Entwicklung des Features erforderlich sind. Diese Artikel werden veröffentlicht, bis die vorgeschlagenen Spezifikationsänderungen abgeschlossen und in die aktuelle ECMA-Spezifikation aufgenommen werden.

Es kann einige Abweichungen zwischen der Featurespezifikation und der abgeschlossenen Implementierung geben. Diese Unterschiede werden in den relevanten Sprachentwurfsbesprechungen (LDM)-Notizen erfasst.

Weitere Informationen zum Einführen von Featurespezifikationen in den C#-Sprachstandard finden Sie im Artikel zu den Spezifikationen.

Champion Issue: https://github.com/dotnet/csharplang/issues/9662

Zusammenfassung

Unions ist eine Reihe von miteinander verknüpften Features, die kombiniert werden, um C#-Unterstützung für Union-Typen bereitzustellen:

  • Union-Typen: Strukturen und Klassen mit einem [Union] Attribut werden als Union-Typen erkannt und unterstützen das Union-Verhalten.
  • Falltypen: Union-Typen haben eine Reihe von Falltypen, die von Parametern für Konstruktoren und Factorymethoden angegeben werden.
  • Union-Verhalten: Union-Typen unterstützen die folgenden Union-Verhaltensweisen:
    • Union-Konvertierungen: Es gibt implizite Union-Konvertierungen von jedem Falltyp in einen Union-Typ.
    • Union-Abgleich: Musterabgleich mit Union-Werten "entwirbt" ihren Inhalt implizit und wendet stattdessen das Muster auf den zugrunde liegenden Wert an.
    • Vollständigkeit der Union: Wechselausdrücke über Union-Werte sind erschöpfend, wenn alle Falltypen abgeglichen wurden, ohne dass ein Fallbackfall erforderlich ist.
    • Union nullability: Die Nullierbarkeitsanalyse hat die Nachverfolgung des NULL-Zustands eines Unionsinhalts verbessert.
  • Union-Muster: Alle Union-Typen folgen einem grundlegenden Union-Muster, es gibt jedoch zusätzliche optionale Muster für bestimmte Szenarien.
  • Union-Deklarationen: Eine Kurzhandsyntax ermöglicht die direkte Deklaration von Union-Typen. Die Implementierung wird "opinionated" - eine Strukturdeklaration, die dem grundlegenden Union-Muster folgt, und speichert den Inhalt als einzelnes Referenzfeld.
  • Union-Schnittstellen: Einige Schnittstellen sind von der Sprache bekannt und werden bei der Umsetzung von Unionserklärungen verwendet.

Motivation

Unions sind ein langes C#-Feature, das das Ausdrücken von Werten aus einem geschlossenen Satz von Typen auf eine Weise ermöglicht, dass der Musterabgleich erschöpfend sein kann.

Durch die Trennung zwischen Unionstypen und Union-Deklarationen kann C# eine prägnante Union-Deklarationssyntax mit der Meinungssemantik haben, während gleichzeitig vorhandene Typen oder Typen mit anderen Implementierungsoptionen die Auswahl von Union-Verhaltensweisen zulassen.

Die vorgeschlagenen Gewerkschaften in C# sind Vereinigungen von Typen und nicht "diskriminiert" oder "markiert". "Diskriminierte Gewerkschaften" können in Bezug auf "Typgewerkschaften" ausgedrückt werden, indem frische Typdeklarationen als Falltypen verwendet werden. Alternativ können sie als geschlossene Hierarchie implementiert werden, bei der es sich um ein weiteres, verwandtes, bevorstehendes C#-Feature handelt, das sich auf Vollständigkeit konzentriert.

Detailliertes Design

Union-Datentypen

Jeder Klassen- oder Strukturtyp mit einem System.Runtime.CompilerServices.UnionAttribute Attribut gilt als Union-Typ:

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(Class | Struct, AllowMultiple = false)]
    public class UnionAttribute : Attribute;
}

Ein Unionstyp muss einem bestimmten Muster öffentlicher Gewerkschaftsmitglieder folgen, das entweder für den Unionstyp selbst deklariert oder an einen "Union-Mitgliedsanbieter" delegiert werden muss.

Einige Gewerkschaftsmitglieder sind obligatorisch, andere sind optional.

Ein Union-Typ verfügt über eine Reihe von Falltypen , die basierend auf den Signaturen bestimmter Gewerkschaftsmitglieder eingerichtet werden.

Auf den Inhalt eines Unionswerts kann über eine Value Eigenschaft zugegriffen werden. Bei der Sprache wird davon ausgegangen, dass Value nur der Wert eines der Groß-/Kleinschreibungstypen oder null (siehe Wohlgeformtheit) enthalten ist.

Union-Mitgliedsanbieter

Standardmäßig werden Union-Mitglieder im Union-Typ selbst gefunden. Wenn der Union-Typ jedoch direkt eine Deklaration einer Schnittstelle enthält, die aufgerufen wird IUnionMembers , fungiert die Schnittstelle als Union-Mitgliedsanbieter. In diesem Fall befinden sich die Gewerkschaftsmitglieder nur auf dem Unionsmitgliedsanbieter, nicht auf dem Unionstyp selbst.

Eine Union-Mitgliedsanbieterschnittstelle muss öffentlich sein, und der Union-Typ selbst muss sie als Schnittstelle implementieren.

Wir verwenden den Begriff union-definierenden Typ für den Typ, in dem die Union-Mitglieder gefunden werden: Der Union-Mitgliedsanbieter, sofern vorhanden, und der Union-Typ selbst.

Union-Mitglieder

Union-Mitglieder werden anhand des Namens und der Signatur nach dem typ "union-defining" nachschlagen. Sie müssen nicht direkt für den union-definierenden Typ deklariert werden, können aber geerbt werden.

Es ist ein Fehler, dass ein Gewerkschaftsmitglied nicht öffentlich ist.

Die Erstellungsmitglieder und die Value Eigenschaft sind obligatorisch und werden gemeinsam als grundlegendes Vereinigungsmuster bezeichnet.

Die HasValue Mitglieder und TryGetValue Die Mitglieder werden gemeinsam als Das Zugriffsmuster für nicht boxende Vereinigungen bezeichnet.

Die verschiedenen Gewerkschaftsmitglieder werden im Folgenden beschrieben.

Mitglieder der Union

Union Creation-Mitglieder werden verwendet, um neue Unionswerte aus einem Falltypwert zu erstellen.

Wenn der Union-definierende Typ der Union selbst ist, ist jeder Konstruktor mit einem einzelnen Parameter ein Union-Konstruktor. Die Falltypen der Union werden wie folgt als Satz von Typen identifiziert, die aus Parametertypen dieser Konstruktoren erstellt wurden:

  • Wenn der Parametertyp ein nullabler Typ ist (unabhängig davon, ob ein Wert oder ein Bezug), ist der Falltyp der zugrunde liegende Typ.
  • Andernfalls ist der Groß-/Kleinschreibungstyp der Parametertyp.
// Union constructor making `Dog` a case type
public Pet(Dog value) { ... }
// Union constructor making `int` a case type
public Union(int? value) { ... }
// Union constructor making `string` a case type
public Union(string? value) { ... }

Wenn der union-definierende Typ ein Union-Memberanbieter ist, ist jede statische Create Methode mit einem einzelnen Parameter und einem Rückgabetyp, der identitätsverwendbar in den Union-Typ selbst ist, eine Union Factory-Methode. Die Falltypen der Union werden wie folgt als Satz von Typen identifiziert, die aus Parametertypen dieser Factorymethoden erstellt wurden:

  • Wenn der Parametertyp ein nullabler Typ ist (unabhängig davon, ob ein Wert oder ein Bezug), ist der Falltyp der zugrunde liegende Typ.
  • Andernfalls ist der Groß-/Kleinschreibungstyp der Parametertyp.
// Union factory method making `Cat` a case type
public static Pet Create(Cat value) { ... }
// Union factory method making `int` a case type
public static Union Create(int? value) { ... }
// Union factory method making `string` a case type
public static Union Create(string? value) { ... }

Union-Konstruktoren und Union Factory-Methoden werden gemeinsam als Mitglieder der Vereinigungserstellung bezeichnet.

Der einzelne Parameter eines Union Creation-Members muss ein By-Value oder in Parameter sein.

Ein Union-Typ muss mindestens ein Unionserstellungsmitglied und daher mindestens einen Falltyp aufweisen.

Value-Eigenschaft

Die Value Eigenschaft ermöglicht den Zugriff auf den in einer Union enthaltenen Wert, unabhängig vom Falltyp.

Jeder uniondefinierungstyp muss eine Value Eigenschaft vom Typ object? oder object. Die Eigenschaft muss über einen get Accessor verfügen und optional über einen init Accessor verfügen set , der eine Barrierefreiheit aufweisen kann und vom Compiler nicht verwendet wird.

// Union 'Value' property
public object? Value { get; }

Elemente ohne Boxzugriff

Ein Union-Typ kann auch das Nicht-Boxing Union-Zugriffsmuster implementieren, das stark typierten bedingten Zugriff auf jeden Falltyp sowie eine Möglichkeit zum Überprüfen auf NULL zulässt.

Auf diese Weise kann der Compiler musterabgleich effizienter implementieren, wenn Falltypen Werttypen sind und als solche in der Union gespeichert werden.

Die Elemente des Nicht-Boxing-Zugriffs sind:

  • Eine HasValue Eigenschaft vom Typ bool mit einem öffentlichen get Accessor. Sie kann optional über einen Accessor oder set einen init Accessor verfügen, der von jeder Barrierefreiheit sein kann und nicht vom Compiler verwendet wird.
  • Eine TryGetValue Methode für jeden Falltyp. Die Methode gibt einen einzelnen Out-Parameter eines Typs zurück bool , der für den Falltyp identitätsverwandelt ist.
// Non-boxing access members
public bool HasValue { get { ... } }
public bool TryGetValue(out Dog value) { ... }

HasValue wird erwartet, dass "true" zurückgegeben wird, wenn die Union Value nicht null ist.

TryGetValue wird erwartet, dass "true" zurückgegeben wird, wenn die Union Value den angegebenen Falltyp aufweist, und in diesem Fall diesen Wert im Ausgabeparameter der Methode liefern.

Wohlgeformtheit

Die Sprache und der Compiler machen eine Reihe von Verhaltensannahmen zu Union-Typen. Wenn ein Typ als Unionstyp qualifiziert ist, diese Annahmen jedoch nicht erfüllt, funktioniert das Union-Verhalten möglicherweise nicht wie erwartet.

  • Soundness: Die Value Eigenschaft wird immer als NULL oder als Wert eines Falltyps ausgewertet. Das gilt auch für den Standardwert des Union-Typs.
  • Stabilität: Wenn ein Union-Wert aus einem Falltyp erstellt wird, stimmt die Eigenschaft mit diesem Value Falltyp oder null überein. Wenn ein Unionswert aus einem null Wert erstellt wird, lautet nulldie Value Eigenschaft .
  • Äquivalenz beim Erstellen: Wenn ein Wert implizit in zwei verschiedene Falltypen konvertierbar ist, weist das Erstellungselement für einen dieser Falltypen dasselbe feststellbare Verhalten auf, wenn er mit diesem Wert aufgerufen wird.
  • Konsistenz des Zugriffsmusters: Das Verhalten der HasValue Zugriffsmember und TryGetValue nicht boxenden Elemente( falls vorhanden) entspricht einer direkten Überprüfung auf die Value Eigenschaft.

Beispiele für Union-Typen

Pet implementiert das grundlegende Union-Muster für den Union-Typ selbst:

[Union] public record struct Pet
{
    // Creation members = case types are 'Dog' and 'Cat'
    public Pet(Dog value) => Value = value;
    public Pet(Cat value) => Value = value;

    // 'Value' property
    public object? Value { get; }
}

IntOrBool implementiert das Nicht-Boxing-Zugriffsmuster für den Union-Typ selbst:

public record struct IntOrBool
{
    private bool _isBool;
    private int _value;

    public IntOrBool(int value) => (_isBool, _value) = (false, value);
    public IntOrBool(bool value) => (_isBool, _value) = (true, value ? 1 : 0);

    public object Value => _isBool ? _value is 1 : _value;

    public bool HasValue => true;
    public bool TryGetValue(out int value)
    {
        value = _value;
        return !_isBool;
    }
    public bool TryGetValue(out bool value)
    {
        value = _isBool && _value is 1;
        return _isBool;
    }
}

Hinweis: Dies ist nur ein Beispiel dafür, wie das Zugriffsmuster ohne Boxen implementiert werden kann. Der Benutzercode kann den Inhalt beliebig speichern. Insbesondere verhindert sie nicht, dass die Implementierung boxen kann! Der non-boxing Name bezieht sich darauf, dass die Musterabgleichsimplementierung des Compilers den Zugriff auf jeden Falltyp auf stark typierte Weise ermöglicht, im Gegensatz zur object?Eigenschaft "-typed Value ".

Result<T> implementiert das grundlegende Muster über einen Union-Mitgliedsanbieter:

public record class Result<T> : Result<T>.IUnionMembers
{
    object? _value;

    public interface IUnionMembers
    {
        public static Result<T> Create(T value) => new() { _value = value };
        public static Result<T> Create(Exception value) => new() { _value = value };

        public object? Value { get; }
    }

    object? IUnionMembers.Value => _value;
}

Union-Verhalten

Das Union-Verhalten wird im Allgemeinen mithilfe des grundlegenden Union-Musters implementiert. Wenn die Union das Nicht-Boxing-Zugriffsmuster anbietet, wird der Union-Musterabgleich bevorzugt verwendet.

Umwandlungen der Union

Eine Union-Konvertierung konvertiert implizit in einen Union-Typ aus den einzelnen Falltypen. Insbesondere gibt es eine Union-Konvertierung in einen Union-Typ U aus einem Typ oder Ausdruck E , wenn eine implizite Standardkonvertierung von E einem Typ in einen Typ C vorhanden ist und C ein Parametertyp eines Union-Erstellungsmitglieds ist U. Wenn der Union-Typ U eine Struktur ist, gibt es eine Union-Konvertierung zum Typ U? aus einem Typ oder Ausdruck E , wenn eine implizite Standardkonvertierung von E einem Typ in einen Typ C vorhanden ist und C ein Parametertyp eines Union-Erstellungsmitglieds ist U.

Eine Union-Konvertierung ist nicht selbst eine implizite Standardkonvertierung. Sie kann daher nicht an einer benutzerdefinierten impliziten Konvertierung oder einer anderen Union-Konvertierung teilnehmen.

Es gibt keine expliziten Unionskonvertierungen, die über die impliziten Union-Konvertierungen hinausgehen. Selbst wenn es eine explizite Konvertierung von E einem Union-Falltyp Cgibt, bedeutet dies nicht, dass es eine explizite Konvertierung von E diesem Union-Typ gibt.

Eine Vereinigungskonvertierung wird durch Aufrufen des Gründungsmitglieds der Union ausgeführt:

Pet pet = dog;
// becomes
Pet pet = new Pet(dog);
// and
Result<string> result = "Hello"
//becomes
Result<string> result = Result<string>.IUnionMembers.Create("Hello");

Es ist ein Fehler, wenn die Überlastungsauflösung kein einziges bestes Kandidatenmitglied findet oder wenn dieses Mitglied nicht eines der Unionsmitglieder des Gewerkschaftstyps ist.

Union-Konvertierung ist nur eine weitere "Form" einer impliziten benutzerdefinierten Konvertierung. Ein anwendbarer benutzerdefinierter Konvertierungsoperator "Shadows" union conversion.

Der Grund für diese Entscheidung:

Wenn jemand einen benutzerdefinierten Operator geschrieben hat, sollte er Priorität erhalten. Mit anderen Worten: Wenn der Benutzer tatsächlich seinen eigenen Betreiber geschrieben hat, möchte er, dass wir ihn nennen. Vorhandene Typen mit Konvertierungsoperatoren, die in Union-Typen umgewandelt wurden, funktionieren weiterhin auf die gleiche Weise wie der vorhandene Code, der die Operatoren heute verwendet.

Im folgenden Beispiel hat eine implizite benutzerdefinierte Konvertierung Vorrang vor einer Union-Konvertierung.

struct S1 : System.Runtime.CompilerServices.IUnion
{
    public S1(int x) => ...
    public S1(string x) => ...
    object System.Runtime.CompilerServices.IUnion.Value => ...
    public static implicit operator S1(int x) => ...
}

class Program
{
    static S1 Test1() => 10; // implicit operator S1(int x) is used
    static S1 Test2() => (S1)20; // implicit operator S1(int x) is used
}

Wenn im folgenden Beispiel explizite Umwandlungen im Code verwendet werden, hat eine explizite benutzerdefinierte Konvertierung Vorrang vor einer Union-Konvertierung. Wenn jedoch kein expliziter Umwandlungscode vorhanden ist, wird eine Union-Konvertierung verwendet, da die explizite benutzerdefinierte Konvertierung nicht anwendbar ist.

struct S2 : System.Runtime.CompilerServices.IUnion
{
    public S2(int x) => ...
    public S2(string x) => ...
    object System.Runtime.CompilerServices.IUnion.Value => ...
    public static explicit operator S2(int x) => ...
}

class Program
{
    static S2 Test3() => 10; // Union conversion S2.S2(int) is used
    static S2 Test4() => (S2)20; // explicit operator S2(int x)
}

Union-Abgleich

Wenn der Eingehende Wert eines Musters einen Union-Typ oder einen Nullwert eines Union-Typs aufweist, kann der nullwerteable Wert und der Inhalt des zugrunde liegenden Unionswerts je nach Muster "entwrappt" sein.

Für bedingungslose _ Und var Muster wird das Muster auf den eingehenden Wert selbst angewendet. Beispiel:

if (GetPet() is var pet) { ... } // 'pet' is the union value returned from `GetPet`

Alle anderen Muster werden jedoch implizit auf die Eigenschaft der zugrunde liegenden Union Value angewendet:

if (GetPet() is Dog dog) { ... }   // 'Dog dog' is applied to 'GetPet().Value'
if (GetPet() is null) { ... }      // 'null' is applied to 'GetPet().Value'
if (GetPet() is { } value) { ... } // '{ } value' is applied to 'GetPet().Value'

Bei logischen Mustern wird diese Regel einzeln auf die Verzweigungen angewendet, wobei berücksichtigt wird, dass sich die linke Verzweigung eines and Musters auf den eingehenden Typ der rechten Verzweigung auswirken kann:

GetPet() switch
{
    var pet and not null   => ... // 'var pet' applies to the incoming 'Pet' and 'not null' to its 'Value'
    not null and var value => ... // 'not null' applies to the 'Value' as does 'var value' because of the 
                                  // left branch changing the incoming type to `object?`.
}

Hinweis: Diese Regel bedeutet, dass dies GetPet() is Pet pet wahrscheinlich nicht erfolgreich ist, wie Pet auf den Inhalt angewendet wird, nicht auf die Pet Vereinigung selbst.

Hinweis: Der Grund für die unterschiedliche Behandlung von bedingungslosen var Mustern (sowie _, die im Wesentlichen eine Abkürzung für var _) ist, ist eine Annahme, dass ihre Verwendung qualitativ von anderen Mustern unterscheidet. var Muster werden einfach verwendet, um den Wert zu benennen, mit dem abgeglichen wird, häufig in geschachtelten Mustern, z PetOwner{ Pet: var pet }. B. . . Hier dient pet die hilfreiche Semantik dazu, den Union-Typ Petbeizubehalten, anstatt dass die Value Eigenschaft auf einen nutzlosen object? Typ abgeleitet wird.

Wenn es sich bei dem eingehenden Wert um einen Klassentyp handelt, wird das null Muster unabhängig davon erfolgreich ausgeführt, ob der Unionswert selbst oder null sein enthaltener Wert lautet null:

if (result is null) { ... } // if (result == null || result.Value == null)

Andere Zuordnungsabgleichsmuster werden nur erfolgreich ausgeführt, wenn der Unionswert selbst nicht nullist.

if (result is 1) { ... } // if (result != null && result.Value is 1)

Wenn es sich bei dem eingehenden Wert um einen Nullwerttyp (Umschließen eines Strukturunionstyps) handelt, wird das null Muster unabhängig davon erfolgreich ausgeführt, ob der eingehende Wert selbst oder null sein enthaltener Wert lautet null:

if (result is null) { ... } // if (result.HasValue == false || result.GetValueOrDefault().Value == null)

Andere Zuordnungsabgleichsmuster werden nur erfolgreich ausgeführt, wenn der eingehende Wert selbst nicht nullist.

if (result is 1) { ... } // if (result.HasValue && result.GetValueOrDefault().Value is 1)

Der Compiler bevorzugt das Implementieren des Musterverhaltens mithilfe von Membern, die durch das Nicht-Boxing-Zugriffsmuster vorgeschrieben werden. Obwohl es frei ist, eine Optimierung innerhalb der Grenzen der Wohlformheitsregeln auszuführen, sind die folgenden Mindestsätze garantiert:

  • Für ein Muster, das die Überprüfung eines bestimmten Typs Timpliziert, wenn eine TryGetValue(S value) Methode verfügbar ist, und es eine Konvertierung von Identität oder impliziter Verweis/Boxing von T zu S" gibt, wird diese Methode verwendet, um den Wert abzurufen. Das Muster wird dann auf diesen Wert angewendet. Wenn mehr als eine solche Methode vorhanden ist, wird die Konvertierung von T in S eine Boxumwandlung bei Bedarf nicht bevorzugt. Wenn immer noch mehrere Methoden vorhanden sind, wird eine methode auf implementierungsdefinierte Weise ausgewählt.
  • Andernfalls wird bei einem Muster überprüft null, auf das überprüft wird, ob eine HasValue Eigenschaft verfügbar ist, diese Eigenschaft verwendet wird, um zu überprüfen, ob der Unionswert null ist.
  • Andernfalls wird das Muster auf das Ergebnis des Zugriffs auf die IUnion.Value Eigenschaft in der eingehenden Union angewendet.

Der is-type-Operator , der auf einen Union-Typ angewendet wird, hat dieselbe Bedeutung wie ein Typmuster, das auf den Union-Typ angewendet wird.

Erschöpfende Union

Es wird angenommen, dass ein Union-Typ von den Falltypen "erschöpft" ist. Dies bedeutet, dass ein switch Ausdruck erschöpfend ist, wenn er alle Falltypen einer Union behandelt:

var name = pet switch
{
    Dog dog => ...,
    Cat cat => ...,
    // No warning about non-exhaustive switch
};

NULL-Zulässigkeit

Der NULL-Zustand der Eigenschaft einer Union Value wird wie jede andere Eigenschaft mit den folgenden Änderungen nachverfolgt:

  • Wenn ein Unionserstellungsmitglied aufgerufen wird (explizit oder durch eine Unionumwandlung), erhält die neue Union den NULL-Status des eingehenden Werts Value .
  • Wenn das Nicht-Boxing-Zugriffsmuster HasValue oder TryGetValue(...) verwendet wird, um den Inhalt eines Union-Typs (explizit oder über Musterabgleich) abzufragen, wirkt es sich auf Valueden Nullierbarkeitszustand aus, wie wenn Value es direkt überprüft wurde: Der NULL-Zustand Value wird in der true Verzweigung "nicht null".

Selbst wenn ein Union-Switch andernfalls erschöpfend ist, wird eine Warnung auf unbehandelte Null angegeben, wenn der Null-Zustand der Eigenschaft der eingehenden Union Value "vielleicht null" lautet.

Pet pet = GetNullableDog(); // 'pet.Value' is "maybe null"
var value = pet switch
{
    Dog dog => ...,
    Cat cat => ...,
    // Warning: 'null' not handled
}

Union-Schnittstellen

Die folgenden Schnittstellen werden von der Sprache bei der Implementierung von Union-Features verwendet.

Union-Zugriffsschnittstelle

Die IUnion Schnittstelle kennzeichnet einen Typ zur Kompilierungszeit als Union-Typ und ermöglicht den Zugriff auf Union-Inhalte zur Laufzeit.

public interface IUnion
{
    // The value of the union or null
    object? Value { get; }
}

Vom Compiler generierte Unions implementieren diese Schnittstelle.

Beispielverwendung:

if (value is IUnion { Value: null }) { ... }

Erklärungen der Union

Union-Erklärungen sind eine prägnante und meinungsierte Möglichkeit, Union-Typen in C# zu deklarieren. Sie deklarieren eine Struktur, die einen einzelnen Objektverweis zum Speichern des ValueObjekts verwendet, was bedeutet:

  • Boxen: Alle Werttypen zwischen ihren Falltypen werden beim Eintrag eingeboxt.
  • Komprimität: Unionswerte enthalten nur ein einzelnes Feld.

Die Absicht besteht darin, dass gewerkschaftsliche Erklärungen die überwiegende Mehrheit der Anwendungsfälle ziemlich gut abdecken. Die beiden Hauptgründe für das Codieren bestimmter Union-Typen statt der Verwendung von Union-Deklarationen sind zu erwarten:

  • Anpassen vorhandener Typen an die Unionsmuster, um Vereinigungsverhalten zu erzielen.
  • Implementierung einer anderen Speicherstrategie aus z.B. Effizienz- oder Interoperabilitätsgründen.

Syntax

Eine Union-Deklaration hat einen Namen und eine Liste von Union-Konstruktorentypen .

union_declaration
    : attributes? struct_modifier* 'partial'? 'union' identifier type_parameter_list?
      '(' type (',' type)* ')'  struct_interfaces? type_parameter_constraints_clause* 
      (`{` struct_member_declaration* `}` | ';')
    ;

Zusätzlich zu den Einschränkungen der Strukturmitglieder (§16.3) gilt Folgendes für Gewerkschaftsmitglieder:

  • Instanzfelder, automatische Eigenschaften oder feldähnliche Ereignisse sind nicht zulässig.
  • Explizit deklarierte öffentliche Konstruktoren mit einem einzigen Parameter sind nicht zulässig.
  • Explizit deklarierte Konstruktoren müssen einen this(...) Initialisierer verwenden, um (direkt oder indirekt) einen der generierten Konstruktoren zu delegieren.

Die Union-Konstruktoren können alle Typen sein, die in objectkonvertiert werden, z. B. Schnittstellen, Typparameter, nullfähige Typen und andere Vereinigungen. Es ist in Ordnung, dass sich daraus resultierende Fälle überlappen und gewerkschaften geschachtelt oder null sind.

Beispiele:

// Union of existing types
public union Pet(Cat, Dog, Bird);

// Union with function member
public union OneOrMore<T>(T, IEnumerable<T>)
{
    public IEnumerable<T> AsEnumerable() => Value switch
    {
        IEnumerable<T> list => list,
        T value => [value],
    }
}

// "Discriminated" union with freshly declared case types
public record class None();
public record class Some<T>(T value);
public union Option<T>(None, Some<T>);

#### Lowering

A union declaration is lowered to a struct declaration with

* the same attributes, modifiers, name, type parameters and constraints,
* implicit implementations of `IUnion`,
* a `public object? Value { get; }` auto-property,
* a public constructor for each *union constructor* type,
* any members in the union declaration's body.

It is an error for user-declared members to conflict with generated members.

Example:

``` c#
public union Pet(Cat, Dog){ ... }

Wird auf Folgendes reduziert:

[Union] public struct Pet : IUnion
{
    public Pet(Cat value) => Value = value;
    public Pet(Dog value) => Value = value;
    
    public object? Value { get; }
    
    ... // original body
}

Offene Fragen

[Behoben] Gibt es eine Unionsdeklaration als Datensatz?

Eine Unionserklärung wird auf eine Datensatzstruktur herabgesetzt.

Ich denke, dass dieses Standardverhalten unnötig ist und angesichts der Tatsache, dass es nicht konfigurierbar ist, die Nutzungsszenarien erheblich einschränken wird. Datensätze generieren eine Menge Code, der nicht verwendet wird oder nicht mit bestimmten Anforderungen übereinstimmt. Beispielsweise sind Datensätze aufgrund dieses Codes ziemlich verboten in der Codebasis des Compilers. Ich denke, es wäre besser, die Standardeinstellung zu ändern:

  • Standardmäßig deklariert eine Unionserklärung eine normale Struktur mit nur unionsspezifischen Mitgliedern.
  • Ein Benutzer kann eine Datensatzunion deklarieren: record union U(E1, ...) ...

Auflösung: Eine Unionserklärung ist eine einfache Struktur, keine Datensatzstruktur. Dies record union ... wird nicht unterstützt.

[Behoben] Syntax der Union-Deklaration

Anscheinend ist die vorgeschlagene Syntax unvollständig oder unnötig begrenzt. So sieht es z. B. aus, als sei eine Basisklausel nicht zulässig. Ich kann mir jedoch leicht vorstellen, dass beispielsweise eine Schnittstelle implementiert werden muss. Ich denke, dass abgesehen von der Elementtypenliste die Syntax mit der regulären struct/record struct Deklaration übereinstimmen sollte, in der das struct Schlüsselwort durch union das Schlüsselwort ersetzt wird.

Auflösung: Die Einschränkung wird entfernt.

[Behoben] Mitglieder der Unionserklärung

Instanzfelder, automatische Eigenschaften oder feldähnliche Ereignisse sind nicht zulässig.

Das fühlt sich willkürlich und absolut unnötig an.

Auflösung: Die Einschränkung wird beibehalten.

[Behoben] Nullwertetypen als Union-Falltypen

Die Falltypen der Union werden als Satz von Parametertypen aus diesen Konstruktoren identifiziert. Die Falltypen der Union werden als Satz von Parametertypen aus diesen Factorymethoden identifiziert.

Gleichzeitig:

Eine TryGetValue Methode für jeden Falltyp. Die Methode gibt einen einzelnen Out-Parameter eines Typs zurück bool , der dem jeweiligen Falltyp auf folgende Weise entspricht:

  • Wenn der Falltyp ein Nullwerttyp ist, sollte der Typ des Parameters identitätsverwandelt in den zugrunde liegenden Typ sein.
  • Andernfalls sollte der Typ identitätsverwendbar in den Falltyp sein.

Gibt es einen Vorteil, dass ein Nullwerttyp zwischen den Falltypen vorhanden ist, insbesondere, dass ein Typmuster keinen Nullwerttyp als Zieltyp verwenden kann? Es scheint, als könnten wir einfach sagen, dass, wenn der Parametertyp des Konstruktors ein Nullwerttyp ist, der entsprechende Groß-/Kleinschreibungstyp der zugrunde liegende Typ ist. Dann benötigen wir diese zusätzliche Klausel für die TryGetValue Methode nicht, alle ausgabeparameter sind Falltypen.

Auflösung: Der Vorschlag wird genehmigt.

[Behoben] Standardmäßiger Nullable-Zustand der Value Eigenschaft

Bei Union-Typen, bei denen keines der Groß-/Kleinschreibungstypen nullfähig ist, lautet der Standardzustand Value "nicht null" und nicht "vielleicht null".

Mit dem neuen Design, bei dem Value die Eigenschaft nicht in einer allgemeinen Schnittstelle definiert ist, sondern eine API ist, die speziell zum deklarierten Typ gehört, fühlt sich die oben zitierte Regel wie über engineering an. Darüber hinaus erzwingt die Regel, dass Verbraucher nullwerte Typen in Situationen verwenden, in denen andernfalls keine nullfähigen Typen verwendet werden.

Betrachten Sie z. B. die folgende Unionserklärung:

union U1(int, bool, DateTime);

Gemäß der zitierten Regel lautet der Standardzustand Value "nicht null". Das stimmt jedoch nicht mit dem Verhalten des Typs überein, default(U1).Value lautet null. Um das Verhalten neu auszurichten, wird der Verbraucher gezwungen, mindestens einen Falltyp null zu machen. Etwas wie:

union U1(int?, bool, DateTime);

Aber das ist wahrscheinlich unerwünschte, Verbraucher möchten möglicherweise keine explizite Erstellung mit int? Wert zulassen.

Vorschlag: Entfernen Sie die an zitierte Regel, nullfähige Analyse sollte Anmerkungen aus der Value Eigenschaft verwenden, um die Standard-Null-Lesbarkeit abzuleiten.

Auflösung: Der Vorschlag wird genehmigt.

[Behoben] Union-Abgleich für Nullwerte eines Union-Werttyps

Wenn der eingehende Wert eines Musters von einem Union-Typ ist, kann der Inhalt des Unionswerts je nach Muster "entwrappt" sein.

Sollten wir diese Regel auf Szenarien erweitern, wenn der eingehende Wert eines Musters von einem Nullable<union type>?

Betrachten Sie das folgende Szenario:

    static bool Test1(StructUnion? u)
    {
        return u is 1;
    }   

    static bool Test2(ClassUnion? u)
    {
        return u is 1;
    }   

Die Bedeutung von u is 1 "Test1" und "Test2" unterscheidet sich sehr. In Test1 ist es kein Vereinigungsabgleich, in Test2 ist es. Vielleicht sollte "Vereinigungsabgleich" als Musterabgleich in anderen Situationen "graben" Nullable<T> .

Wenn wir damit fortfahren, sollte das Zuordnungsabgleichsmuster nullNullable<union type> gegen Klassen funktionieren. D.h. das Muster ist true, wenn (!nullableValue.HasValue || nullableValue.Value.Value is null).

Auflösung: Der Vorschlag wird genehmigt.

Was ist mit "schlechten" APIs zu tun?

Was sollten Compiler mit Union-Abgleichs-APIs tun, die wie eine Übereinstimmung aussehen, andernfalls aber "schlecht"? Der Compiler findet beispielsweise TryGetValue/HasValue mit übereinstimmenden Signaturen, ist aber "schlecht", da ein erforderlicher benutzerdefinierter Modifizierer oder ein unbekanntes Feature usw. erforderlich ist. Sollte der Compiler die API im Hintergrund ignorieren oder einen Fehler melden? Ähnlich ist die API möglicherweise als veraltet/experimentell gekennzeichnet. Sollten Compiler Diagnosen melden, die API im Hintergrund verwenden oder die API nicht im Hintergrund verwenden?

Was ist, wenn Typen für die Union-Deklaration fehlen

Was geschieht, wenn UnionAttribute, IUnion oder IUnion<TUnion> fehlen? Fehler? Synthetisieren? Etwas anderes?

[Behoben] Design der generischen IUnion-Schnittstelle

Argumente wurden vorgenommen, die IUnion<TUnion> nicht von IUnion ihrem Typparameter erben oder einschränken dürfen.IUnion<TUnion> Wir sollten uns überarbeiten.

Auflösung: Die IUnion<TUnion> Schnittstelle wird vorerst entfernt.

[Behoben] Nullwertetypen als Falltypen und deren Interaktion mit TryGetValue

Die obigen Regeln geben an, dass bei einem Falltyp ein Nullwerttyp ist, der in einer entsprechenden TryGetValue Methode verwendete Parametertyp der zugrunde liegende Typ sein sollte. Dies ist motiviert durch die Tatsache, dass ein null Wert niemals durch diese Methode erzielt werden würde. Auf der Verbrauchsseite ist ein Nullwerttyp nicht als Typmuster zulässig, während eine Übereinstimmung mit dem zugrunde liegenden Typ einem Aufruf dieser Methode zugeordnet werden kann.

Wir sollten bestätigen, dass wir mit dieser Entwrappung einverstanden sind.

Auflösung: Vereinbart/bestätigt

Das Nicht-Box-Union-Zugriffsmuster

Müssen genaue Regeln für die Suche nach geeigneten HasValue und TryGetValue APIs angeben. Ist die Vererbung beteiligt? Ist Lese-/Schreibzugriff HasValue eine akzeptable Übereinstimmung? usw.

[Behoben] TryGetValue Übereinstimmende Konvertierungen

Der Abschnitt "Union Matching" sagt:

Für ein Muster, das die Überprüfung auf einen bestimmten Typ Timpliziert, wenn eine TryGetValue(S value) Methode verfügbar ist, und es eine implizite Konvertierung von T zu S, dann wird diese Methode verwendet, um den Wert abzurufen.

Ist der Satz impliziter Konvertierungen auf irgendeine Weise eingeschränkt? Sind beispielsweise benutzerdefinierte Konvertierungen zulässig? Was ist mit Tupelkonvertierungen und anderen nicht so trivialen Konvertierungen? Einige davon sind sogar Standardkonvertierungen.

Ist der Satz von TryGetValue Methoden auf andere Weise eingeschränkt? Der Abschnitt "Union Patterns" impliziert beispielsweise, dass nur Methoden mit einem Parametertyp berücksichtigt werden, der einem Falltyp entspricht:

eine public bool TryGetValue(out T value) Methode für jeden Falltyp T.

Es wäre gut, eine explizite Antwort zu haben.

Auflösung: Es werden nur implizite Identitäten oder Verweise oder Boxumwandlungen berücksichtigt.

TryGetValue und nullable Analyse

Wenn das Nicht-Boxing-Zugriffsmuster HasValue oder TryGetValue(...) verwendet wird, um den Inhalt eines Union-Typs (explizit oder über Musterabgleich) abzufragen, wirkt es sich auf Valueden Nullierbarkeitszustand aus, wie wenn Value es direkt überprüft wurde: Der NULL-Zustand Value wird in der true Verzweigung "nicht null".

Ist der Satz von TryGetValue Methoden auf irgendeine Weise eingeschränkt? Der Abschnitt "Union Patterns" impliziert beispielsweise, dass nur Methoden mit einem Parametertyp berücksichtigt werden, der einem Falltyp entspricht:

eine public bool TryGetValue(out T value) Methode für jeden Falltyp T.

Es wäre gut, eine explizite Antwort zu haben.

Klarstellen von Regeln für default Werte von Struktur-Union-Typen

Hinweis: Die unten erwähnte Standardregel zur Nullierbarkeit wurde entfernt.

Hinweis: Die unten genannten Regeln für "Standardformheit" wurden entfernt. Wir sollten bestätigen, dass dies unser Wunsch ist.

Der Abschnitt "Nullierbarkeit" sagt:

Bei Union-Typen, bei denen keines der Groß-/Kleinschreibungstypen nullfähig ist, lautet der Standardzustand Value "nicht null" und nicht "vielleicht null".

Da für das folgende Beispiel die aktuelle Implementierung als "nicht null" betrachtet wird Values2 :

S2 s2 = default;

struct S2 : System.Runtime.CompilerServices.IUnion
{
    public S2(int x) => throw null!;
    public S2(bool x) => throw null!;
    object? System.Runtime.CompilerServices.IUnion.Value => throw null!;
}

Gleichzeitig sagt der Abschnitt "Well-formedness ":

  • Standardwert: Wenn ein Union-Typ ein Werttyp ist, hat null er den Standardwert als wert Value.
  • Standardkonstruktor: Wenn ein Union-Typ einen Null-Konstruktor (kein Argument) aufweist, weist null die resultierende Union als Konstruktor auf Value.

Eine Implementierung wie dies wird im Widerspruch zu nullablem Analyseverhalten für das obige Beispiel stehen.

Sollten die Wohlformheitsregeln angepasst werden oder sollte der Valuedefault Zustand "vielleicht null" sein? Wenn letzteres initialisiert S2 s2 = default; werden soll, sollte eine Nullbarkeitswarnung erzeugt werden?

Vergewissern Sie sich, dass ein Typparameter niemals ein Union-Typ ist, auch wenn er auf einen parameter beschränkt ist.

class C1 : System.Runtime.CompilerServices.IUnion
{
    private readonly object _value;
    public C1(int x) { _value = x; }
    public C1(string x) { _value = x; }
    object System.Runtime.CompilerServices.IUnion.Value => _value;
}

class Program
{
    static bool Test1<T>(T u) where T : C1
    {
        return u is int; // Not a union matching
    }   

    static bool Test2<T>(T u) where T : C1
    {
        return u is string; // Not a union matching
    }   
}

Sollten die Attribute nach der Bedingung die Standardnullierbarkeit einer Union-Instanz beeinflussen?

Hinweis: Die unten erwähnte Standardregel zur Nullierbarkeit wurde entfernt. Und wir ziehen die Standardmäßige Nullierbarkeit von Value Eigenschaften nicht mehr von Union Creation-Methoden ab. Daher ist die Frage veraltet/nicht mehr auf den aktuellen Entwurf anwendbar.

Bei Union-Typen, bei denen keines der Groß-/Kleinschreibungstypen nullfähig ist, lautet der Standardzustand Value "nicht null" und nicht "vielleicht null".

Ist die Warnung im folgenden Szenario erwartet

#nullable enable

struct S1 : System.Runtime.CompilerServices.IUnion
{
    public S1(int x) => throw null!;
    public S1([System.Diagnostics.CodeAnalysis.NotNull] bool? x) => throw null!;
    object? System.Runtime.CompilerServices.IUnion.Value => throw null!;
}
class Program
{
    static void Test2(S1 s)
    {
       // warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive).
       //                 For example, the pattern 'null' is not covered.
        _ = s switch { int => 1, bool => 3 }; // 
    } 
}

Umwandlungen der Union

[Behoben] Wo gehören sie zu anderen Konvertierungen prioritätsmäßig?

Union-Konvertierungen wirken wie eine andere Form einer benutzerdefinierten Konvertierung. Daher klassifiziert die aktuelle Implementierung sie direkt nach einem fehlgeschlagenen Versuch, eine implizite benutzerdefinierte Konvertierung zu klassifizieren, und im Falle des Vorhandenseins wird nur eine andere Form einer benutzerdefinierten Konvertierung behandelt. Dies hat die folgenden Folgen:

  • Eine implizite benutzerdefinierte Konvertierung hat Vorrang vor einer Union-Konvertierung.
  • Wenn eine explizite Umwandlung im Code verwendet wird, hat eine explizite benutzerdefinierte Konvertierung Vorrang vor einer Union-Konvertierung.
  • Wenn kein expliziter Umwandlungscode vorhanden ist, hat eine Union-Konvertierung Vorrang vor einer expliziten benutzerdefinierten Konvertierung.
struct S1 : System.Runtime.CompilerServices.IUnion
{
    public S1(int x) => ...
    public S1(string x) => ...
    object System.Runtime.CompilerServices.IUnion.Value => ...
    public static implicit operator S1(int x) => ...
}

struct S2 : System.Runtime.CompilerServices.IUnion
{
    public S2(int x) => ...
    public S2(string x) => ...
    object System.Runtime.CompilerServices.IUnion.Value => ...
    public static explicit operator S2(int x) => ...
}

class Program
{
    static S1 Test1() => 10; // implicit operator S1(int x) is used
    static S1 Test2() => (S1)20; // implicit operator S1(int x) is used
    static S2 Test3() => 10; // Union conversion S2.S2(int) is used
    static S2 Test4() => (S2)20; // explicit operator S2(int x)
}

Müssen Sie bestätigen, dass dies das Verhalten ist, das wir mögen. Andernfalls sollten die Konvertierungsregeln geklärt werden.

Lösung:

Genehmigt von der Arbeitsgruppe.

[Behoben] Ref-ness des Konstruktorparameters

Die Sprache lässt derzeit nur Nachwert und in Parameter für benutzerdefinierte Konvertierungsoperatoren zu. Es fühlt sich an, dass auch Gründe für diese Einschränkung auf Konstruktoren anwendbar sind, die für Vereinigungskonvertierungen geeignet sind.

Vorschlag:

Passen Sie die Definition eines case type constructor abschnitts Union types oben an:

-For each public constructor with exactly one parameter, the type of that parameter is considered a *case type* of the union type.
+For each public constructor with exactly one **by-value or `in`** parameter, the type of that parameter is considered a *case type* of the union type.

Lösung:

Genehmigt von der Arbeitsgruppe für jetzt. Es kann jedoch sein, dass die Gruppe von Falltypkonstruktoren und die Gruppe von Konstruktoren, die für Union-Typkonvertierungen geeignet sind, "aufteilen".

[Behoben] Nullfähige Konvertierungen

Der Abschnitt "Nullable Conversions" listet explizit Konvertierungen auf, die als zugrunde liegender Wert verwendet werden können. Die aktuelle Spezifikation schlägt keine Anpassungen für diese Liste vor. Dies führt zu einem Fehler für das folgende Szenario:

struct S1 : System.Runtime.CompilerServices.IUnion
{
    public S1(int x) => throw null;
    public S1(string x) => throw null;
    object System.Runtime.CompilerServices.IUnion.Value => throw null;
}

class Program
{
    static S1? Test1(int x)
    {
        return x; // error CS0029: Cannot implicitly convert type 'int' to 'S1?'
    }   
}

Vorschlag:

Passen Sie die Spezifikation an, um eine implizite nullable Konvertierung von S einer Union-Konvertierung in T? eine Union-Konvertierung zu unterstützen. Angenommen, T ein Union-Typ gibt es eine implizite Konvertierung in einen Typ aus einem Typ T? oder Ausdruck E , wenn eine Union-Konvertierung von E einem Typ in einen Typ C vorhanden ist und C ein Falltyp von T. Beachten Sie, dass es keine Anforderung gibt, dass E es sich um einen Nicht-Null-Werttyp handelt. Die Konvertierung wird als zugrunde liegende Union-Konvertierung von S zu T gefolgt von einem Umbruch von T zu T?

Lösung:

Gebilligt.

[Behoben] Aufgehobene Konvertierungen

Möchten wir den Abschnitt "Lifted Conversions " anpassen, um aufgehobene Vereinigungskonvertierungen zu unterstützen? Derzeit sind sie nicht zulässig:

struct S1 : System.Runtime.CompilerServices.IUnion
{
    public S1(int x) => throw null;
    public S1(string x) => throw null;
    object System.Runtime.CompilerServices.IUnion.Value => throw null;
}

class Program
{
    static S1 Test1(int? x)
    {
        return x; // error CS0029: Cannot implicitly convert type 'int?' to 'S1'
    }   

    static S1? Test2(int? y)
    {
        return y; // error CS0029: Cannot implicitly convert type 'int?' to 'S1?'
    }   
}

Lösung:

Für jetzt keine aufgehobenen Vereinigungsumwandlungen. Einige Hinweise aus der Diskussion:

Die Analogie zu den benutzerdefinierten Konvertierungen bricht hier ein wenig auf. In der Regel können Gewerkschaften einen Nullwert enthalten, der eingeht. Es ist nicht klar, ob das Heben eine Instanz eines Union-Typs mit null darin gespeichertem Wert erstellen soll oder ob ein Wert erstellt nullNullable<Union>werden soll.

[Behoben] Union-Konvertierung von einer Instanz eines Basistyps blockieren?

Möglicherweise ist das aktuelle Verhalten verwirrend:

struct S1 : System.Runtime.CompilerServices.IUnion
{
    public S1(System.ValueType x)
    {
    }
    public S1(string x) => throw null;
    object System.Runtime.CompilerServices.IUnion.Value => throw null;
}

class Program
{
    static S1 Test1(System.ValueType x)
    {
        return x; // Union conversion
    }   

    static S1 Test2(System.ValueType y)
    {
        return (S1)y; // Unboxing conversion
    }   
}

Die Sprache verbietet explizit das Deklarieren von benutzerdefinierten Konvertierungen von einem Basistyp. Daher kann es sein, dass unionliche Umwandlungen wie das nicht zulässig sind.

Lösung:

Machen Sie nichts Besonderes für jetzt. Generische Szenarien können trotzdem nicht vollständig geschützt werden.

[Behoben] Union-Konvertierung von einer Instanz eines Schnittstellentyps blockieren?

Möglicherweise ist das aktuelle Verhalten verwirrend:

struct S1 : I1, System.Runtime.CompilerServices.IUnion
{
    public S1(I1 x) => throw null;
    public S1(string x) => throw null;
    object System.Runtime.CompilerServices.IUnion.Value => throw null;
}

interface I1 { }

struct S2 : System.Runtime.CompilerServices.IUnion
{
    public S2(I1 x) => throw null;
    public S2(string x) => throw null;
    object System.Runtime.CompilerServices.IUnion.Value => throw null;
}

class C3 : System.Runtime.CompilerServices.IUnion
{
    public C3(I1 x) => throw null;
    public C3(string x) => throw null;
    object System.Runtime.CompilerServices.IUnion.Value => throw null;
}

class Program
{
    static S1 Test1(I1 x)
    {
        return x; // Union conversion
    }   

    static S1 Test2(I1 x)
    {
        return (S1)x; // Unboxing
    }   

    static S2 Test3(I1 x)
    {
        return x; // Union conversion
    }   

    static S2 Test4(I1 x)
    {
        return (S2)x; // Union conversion
    }   

    static C3 Test3(I1 x)
    {
        return x; // Union conversion
    }   

    static C3 Test4(I1 x)
    {
        return (C3)x; // Reference conversion
    }   
}

Die Sprache verbietet explizit das Deklarieren von benutzerdefinierten Konvertierungen von einem Basistyp. Daher kann es sein, dass unionliche Umwandlungen wie das nicht zulässig sind.

Lösung:

Machen Sie nichts Besonderes für jetzt. Generische Szenarien können trotzdem nicht vollständig geschützt werden.

Namespace der IUnion-Schnittstelle

Der Namespace für IUnion die Schnittstelle bleibt nicht angegeben. Wenn die Absicht besteht, sie in einem global Namespace beizubehalten, geben wir das explizit an.

Vorschlag: Wenn dies einfach übersehen wird, könnten wir namespace verwenden System.Runtime.CompilerServices .

Klassen als Union Typen

[Behoben] Überprüfen der Instanz selbst auf null

Wenn ein Union-Typ ein Klassentyp ist, kann er selbst null sein. Was ist dann mit NULL-Prüfungen? Das null Muster wurde für die Überprüfung der Value Eigenschaft kooptiert. Wie überprüfen Sie also, ob die Union selbst nicht null ist?

Beispiel:

  • Wenn S es sich um eine Union Struktur handelt, s is null gilt truefür einen Wert von S?nur, wenn s es sich selbst istnull. Wenn C es sich um eine Union Klasse handelt, c is null ist falsefür einen Wert von C?, wenn c es sich selbst istnull, aber wenn c es true nicht nullist und c.Value ist null.

Ein weiteres Beispiel:

class C1 : IUnion
{
    private readonly object? _value;

    public C1(){}
    public C1(int x) { _value = x; }
    public C1(string x) { _value = x; }
    object? IUnion.Value => _value;
}

class Program
{
    static int Test1(C1? u)
    {
        // warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive).
        //                 For example, the pattern 'null' is not covered.
        // This is very confusing, the switch expression is indeed not exhaustive (u itself is not
        // checked for null), but there is a case 'null => 3' in the switch expression. 
        // It looks like the only way to shut off the warning is to use 'case _'. Adding it removes
        // all benefits of exhaustiveness checking, any union case could be missing and there would
        // be no diagnostic about that.  
        return u switch { int => 1, string => 2, null => 3 };
    }
}

Dieser Teil des Designs ist deutlich optimiert um die Erwartung, dass ein Vereinigungstyp eine Struktur ist. Einige Optionen:

  • Was für ein Pech. Wird für die NULL-Überprüfung anstelle einer Mustervergleichung verwendet == .
  • Lassen Sie das null Muster (und die implizite NULL-Überprüfung in anderen Mustern) sowohl auf den Unionswert als auch auf seine Value Eigenschaft anwenden: u is null ==> u == null || u.Value == null
  • Hindern Sie Klassen daran, Union-Typen zu sein!

[Behoben] Ableiten von einer Union Klasse

Wenn eine Klasse gemäß der aktuellen Spezifikation eine UnionKlasse als Basisklasse verwendet, wird sie selbst zu einer UnionKlasse. Dies geschieht, da sie automatisch die Implementierung der IUnion Schnittstelle "erbt", es ist nicht erforderlich, sie erneut zu implementieren. Gleichzeitig definieren Konstruktoren des abgeleiteten Typs den Satz von Typen in diesem neuen Union. Es ist sehr einfach, sehr seltsames Sprachverhalten um die beiden Klassen zu erreichen:

class C1 : IUnion
{
    private readonly object _value;
    public C1(long x) { _value = x; }
    public C1(string x) { _value = x; }
    object IUnion.Value => _value;
}

class C2(int x) : C1(x);

class Program
{
    static int Test1(C1 u)
    {
        // Good
        return u switch { long => 1, string => 2, null => 3 };
    } 

    static int Test2(C2 u)
    {
        // error CS8121: An expression of type 'C2' cannot be handled by a pattern of type 'long'.
        // error CS8121: An expression of type 'C2' cannot be handled by a pattern of type 'string'.
        return u switch { long => 1, string => 2, null => 3 };
    } 
}

Einige Optionen:

  • Ändern, wenn ein Klassentyp ein Union Typ ist. Eine Klasse ist z. B. ein Union Typ, wenn alle wahr sind:

    • Dies liegt sealed daran, dass abgeleitete Typen nicht als UnionTypen betrachtet werden, was verwirrend ist.
    • Keines seiner Basisen IUnion

    Das ist immer noch nicht perfekt. Die Regeln sind zu subtil. Es ist einfach, einen Fehler zu machen. Es gibt keine Diagnose für die Deklaration, aber Union der Abgleich funktioniert nicht.

  • Hindern Sie Klassen daran, Union-Typen zu sein.

[Behoben] Der Is-Type-Operator

Der Is-Type-Operator wird als Laufzeittypüberprüfung angegeben. Syntaktisch sieht es sehr ähnlich aus wie ein Typmuster, aber es ist nicht. Daher wird der spezielle UnionAbgleich nicht verwendet, was zu Verwirrung durch den Benutzer führen kann.

struct S1 : IUnion
{
    private readonly object _value;
    public S1(int x) { _value = x; }
    public S1(string x) { _value = x; }
    object IUnion.Value => _value;
}

class Program
{
    static bool Test1(S1 u)
    {
        return u is int; // warning CS0184: The given expression is never of the provided ('int') type
    }   

    static bool Test2(S1 u)
    {
        return u is string and ['1', .., '2']; // Good
    }   
}

Im Falle einer rekursiven Vereinigung gibt das Typmuster möglicherweise keine Warnung, aber es wird trotzdem nicht ausgeführt, was der Benutzer möglicherweise tun würde.

Auflösung: Sollte als Typmuster funktionieren.

Listenmuster

Das Listenmuster schlägt immer mit Union übereinstimmenden Übereinstimmungen fehl:

struct S1 : IUnion
{
    private readonly object _value;
    public S1(int[] x) { _value = x; }
    public S1(string[] x) { _value = x; }
    object IUnion.Value => _value;
}

class Program
{
    static bool Test1(S1 u)
    {
        // error CS8985: List patterns may not be used for a value of type 'object'. No suitable 'Length' or 'Count' property was found.
        // error CS0021: Cannot apply indexing with [] to an expression of type 'object'
        return u is [10];
    }   
}

static class Extensions
{
    extension(object o)
    {
        public int Length => 0;
    }
}

Andere Fragen

  • Sowohl die Verwendung von Konstruktoren in Union-Konvertierungen als auch die Verwendung des Zuordnungsmusters TryGetValue(...) in Union wird angegeben, damit sie bei Anwendung mehrerer Konstruktoren lenient sind: Sie wählen nur eine aus. Dies sollte nicht je nach wohlgeformten Regeln wichtig sein, aber sind wir damit vertraut?
  • Die Spezifikation basiert subtil auf der Implementierung der IUnion.Value Eigenschaft und nicht auf einer Value Eigenschaft, die auf dem Union-Typ selbst gefunden wurde. Dies soll eine größere Flexibilität für vorhandene Typen bieten (die möglicherweise eine eigene Value Eigenschaft für andere Verwendungen haben), um das Muster zu implementieren. Aber es ist ungünstig, und inkonsistent mit der Art, wie andere Mitglieder gefunden und direkt auf dem Union-Typ verwendet werden. Sollten wir eine Änderung vornehmen? Einige andere Optionen:
    • Union-Typen erforderlich, um eine öffentliche Value Eigenschaft verfügbar zu machen.
    • Bevorzugen Sie eine öffentliche Value Eigenschaft, wenn sie vorhanden ist, wenden Sie sich jedoch auf die IUnion.Value Implementierung zurück, wenn dies nicht der Fall ist (ähnlich wie GetEnumerator Regeln).
  • Die vorgeschlagene Syntax der Unionsdeklaration wird nicht allgemein geliebt, insbesondere wenn es darum geht, die Falltypen auszudrücken. Alternativen treffen sich bisher auch mit Kritik, aber es ist möglich, dass wir eine Änderung vornehmen werden. Einige der wichtigsten Bedenken bezüglich des aktuellen:
    • Kommas als Trennzeichen zwischen Groß-/Kleinschreibungstypen scheinen zu bedeuten, dass die Reihenfolge wichtig ist.
    • In Klammern angeordnete Listen sehen zu viel wie primäre Konstruktoren aus (obwohl keine Parameternamen vorhanden sind).
    • Zu unterschiedlich von Enumerationen, die ihre "Fälle" in geschweiften Klammern haben.
  • Während Union-Deklarationen Strukturen mit einem einzigen Referenzfeld generieren, sind sie bei verwendung in einem gleichzeitigen Kontext immer noch etwas anfällig für unerwartetes Verhalten. Wenn beispielsweise ein benutzerdefiniertes Funktionsmememb mehrere Ableitungen this ableiten, wurde die enthaltende Variable möglicherweise durch einen anderen Thread zwischen den beiden Zugängen als Ganzes zugewiesen. Der Compiler könnte Code generieren, der bei Bedarf in ein lokales Dokument kopiert this werden kann. Sollte dies der Vorgang sein? Im Allgemeinen ist der Grad der Parallelitätsresilienz wünschenswert und vernünftig zu erreichen?