Kommentar
Åtkomst till den här sidan kräver auktorisering. Du kan prova att logga in eller ändra kataloger.
Åtkomst till den här sidan kräver auktorisering. Du kan prova att ändra kataloger.
Anmärkning
Den här artikeln är en funktionsspecifikation. Specifikationen fungerar som designdokument för funktionen. Den innehåller föreslagna specifikationsändringar, tillsammans med information som behövs under utformningen och utvecklingen av funktionen. Dessa artiklar publiceras tills de föreslagna specifikationsändringarna har slutförts och införlivats i den aktuella ECMA-specifikationen.
Det kan finnas vissa skillnader mellan funktionsspecifikationen och den slutförda implementeringen. Dessa skillnader samlas in i de relevanta LDM-anteckningarna (Language Design Meeting).
Du kan lära dig mer om processen för att införa funktionsspecifikationer i C#-språkstandarden i artikeln om specifikationerna.
Champion-fråga: https://github.com/dotnet/csharplang/issues/9662
Sammanfattning
Unioner är en uppsättning sammankopplade funktioner som kombineras för att ge C#-stöd för unionstyper:
-
Union-typer: Structs och klasser som har ett
[Union]attribut identifieras som unionstyper och stöder fackliga beteenden. - Ärendetyper: Union-typer har en uppsättning skiftlägestyper, som ges av parametrar till konstruktorer och fabriksmetoder.
-
Unionsbeteenden: Union-typer stöder följande fackliga beteenden:
- Union-konverteringar: Det finns implicita unionskonverteringar från varje ärendetyp till en unionstyp.
- Union-matchning: Mönstermatchning mot unionsvärden "skriver implicit upp" innehållet och tillämpar mönstret på det underliggande värdet i stället.
- Unionens fullständighet: Växla uttryck över unionsvärden är uttömmande när alla falltyper har matchats, utan behov av ett reservfall.
- Unionens nullbarhet: Nullabilitetsanalysen har förbättrat spårningen av null-tillståndet för en unions innehåll.
- Unionsmönster: Alla unionstyper följer ett grundläggande unionsmönster, men det finns ytterligare valfria mönster för specifika scenarier.
- Unionsdeklarationer: En kortfattad syntax tillåter deklaration av unionstyper direkt. Implementeringen är "åsiktsbaserad" – en structdeklaration som följer det grundläggande unionsmönstret och lagrar innehållet som ett enda referensfält.
- Unionens gränssnitt: Några gränssnitt är kända av språket och används i genomförandet av fackliga deklarationer.
Motivation
Unioner är en sedan länge efterfrågad C#-funktion, som gör det möjligt att uttrycka värden från en sluten uppsättning typer på ett sätt som mönstermatchning kan lita på är uttömmande.
Separationen mellan unionstyper och fackliga deklarationer gör att C# kan ha en kortfattad syntax för fackliga deklarationer med åsiktssemantik, samtidigt som befintliga typer eller typer med andra implementeringsalternativ kan välja fackliga beteenden.
De föreslagna fackföreningarna i C# är typer av fackföreningar och inte "diskriminerade" eller "taggade". "Diskriminerade fackföreningar" kan uttryckas i termer av "typfack" genom att använda nya typdeklarationer som falltyper. Alternativt kan de implementeras som en sluten hierarki, vilket är en annan, relaterad, kommande C#-funktion som fokuserar på fullständighet.
Detaljerad design
Union-typer
Alla klass- eller structtyper med ett System.Runtime.CompilerServices.UnionAttribute attribut anses vara en unionstyp:
namespace System.Runtime.CompilerServices
{
[AttributeUsage(Class | Struct, AllowMultiple = false)]
public class UnionAttribute : Attribute;
}
En fackföreningstyp måste följa ett visst mönster av offentliga fackföreningsmedlemmar, som antingen måste deklareras på själva fackföreningstypen eller delegeras till en "fackföreningsmedlemsleverantör".
Vissa fackföreningsmedlemmar är obligatoriska och andra är valfria.
En fackföreningstyp har en uppsättning falltyper som upprättas baserat på signaturer för vissa fackföreningsmedlemmar.
Innehållet i ett unionsvärde kan nås via en Value egenskap. Språket förutsätter att Value endast innehåller ett värde för en av skiftlägestyperna eller null (se Välformning).
Unionsmedlemsleverantörer
Som standard finns fackföreningsmedlemmar på själva unionstypen. Men om uniontypen direkt innehåller en deklaration av ett gränssnitt med namnet IUnionMembers fungerar gränssnittet som en facklig medlemsprovider. I så fall finns fackföreningsmedlemmar endast på den fackliga medlemsleverantören, inte på själva fackföreningstypen.
Ett gränssnitt för unionsmedlemsprovidern måste vara offentligt och själva unionstypen måste implementera det som ett gränssnitt.
Vi använder termen unionsdefinieringstyp för den typ där fackföreningsmedlemmarna finns: Den fackliga medlemsprovidern om den finns och själva unionstypen annars.
Medlemmar i unionen
Fackföreningsmedlemmar letas upp med namn och underskrift på den unionsdefinierande typen. De behöver inte deklareras direkt på den unionsdefinierande typen, men de kan ärvas.
Det är ett fel att en fackföreningsmedlem inte är offentlig.
De skapande medlemmarna och egenskapen Value är obligatoriska och kallas gemensamt för det grundläggande unionsmönstret.
Medlemmarna HasValue och TryGetValue kallas gemensamt för icke-boxningsfackets åtkomstmönster.
De olika fackföreningsmedlemmarna beskrivs i följande.
Medlemmar i skapande av unionen
Medlemmar som skapar unionen används för att skapa nya unionsvärden från ett ärendetypsvärde.
Om den unionsdefinierande typen är själva unionstypen är varje konstruktor med en enda parameter en unionskonstruktor. Ärendetyperna för unionen identifieras som en uppsättning typer som skapats från parametertyperna för dessa konstruktorer på följande sätt:
- Om parametertypen är en nullbar typ (oavsett om det är ett värde eller en referens) är skiftlägestypen den underliggande typen
- Annars är skiftlägestypen parametertypen.
// 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) { ... }
Om den unionsdefinierande typen är en unionsmedlemsprovider är varje statisk Create metod med en enda parameter och en returtyp som är identitetskonverterad till själva unionstypen en union factory-metod.
Skiftlägestyperna för unionen identifieras som en uppsättning typer som skapats från parametertyperna för dessa fabriksmetoder på följande sätt:
- Om parametertypen är en nullbar typ (oavsett om det är ett värde eller en referens) är skiftlägestypen den underliggande typen
- Annars är skiftlägestypen parametertypen.
// 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) { ... }
Fackliga konstruktorer och fackliga fabriksmetoder kallas gemensamt för medlemmar i skapande av fackföreningar.
Den enda parametern för en medlem som skapar en union måste vara ett värde eller in en parameter.
En unionstyp måste ha minst en medlem som skapar unionen och därför minst en falltyp.
Värdeegenskap
Egenskapen Value ger åtkomst till värdet i en union, oavsett ärendetyp.
Varje uniondefinieringstyp måste deklarera en Value egenskap av typen object? eller object. Egenskapen måste ha en get accessor och kan eventuellt ha en init eller-accessor set , som kan vara av alla hjälpmedel och som inte används av kompilatorn.
// Union 'Value' property
public object? Value { get; }
Åtkomstmedlemmar som inte är boxningsmedlemmar
En unionstyp kan välja att dessutom implementera mönstret för icke-boxningsunionsåtkomst, vilket ger starkt typad villkorlig åtkomst till varje ärendetyp, samt ett sätt att söka efter null.
På så sätt kan kompilatorn implementera mönstermatchning mer effektivt när skiftlägestyper är värdetyper och lagras som sådana i unionen.
Medlemmar i icke-boxningsåtkomst är:
- En
HasValueegenskap av typenboolmed en offentliggetaccessor. Det kan också ha eniniteller-accessorset, som kan vara av alla hjälpmedel och inte används av kompilatorn. - En
TryGetValuemetod för varje skiftlägestyp. Metoden returnerarbooloch tar en enskild out-parameter av en typ som är identitetskonverterad till skiftlägestypen.
// Non-boxing access members
public bool HasValue { get { ... } }
public bool TryGetValue(out Dog value) { ... }
HasValue förväntas returnera true om och endast om fackets Value inte är null.
TryGetValue förväntas returnera sant om och endast om fackets Value är av den angivna ärendetypen och i så fall levererar det värdet i metodens out-parameter.
Välformad
Språket och kompilatorn gör ett antal beteendemässiga antaganden om unionstyper. Om en typ kvalificerar sig som en unionstyp men inte uppfyller dessa antaganden kanske inte fackliga beteenden fungerar som förväntat.
-
Ljud: Egenskapen
Valueutvärderas alltid till null eller till ett värde av skiftlägestyp. Det gäller även för standardvärdet för union-typen. -
Stabilitet: Om ett union-värde skapas från en skiftlägestyp matchar egenskapen skiftlägestypen
Valueeller null. Om ett union-värde skapas från ettnullvärde blirnullegenskapenValue. - Ekvivalens för skapande: Om ett värde implicit kan konverteras till två olika skiftlägestyper har den skapande medlemmen för någon av dessa falltyper samma observerbara beteende när det anropas med det värdet.
-
Konsekvens för åtkomstmönster: Beteendet för och
TryGetValueicke-boxningsåtkomstmedlemmarHasValue, om det finns, är märkbart likvärdigt med kontrollen motValueegenskapen direkt.
Exempel på unionstyper
Pet implementerar det grundläggande unionsmönstret på själva unionstypen:
[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 implementerar åtkomstmönstret för icke-boxning på själva unionstypen:
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;
}
}
Observera: Detta är bara ett exempel på hur åtkomstmönstret för icke-boxning kan implementeras. Användarkoden kan lagra innehållet hur det vill. I synnerhet hindrar det inte genomförandet från boxning! I non-boxing namnet refererar till att tillåta kompilatorns mönstermatchningsimplementering att komma åt varje ärendetyp på ett starkt skrivet sätt, i motsats till object?egenskapen -typed Value .
Result<T> implementerar det grundläggande mönstret via en unionsmedlemsleverantör:
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;
}
Fackliga beteenden
Fackliga beteenden implementeras vanligtvis med hjälp av det grundläggande unionsmönstret. Om facket erbjuder mönstret för icke-boxningsåtkomst kommer unionsmönstermatchning företrädesvis att använda det.
Unionskonverteringar
En unionskonvertering konverteras implicit till en unionstyp från var och en av dess skiftlägestyper. Mer specifikt finns det en unionskonvertering till en unionstyp U från en typ eller ett uttryck E om det finns en implicit standardkonvertering från E till en typ C och C är en parametertyp för en medlem i skapande av unionen iU.
Om uniontypen U är en struct, finns det en unionskonvertering att skriva U? från en typ eller ett uttryck E om det finns en implicit standardkonvertering från E till en typ C och C är en parametertyp för en medlem i unionskapandet i U.
En unionskonvertering är inte i sig en implicit standardkonvertering. Den får därför inte delta i en användardefinierad implicit konvertering eller någon annan unionkonvertering.
Det finns inga explicita unionskonverteringar utöver de implicita unionskonverteringarna. Även om det finns en explicit konvertering från E till en unions ärendetyp Cbetyder det alltså inte att det finns en explicit konvertering från E till den unionstypen.
En fackföreningskonvertering utförs genom att anropa förbundets medlem:
Pet pet = dog;
// becomes
Pet pet = new Pet(dog);
// and
Result<string> result = "Hello"
//becomes
Result<string> result = Result<string>.IUnionMembers.Create("Hello");
Det är ett fel om överbelastningslösningen inte hittar en enda bästa kandidatmedlem, eller om den medlemmen inte är en av fackföreningstypens fackföreningsmedlemmar.
Union-konvertering är bara ännu en "form" av en implicit användardefinierad konvertering. En tillämplig användardefinierad konverteringsoperator "skuggar" unionskonvertering.
Motiveringen bakom detta beslut:
Om någon har skrivit en användardefinierad operator bör den prioriteras. Med andra ord, om användaren faktiskt skrev sin egen operatör vill de att vi ska anropa den. Befintliga typer med konverteringsoperatorer som omvandlas till unionstyper fortsätter att fungera på samma sätt med avseende på befintlig kod som använder operatörerna idag.
I följande exempel prioriteras en implicit användardefinierad konvertering framför en unionskonvertering.
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
}
I följande exempel, när explicit cast används i kod, prioriteras en explicit användardefinierad konvertering framför en unionskonvertering. Men när det inte finns någon explicit gjuten kod används en unionskonvertering eftersom explicit användardefinierad konvertering inte är tillämplig.
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-matchning
När det inkommande värdet för ett mönster är av en unionstyp eller av en nullbar unionstyp, kan värdet för null och det underliggande unionsvärdets innehåll "packas upp", beroende på mönstret.
För ovillkorliga _ mönster och var mönster tillämpas mönstret på själva det inkommande värdet. Som exempel:
if (GetPet() is var pet) { ... } // 'pet' is the union value returned from `GetPet`
Alla andra mönster tillämpas dock implicit på den underliggande föreningens Value egenskap:
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'
För logiska mönster tillämpas den här regeln individuellt på grenarna, med tanke på att den vänstra grenen av ett and mönster kan påverka den inkommande typen av den högra grenen:
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?`.
}
Observera: Den här regeln innebär att sannolikt inte kommer att GetPet() is Pet pet lyckas, vilket Pet tillämpas på innehållet, inte på Pet själva unionen.
Observera: Anledningen till den olika behandlingen av villkorslöst var mönster (samt _, som i huvudsak är en förkortning för var _) är ett antagande att deras användning kvalitativt skiljer sig från andra mönster.
var mönster används bara för att namnge värdet som matchas mot, ofta i kapslade mönster, till exempel PetOwner{ Pet: var pet }. Här är den användbara semantiken för att behålla unionstypen Pet, i stället för petValue att egenskapen derefereras till en värdelös object? typ.
Om det inkommande värdet är en klasstyp null lyckas mönstret oavsett om själva unionsvärdet är null eller dess inneslutna värde är null:
if (result is null) { ... } // if (result == null || result.Value == null)
Andra matchningsmönster för union lyckas endast när själva unionsvärdet inte nullär .
if (result is 1) { ... } // if (result != null && result.Value is 1)
På samma sätt, om det inkommande värdet är en nullbar värdetyp (omsluter en struct union-typ), null kommer mönstret att lyckas oavsett om det inkommande värdet självt är null eller dess inneslutna värde är null:
if (result is null) { ... } // if (result.HasValue == false || result.GetValueOrDefault().Value == null)
Andra matchningsmönster för union lyckas bara när det inkommande värdet i sig inte nullär .
if (result is 1) { ... } // if (result.HasValue && result.GetValueOrDefault().Value is 1)
Kompilatorn föredrar att implementera mönsterbeteende med hjälp av medlemmar som föreskrivs i åtkomstmönstret för icke-boxning. Även om det är fritt att göra någon optimering inom gränserna för de välformulerade reglerna, är följande den minsta uppsättning som garanteras tillämpas:
- För ett mönster som innebär att söka efter en viss typ
T, om enTryGetValue(S value)metod är tillgänglig och det finns en identitet, eller implicit referens/boxningskonvertering frånTtillS, används den metoden för att hämta värdet. Mönstret tillämpas sedan på det värdet. Om det finns mer än en sådan metod, är alla där konverteringen frånTtillSinte är en boxningskonvertering att föredra om den är tillgänglig. Om det fortfarande finns fler än en metod väljs en på ett implementeringsdefinierat sätt. - Annars används egenskapen för
nullett mönster som innebär att söka efter , om enHasValueegenskap är tillgänglig, för att kontrollera om union-värdet är null. - Annars tillämpas mönstret på resultatet av åtkomsten
IUnion.Valuetill egenskapen i den inkommande unionen.
Operatorn is-type som tillämpas på en unionstyp har samma betydelse som ett typmönster som tillämpas på unionstypen.
Unionens fullständighet
En unionstyp antas vara "uttömd" av sina falltyper. Det innebär att ett switch uttryck är fullständigt om det hanterar alla en unions ärendetyper:
var name = pet switch
{
Dog dog => ...,
Cat cat => ...,
// No warning about non-exhaustive switch
};
Nullbarhet
Null-tillståndet för en unions egenskap spåras Value som vilken annan egenskap som helst, med dessa ändringar:
- När en medlem som skapar en union anropas
Value(uttryckligen eller via en unionskonvertering) får den nya fackföreningen null-tillståndet för det inkommande värdet. - När åtkomstmönstret för icke-boxning används för att fråga efter innehållet i en unionstyp (explicit eller via mönstermatchning) påverkar
Valuedet nullbarhetstillståndet på samma sätt som omValuedet hade kontrollerats direkt: Null-tillståndetValueblir "inte null" på grenentrue.HasValueTryGetValue(...)
Även om en unionsväxel annars är fullständig, om null-tillståndet för den inkommande fackets Value egenskap är "kanske null", ges en varning om ohanterad null.
Pet pet = GetNullableDog(); // 'pet.Value' is "maybe null"
var value = pet switch
{
Dog dog => ...,
Cat cat => ...,
// Warning: 'null' not handled
}
Union-gränssnitt
Följande gränssnitt används av språket i dess implementering av unionsfunktioner.
Gränssnitt för unionsåtkomst
Gränssnittet IUnion markerar en typ som en unionstyp vid kompileringstillfället och ger ett sätt att komma åt unionsinnehåll vid körning.
public interface IUnion
{
// The value of the union or null
object? Value { get; }
}
Unioner som genereras av kompilatorn implementerar det här gränssnittet.
Exempel på användning:
if (value is IUnion { Value: null }) { ... }
Unionsdeklarationer
Unionsdeklarationer är ett kortfattat och åsiktsrikt sätt att deklarera fackliga typer i C#. De deklarerar en struct som använder en referens för ett enda objekt för att lagra dess Value, vilket innebär:
- Boxning: Alla värdetyper bland deras skiftlägestyper kommer att boxas vid inmatning.
- Kompakthet: Union-värden innehåller bara ett enda fält.
Avsikten är att fackliga förklaringar ska täcka de allra flesta användningsfall ganska bra. De två främsta orsakerna till handkodning av specifika unionstyper i stället för att använda fackliga deklarationer förväntas vara:
- Anpassa befintliga typer till unionmönster för att få fackliga beteenden.
- Implementera en annan lagringsstrategi av t.ex. effektivitets- eller interop-skäl.
Syntax
En unionsdeklaration har ett namn och en lista över typer av unionskonstruktorer .
union_declaration
: attributes? struct_modifier* 'partial'? 'union' identifier type_parameter_list?
'(' type (',' type)* ')' struct_interfaces? type_parameter_constraints_clause*
(`{` struct_member_declaration* `}` | ';')
;
Förutom begränsningarna för structmedlemmar (§16.3) gäller följande för fackföreningsmedlemmar:
- Instansfält, autoegenskaper eller fältliknande händelser är inte tillåtna.
- Explicit deklarerade offentliga konstruktorer med en enda parameter är inte tillåtna.
- Explicit deklarerade konstruktorer måste använda en
this(...)initierare för att (direkt eller indirekt) delegera till en av de genererade konstruktorerna.
De fackliga konstruktortyperna kan vara alla typer som konverteras till object, t.ex. gränssnitt, typparametrar, nullbara typer och andra unioner. Det är bra för resulterande fall att överlappa, och för fackföreningar att kapsla eller vara null.
Exempel:
// 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){ ... }
Sänks till:
[Union] public struct Pet : IUnion
{
public Pet(Cat value) => Value = value;
public Pet(Dog value) => Value = value;
public object? Value { get; }
... // original body
}
Öppna frågor
[Löst] Är facklig deklaration en post?
En facklig deklaration sänks till en poststruct
Jag tror att det här standardbeteendet är onödigt och, med tanke på att det inte kan konfigureras, kommer att avsevärt begränsa användningsscenarier. Poster genererar mycket kod som antingen inte används eller som inte matchar specifika krav. Poster är till exempel i stort sett förbjudna i kompilatorns kodbas på grund av kodsvälten. Jag tror att det skulle vara bättre att ändra standardvärdet:
- Som standard deklarerar en facklig förklaring en vanlig struct med bara fackliga medlemmar.
- En användare kan deklarera en postunion:
record union U(E1, ...) ...
Upplösning: En facklig förklaring är en vanlig struct, inte post struct.
record union ... Stöds inte
[Löst] Union-deklarationssyntax
Det verkar som om den föreslagna syntaxen är ofullständig eller i onödan begränsande. Det verkar till exempel som om bassatsen inte är tillåten. Men jag kan lätt föreställa mig ett behov av att implementera ett gränssnitt, till exempel.
Jag tror att förutom element-typer-listan syntaxen bör matcha regelbunden struct/record struct deklaration där nyckelordet struct ersätts med union nyckelord.
Upplösning: Begränsningen tas bort.
[Löst] Medlemmar i unionens deklaration
Instansfält, autoegenskaper eller fältliknande händelser är inte tillåtna.
Detta känns godtyckligt och helt onödigt.
Upplösning: Begränsningen behålls.
[Löst] Nullbara värdetyper som unionärendetyper
Skiftlägestyperna för unionen identifieras som en uppsättning parametertyper från dessa konstruktorer. Skiftlägestyperna för unionen identifieras som en uppsättning parametertyper från dessa fabriksmetoder.
Samtidigt:
En
TryGetValuemetod för varje skiftlägestyp. Metoden returnerarbooloch tar en enskild out-parameter av en typ som motsvarar den angivna falltypen på följande sätt:
- Om skiftlägestypen är en nullbar värdetyp ska parametertypen vara identitetskonverterad till den underliggande typen
- Annars ska typen vara identitetskonverterad till skiftlägestypen.
Finns det en fördel med att ha en nullbar värdetyp bland skiftlägestyperna, särskilt att ett typmönster inte kan använda nullbar värdetyp som måltyp? Det känns som om vi bara skulle kunna säga att om konstruktorns/fabrikens parametertyp är en nullbar värdetyp är motsvarande skiftlägestyp den underliggande typen. Då skulle vi inte behöva den TryGetValue extra satsen för metoden, alla utparametrar är skiftlägestyper.
Upplösning: Förslaget godkänns
[Löst] Standardtillstånd Value för nullbar egenskap
För unionstyper där ingen av skiftlägestyperna är null är standardtillståndet för "inte null" i stället för
Value"kanske null".
Med den nya designen, där Value egenskapen inte definieras i något allmänt gränssnitt, men är ett API som specifikt tillhör den deklarerade typen, känns regeln som citeras ovan som överteknik. Dessutom kommer regeln sannolikt att tvinga konsumenterna att använda nullbara typer i situationer där annars nullbara typer inte skulle användas.
Tänk till exempel på följande fackliga förklaring:
union U1(int, bool, DateTime);
Enligt den citerade regeln är standardtillståndet för Value "inte null". Men det matchar inte beteendet av typen, default(U1).Value är null. För att justera beteendet tvingas konsumenten att göra minst en falltyp nullbar. Ungefär så här:
union U1(int?, bool, DateTime);
Men det är sannolikt inte önskvärt, konsumenten kanske inte vill tillåta explicit skapande med int? värde.
Förslag: Ta bort den citerade regeln, nullbar analys bör använda anteckningar från Value egenskapen för att härleda dess standard nullabilitet.
Upplösning: Förslaget godkänns
[Löst] Union-matchning för Nullable av en union-värdetyp
När det inkommande värdet för ett mönster är av en unionstyp kan unionsvärdets innehåll "packas upp", beroende på mönstret.
Ska vi utöka den här regeln till scenarier när inkommande värde för ett mönster är av en Nullable<union type>?
Tänk på följande scenario:
static bool Test1(StructUnion? u)
{
return u is 1;
}
static bool Test2(ClassUnion? u)
{
return u is 1;
}
Innebörden av u is 1 i Test1 och Test2 är mycket olika. I Test1 är det inte en unionsmatchning, i Test2 är det det.
Kanske borde "union matching" "gräva" igenom Nullable<T> som mönstermatchning vanligtvis gör i andra situationer.
Om vi går med på det bör det fackliga matchningsmönstret null mot Nullable<union type> fungera som mot klasser.
Dvs. mönstret är sant när (!nullableValue.HasValue || nullableValue.Value.Value is null).
Upplösning: Förslaget godkänns.
Vad ska man göra med "dåliga" API:er?
Vad ska kompilatorn göra med matchande API:er för union som ser ut som en matchning, men som annars är "dåliga"? Kompilatorn hittar till exempel TryGetValue/HasValue med matchande signatur, men det är "dåligt" eftersom en nödvändig anpassad modifierare eller en okänd funktion krävs osv. Bör kompilatorn ignorera API:et eller rapportera ett fel? På liknande sätt kan API:et markeras som föråldrad/experimentell. Ska kompilatorn rapportera diagnostik, tyst använda API:et eller tyst inte använda API:et?
Vad händer om typer för unionsdeklaration saknas
Vad händer om UnionAttribute, IUnion eller IUnion<TUnion> saknas? Fel? Syntetisera? Något annat?
[Löst] Design av generiskt IUnion-gränssnitt
Argument har gjorts som IUnion<TUnion> inte ska ärva från IUnion eller begränsa dess typparameter till IUnion<TUnion>. Vi borde återkomma.
Upplösning: Gränssnittet IUnion<TUnion> har tagits bort för tillfället.
[Löst] Nullbara värdetyper som skiftlägestyper och deras interaktion med TryGetValue
Reglerna ovan anger att om en ärendetyp är en nullbar värdetyp ska parametertypen som används i en motsvarande TryGetValue metod vara den underliggande typen.
Detta motiveras av det faktum att ett null värde aldrig skulle ges genom den här metoden. På förbrukningssidan tillåts inte en nullbar värdetyp som ett typmönster, medan en matchning mot den underliggande typen ska kunna mappas till ett anrop av den här metoden.
Vi bör bekräfta att vi håller med om den här omskrivningen.
Upplösning: Överenskommet/bekräftat
Åtkomstmönstret för icke-boxningsfacket
Behöver ange exakta regler för att hitta lämpliga HasValue API:er och TryGetValue API:er.
Är arv inblandat? Är läsning/skrivning HasValue en acceptabel matchning? Etc.
[Löst] TryGetValue matchande konverteringar
I avsnittet Union Matching står det:
För ett mönster som innebär att söka efter en specifik typ
T, om enTryGetValue(S value)metod är tillgänglig och det finns en implicit konvertering frånTtillS, används den metoden för att hämta värdet.
Begränsas uppsättningen implicita konverteringar på något sätt? Tillåts till exempel användardefinierade konverteringar? Hur är det med tuppeln konverteringar och andra inte så triviala konverteringar? Vissa av dessa är till och med standardkonverteringar.
Är uppsättningen TryGetValue metoder begränsade på något annat sätt? Avsnittet Union Patterns innebär till exempel att endast metoder med en parametertyp som matchar en ärendetyp beaktas:
en
public bool TryGetValue(out T value)metod för varje falltypT.
Det vore bra att ha ett uttryckligt svar.
Upplösning: Endast implicit identitet, referens eller boxningskonverteringar betraktas som
TryGetValue och nullbar analys
När åtkomstmönstret för icke-boxning används för att fråga efter innehållet i en unionstyp (explicit eller via mönstermatchning) påverkar
Valuedet nullbarhetstillståndet på samma sätt som omValuedet hade kontrollerats direkt: Null-tillståndetValueblir "inte null" på grenentrue.HasValueTryGetValue(...)
Är uppsättningen TryGetValue metoder begränsade på något sätt? Avsnittet Union Patterns innebär till exempel att endast metoder med en parametertyp som matchar en ärendetyp beaktas:
en
public bool TryGetValue(out T value)metod för varje falltypT.
Det vore bra att ha ett uttryckligt svar.
Förtydliga regler kring default värden för struct union-typer
Obs! Standardregeln för nullabilitet som nämns nedan har tagits bort.
Obs! De "standard" välformningsregler som nämns nedan har tagits bort. Vi bör bekräfta att detta är vad vi vill ha.
I avsnittet Nullability står det:
För unionstyper där ingen av skiftlägestyperna är null är standardtillståndet för "inte null" i stället för
Value"kanske null".
Med tanke på att den aktuella implementeringen i exemplet nedan betraktar Values2 som "inte null":
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!;
}
Samtidigt säger avsnittet Well-formedness :
- Standardvärde: Om en unionstyp är en värdetyp har
nullstandardvärdet som dessValue.- Standardkonstruktor: Om en unionstyp har en null-konstruktor (inget argument) har
nullden resulterande unionen som dessValue.
En implementering som den kommer att stå i strid med nullbart analysbeteende för exemplet ovan.
Ska reglerna för välformning justeras eller ska tillståndet Valuedefault vara "kanske null"?
Om det senare, bör initieringen S2 s2 = default; ge en varning om nullabilitet?
Bekräfta att en typparameter aldrig är en unionstyp, även om den är begränsad till en.
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
}
}
Ska attribut efter villkor påverka standardmässig nullbarhet för en Unionsinstans?
Obs! Standardregeln för nullabilitet som nämns nedan har tagits bort. Och vi härleder inte längre standardmässig nullabilitet för Value egenskapen från unionskapningsmetoder. Därför är frågan föråldrad/inte längre tillämplig på den aktuella designen.
För unionstyper där ingen av skiftlägestyperna är null är standardtillståndet för "inte null" i stället för
Value"kanske null".
Förväntas varningen i följande scenario
#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 }; //
}
}
Unionskonverteringar
[Löst] Var hör de hemma bland andra konverteringar prioritetsmässigt?
Union-konverteringar känns som en annan form av en användardefinierad konvertering. Den aktuella implementeringen klassificerar dem därför direkt efter ett misslyckat försök att klassificera en implicit användardefinierad konvertering, och i händelse av existens behandlas de som bara en annan form av en användardefinierad konvertering. Detta får följande konsekvenser:
- En implicit användardefinierad konvertering prioriteras framför en unionskonvertering
- När explicit cast används i kod prioriteras en explicit användardefinierad konvertering framför en unionskonvertering
- När det inte finns någon explicit gjuten kod prioriteras en unionskonvertering framför en explicit användardefinierad konvertering
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)
}
Vi måste bekräfta att det här är det beteende som vi gillar. I annat fall bör konverteringsreglerna klargöras.
Upplösning:
Godkänd av arbetsgruppen.
[Löst] Referens av konstruktorns parameter
För närvarande tillåter språk endast per värde och in parametrar för användardefinierade konverteringsoperatorer.
Det känns som att orsakerna till den här begränsningen också gäller konstruktorer som är lämpliga för fackliga konverteringar.
Förslag:
Justera definitionen av ett case type constructor i Union types avsnittet ovan:
-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.
Upplösning:
Godkänt av arbetsgruppen för tillfället. Vi kan dock överväga att "dela upp" uppsättningen av skiftlägestypkonstruktorer och den uppsättning konstruktorer som är lämpliga för konverteringar av unionstyp.
[Löst] Konverteringar som kan ogiltigförklaras
Avsnittet Nullable Conversions listar explicit konverteringar som kan användas som underliggande. Den aktuella specifikationen föreslår inga justeringar i listan. Detta resulterar i ett fel för följande scenario:
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?'
}
}
Förslag:
Justera specifikationen för att stödja en implicit nullbar konvertering från S till T? som backas upp av en unionskonvertering.
Mer specifikt förutsätter vi att T det finns en implicit konvertering till en typ T? från en typ eller ett uttryck E om det finns en unionskonvertering från E till en typ C och C är en falltyp av T.
Observera att det inte finns något krav på att typen av E ska vara en värdetyp som inte kan vara null.
Konverteringen utvärderas som den underliggande unionskonverteringen från S till T följt av en omslutning från T till T?
Upplösning:
Godkända.
[Löst] Hävda konverteringar
Vill vi justera avsnittet Omlyfta konverteringar för att stödja hävda fackliga konverteringar? För närvarande är de inte tillåtna:
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?'
}
}
Upplösning:
Inga hävda fackliga konverteringar för tillfället. Några kommentarer från diskussionen:
Analogi till användardefinierade konverteringar delar upp lite här. I allmänhet kan fackföreningar innehålla ett null-värde som kommer in. Det är oklart om lyft ska skapa en instans av en unionstyp med
nullvärdet som lagras i den eller om den ska skapa ettnullvärde påNullable<Union>.
[Löst] Vill du blockera unionskonvertering från en instans av en bastyp?
Det aktuella beteendet kan vara förvirrande:
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
}
}
Observera att språket uttryckligen inte tillåter att användardefinierade konverteringar deklareras från en bastyp. Därför kan det vara bra att inte tillåta unionskonverteringar på det viset.
Upplösning:
Gör inget speciellt för tillfället. Allmänna scenarier kan ändå inte skyddas fullständigt.
[Löst] Vill du blockera unionskonvertering från en instans av en gränssnittstyp?
Det aktuella beteendet kan vara förvirrande:
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
}
}
Observera att språket uttryckligen inte tillåter att användardefinierade konverteringar deklareras från en bastyp. Därför kan det vara bra att inte tillåta unionskonverteringar på det viset.
Upplösning:
Gör inget speciellt för tillfället. Allmänna scenarier kan ändå inte skyddas fullständigt.
Namnområde för IUnion-gränssnittet
Innehållande namnområde för IUnion gränssnittet förblir ospecificerat. Om avsikten är att behålla den i ett global namnområde ska vi ange det explicit.
Förslag: Om detta är något som helt enkelt förbises kan vi använda System.Runtime.CompilerServices namnområdet.
Klasser som Union typer
[Löst] Kontrollera själva instansen för null
Om en unionstyp är en klasstyp kan värdet i sig vara null. Hur är det med null-kontroller då?
Mönstret null har valts för att kontrollera Value egenskapen, så hur kontrollerar du att själva facket inte är null?
Som exempel:
- När
Sär enUnionstructs is nullärtruevärdetS?endast närsdet ärnull. NärCär enUnionklass,c is nullför värdetC?är närci sig ärfalsenull, men det ärtruenärci sig är intenullochc.Valueärnull.
Ett annat exempel:
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 };
}
}
Den här delen av designen är tydligt optimerad kring förväntningarna att en union-typ är en struct. Några alternativ:
- Synd. Använd
==för null-kontrollen i stället för en mönstermatchning. -
nullLåt mönstret (och implicit null-kontroll i andra mönster) gälla för både union-värdet och dessValueegenskap:u is null ==> u == null || u.Value == null. - Tillåt inte klasser från att vara unionstyper!
[Löst] Härleda från en Union klass
När en klass använder en Unionklass som basklass, enligt den aktuella specifikationen, blir den en Unionklass själv. Detta händer eftersom det automatiskt "ärver" implementeringen av IUnion gränssnittet, det krävs inte för att implementera det igen. Samtidigt definierar konstruktorer av den härledda typen uppsättningen typer i den här nya Union. Det är mycket enkelt att komma till mycket konstigt språkbeteende runt de två klasserna:
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 };
}
}
Några alternativ:
Ändra när en klasstyp är en
Uniontyp. En klass är till exempel enUniontyp när allt är sant:- Det beror
sealedpå att härledda typer inte betraktas somUniontyper, vilket gör det förvirrande. - Ingen av dess baser implementerar
IUnion
Det här är fortfarande inte perfekt. Reglerna är för subtila. Det är lätt att göra ett misstag. Det finns ingen diagnostik för deklarationen, men
Unionmatchning fungerar inte.- Det beror
Tillåt inte klasser från att vara unionstyper.
[Löst] Operatorn är av typen
Operatorn is-type anges som en kontroll av körningstyp. Syntaktiskt ser det väldigt mycket ut som ett typmönster, men det är det inte. Därför används inte den speciella Unionmatchningen, vilket kan leda till en användarförvirring.
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
}
}
I händelse av en rekursiv union kanske typmönstret inte ger någon varning, men det gör fortfarande inte vad användaren kan tro att det skulle göra.
Upplösning: Bör fungera som ett typmönster.
Listmönster
Listmönstret misslyckas alltid med Union matchning:
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;
}
}
Andra frågor
- Både användningen av konstruktorer i unionskonverteringar och användningen av
TryGetValue(...)i union mönstermatchning anges vara överseende när flera gäller: De väljer bara en. Detta bör inte spela någon roll enligt välformuleringsreglerna, men är vi bekväma med det? - Specifikationen förlitar sig subtilt på implementeringen av
IUnion.Valueegenskapen snarare än någonValueegenskap som finns på själva unionstypen. Detta är avsett att ge större flexibilitet för befintliga typer (som kan ha sin egenValueegenskap för andra användningsområden) för att implementera mönstret. Men det är besvärligt och inkonsekvent med hur andra medlemmar hittas och används direkt på fackföreningstypen. Ska vi göra en ändring? Några andra alternativ:- Kräv fackliga typer för att exponera en offentlig
Valueegenskap. - Föredrar en offentlig
Valueegenskap om den finns, men återgår till implementeringen om denIUnion.Valueinte är det (liknarGetEnumeratorregler).
- Kräv fackliga typer för att exponera en offentlig
- Den föreslagna unionsdeklarationssyntaxen är inte universellt älskad, särskilt inte när det gäller att uttrycka skiftlägestyperna. Alternativ hittills möter också kritik, men det är möjligt att vi kommer att göra en förändring. Några främsta farhågor uttrycktes om den nuvarande:
- Kommatecken som avgränsare mellan skiftlägestyper kan tyckas antyda att ordningen är viktig.
- Parenteserade listor ser för mycket ut som primära konstruktorer (trots att de inte har parameternamn).
- För olika uppräkningar, som har sina "fall" i klammerparenteser.
- Även om unionsdeklarationer genererar structs med ett enda referensfält, är de fortfarande något mottagliga för oväntat beteende när de används i en samtidig kontext. Om till exempel en användardefinierad funktionsmedlem avrefererar
thismer än en gång, kan den innehållande variabeln ha omtilldelats som helhet av en annan tråd mellan de två åtkomsterna. Kompilatorn kan generera kod för att kopierathistill en lokal när det behövs. Borde det? I allmänhet, vilken grad av samtidighetsåterhämtning är önskvärd och rimligt uppnåelig?
C# feature specifications