Změny porovnávání vzorů pro C# 9.0
Poznámka
Tento článek je specifikace funkce. Specifikace slouží jako návrhový dokument pro funkci. Zahrnuje navrhované změny specifikace spolu s informacemi potřebnými při návrhu a vývoji funkce. Tyto články se publikují, dokud nebudou navrhované změny specifikace finalizovány a začleněny do aktuální specifikace ECMA.
Mezi specifikací funkce a dokončenou implementací může docházet k nějakým nesrovnalostem. Tyto rozdíly jsou zachyceny v poznámkách schůzky návrhu jazyka (LDM), .
Další informace o procesu přijetí specifikací funkcí do jazyka C# najdete v článku o specifikacích .
Zvažujeme malý počet vylepšení porovnávání vzorů pro C# 9.0, které mají přirozenou součinnost a dobře pracují na řešení řady běžných programovacích problémů:
- vzory typů https://github.com/dotnet/csharplang/issues/2925
- https://github.com/dotnet/csharplang/issues/1350 Vzory v závorkách pro vynucení nebo zvýraznění priority nových kombinátorů
-
https://github.com/dotnet/csharplang/issues/1350 Konjunktivní vzory
and
, které vyžadují, aby se oba různé vzory odpovídaly; -
https://github.com/dotnet/csharplang/issues/1350 Disjunktivní
or
vzory, které vyžadují shodu alespoň jednoho ze dvou různých vzorů; -
https://github.com/dotnet/csharplang/issues/1350 negované
not
vzory, které vyžadují, aby se daný vzor nesměl shodovat; a - https://github.com/dotnet/csharplang/issues/812 Relační vzory, které vyžadují, aby vstupní hodnota byla menší než, menší nebo rovna, atd. daná konstanta.
Vzory se závorkami
Závorky umožňují programátorům umístit závorky kolem libovolného vzoru. To není tak užitečné u existujících vzorů v jazyce C# 8.0, ale nové kombinátory vzorů představují prioritu, kterou programátor může chtít přepsat.
primary_pattern
: parenthesized_pattern
| // all of the existing forms
;
parenthesized_pattern
: '(' pattern ')'
;
Vzory typů
Povolujeme typ jako vzor:
primary_pattern
: type-pattern
| // all of the existing forms
;
type_pattern
: type
;
Retroaktivně mění stávající je výraz typu tak, aby byl vzorovým výrazem, ve kterém je vzor typ-vzor, a přitom bychom nezměnili strom syntaxe vytvořený kompilátorem.
Jedním z drobných problémů s implementací je, že tato gramatika je nejednoznačná. Řetězec, jako je například a.b
, lze analyzovat buď jako kvalifikovaný název (v kontextu typu), nebo tečkovaný výraz (v kontextu výrazu). Kompilátor již dokáže zacházet s kvalifikovaným názvem stejným jako tečkovaný výraz, aby zvládl něco jako e is Color.Red
. Sémantická analýza kompilátoru by byla dále rozšířena tak, aby byla schopna vytvořit vazbu (syntaktického) konstantního vzoru (např. tečkovaného výrazu) jako typu, aby bylo možné s ním pracovat jako se vzorem vázaného typu za účelem podpory tohoto konstruktoru.
Po této změně byste mohli psát
void M(object o1, object o2)
{
var t = (o1, o2);
if (t is (int, string)) {} // test if o1 is an int and o2 is a string
switch (o1) {
case int: break; // test if o1 is an int
case System.String: break; // test if o1 is a string
}
}
Relační vzory
Relační vzory umožňují programátoru vyjádřit, že vstupní hodnota musí splňovat relační omezení v porovnání s konstantní hodnotou:
public static LifeStage LifeStageAtAge(int age) => age switch
{
< 0 => LifeStage.Prenatal,
< 2 => LifeStage.Infant,
< 4 => LifeStage.Toddler,
< 6 => LifeStage.EarlyChild,
< 12 => LifeStage.MiddleChild,
< 20 => LifeStage.Adolescent,
< 40 => LifeStage.EarlyAdult,
< 65 => LifeStage.MiddleAdult,
_ => LifeStage.LateAdult,
};
Relační vzory podporují relační operátory <
, <=
, >
a >=
na všech předdefinovaných typech, které podporují takové binární relační operátory se dvěma operandy stejného typu ve výrazu. Konkrétně podporujeme všechny tyto relační vzory pro sbyte
, byte
, short
, ushort
, int
, uint
, long
, ulong
, char
, float
, double
, decimal
, nint
a nuint
.
primary_pattern
: relational_pattern
;
relational_pattern
: '<' relational_expression
| '<=' relational_expression
| '>' relational_expression
| '>=' relational_expression
;
Je vyžadováno, aby výraz byl vyhodnocen na konstantní hodnotu. Jedná se o chybu, pokud je konstantní hodnota double.NaN
nebo float.NaN
. Jedná se o chybu, pokud je výraz konstantou null.
Pokud je vstup typem, pro který existuje vhodný integrovaný binární relační operátor, kde vstup funguje jako levý operand a daná konstanta jako pravý operand, vyhodnocení tohoto operátoru se považuje za význam relačního vzoru. V opačném případě převedeme vstup na typ výrazu pomocí explicitního převodu s možnou hodnotou null nebo rozbalení. Jedná se o chybu v době kompilace, pokud neexistuje žádný takový převod. Model se považuje za neodpovídající, pokud převod selže. Pokud převod proběhne úspěšně, výsledkem operace porovnávání vzorů je výsledek vyhodnocení výrazu e OP v
, kde e
je převedený vstup, OP
je relační operátor a v
je konstantní výraz.
Kombinátory vzorů
Kombinátory vzorů umožňují shodovat oba různé vzory pomocí and
(to lze rozšířit na libovolný počet vzorů opakovaným použitím and
), jeden ze dvou různých vzorů pomocí or
(totéž), nebo negaci vzoru pomocí not
.
Běžným použitím kombinátoru bude idiom
if (e is not null) ...
Čitelnější než aktuální idiom e is object
, tento vzor jasně vyjadřuje, že jeden kontroluje nenulovou hodnotu.
Kombinátory and
a or
budou užitečné pro testování rozsahů hodnot.
bool IsLetter(char c) => c is >= 'a' and <= 'z' or >= 'A' and <= 'Z';
Tento příklad ukazuje, že and
bude mít vyšší prioritu analýzy (tj. bude vázat blíže) než or
. Programátor může použít závorkový vzor k explicitnímu nastavení priority:
bool IsLetter(char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z');
Stejně jako všechny vzory lze tyto kombinátory použít v libovolném kontextu, ve kterém se očekává vzor, včetně vnořených vzorů, je vzorový výraz, výraz přepínačea vzor označení případu příkazu switch.
pattern
: disjunctive_pattern
;
disjunctive_pattern
: disjunctive_pattern 'or' conjunctive_pattern
| conjunctive_pattern
;
conjunctive_pattern
: conjunctive_pattern 'and' negated_pattern
| negated_pattern
;
negated_pattern
: 'not' negated_pattern
| primary_pattern
;
primary_pattern
: // all of the patterns forms previously defined
;
Změna na 6.2.5 Gramatické nejednoznačnosti
Vzhledem k zavedení vzoru typu je možné, aby se obecný typ zobrazil před tokenem =>
. Proto přidáme =>
do sady tokenů uvedených v §6.2.5 Gramatické nejednoznačnosti, abychom umožnili odstranění nejednoznačnosti <
, která začíná seznamem typových argumentů. Viz také https://github.com/dotnet/roslyn/issues/47614.
Otevřené problémy s navrhovanými změnami
Syntaxe relačních operátorů
Jsou and
, or
a not
nějaké kontextové klíčové slovo? Pokud ano, dojde k výrazné změně (např. ve srovnání s jejich použitím jako označovatele ve vzoru deklarace
Sémantika (např. typ) pro relační operátory
Očekáváme podporu všech primitivních typů, které je možné porovnat ve výrazu pomocí relačního operátoru. Význam v jednoduchých případech je jasný.
bool IsValidPercentage(int x) => x is >= 0 and <= 100;
Ale když vstup není takový primitivní typ, na jaký typ se ho pokusíme převést?
bool IsValidPercentage(object x) => x is >= 0 and <= 100;
Navrhli jsme, že pokud je vstupní typ již srovnatelným primitivem, jedná se o typ porovnání. Pokud však vstup není srovnatelné primitivum, považujeme relaci za zahrnující implicitní test typu na typ konstanty na pravé straně relace. Pokud programátor hodlá podporovat více než jeden typ vstupu, musí to být provedeno explicitně:
bool IsValidPercentage(object x) => x is
>= 0 and <= 100 or // integer tests
>= 0F and <= 100F or // float tests
>= 0D and <= 100D; // double tests
Výsledek: Relační operátor zahrnuje implicitní test typu ve vztahu k typu konstanty na pravé straně operátoru.
Tok informací o typu zleva doprava od and
Bylo navrženo, že když napíšete kombinátor and
, informace o typu získané nalevo o nejvyšší úrovni typu by mohly proudit doprava. Například
bool isSmallByte(object o) => o is byte and < 100;
Zde je vstupní typ na druhý vzor zúžen typ zužující požadavky nalevo od and
. Nadefinujeme typ zúžení sémantiky pro všechny vzory následujícím způsobem. Zúžený typ vzoruP
je definován takto:
- Pokud je
P
vzorem typu, pak zúžený typ je typ tohoto vzoru. - Pokud
P
je vzor deklarace, zúžený typ je typ modelu deklarace. - Pokud
P
je rekurzivní vzor, který dává explicitní typ, zúžený typ je tento typ. - Je-li
spárován podle pravidel pro , typ, který je zúžený na , je typu . - Pokud je
P
konstantní vzor, v němž konstanta není nulová konstanta a kde výraz nemá žádný převod konstantního výrazu na vstupní typ , pak zúžený typ podle je typem konstanty. - Pokud je
P
relačním vzorem, v němž konstantní výraz nemá žádnou možnost převést na vstupní typ, pak je zúžený typ typem této konstanty. - Je-li
P
vzoremor
, zúžený typ je běžným typem zúžený typ podpatternů, pokud takový společný typ existuje. Pro tento účel algoritmus obecného typu bere v úvahu pouze konverze identity, boxování a implicitního odkazu a zvažuje všechny subpatterny posloupnosti vzorůor
, s ignorováním závorek. - Pokud je
vzorem , pak je zúžený typ pro takový, že odpovídá zúženému typu pravého vzoru . zúžený typ levého vzoru je vstupním typem pravého vzoru. - V opačném případě je vstupní typ
zúženým typem .
Výsledek: Výše uvedená zúžení sémantiky byla implementována.
Definice proměnných a určité přiřazení
Přidání vzorů or
a not
vytváří zajímavé nové problémy související s proměnnými vzorů a určitým přiřazením. Vzhledem k tomu, že proměnné lze běžně deklarovat pouze jednou, zdá se, že jakákoli proměnná ve vzoru deklarovaná na jedné straně or
vzoru by nebyla jednoznačně přiřazena při shodě vzorů. Podobně by proměnná deklarovaná uvnitř vzoru not
neměla být při porovnávání vzorů rozhodně přiřazena. Nejjednodušším způsobem, jak to vyřešit, je zakázat deklarování proměnných vzorů v těchto kontextech. To však může být příliš omezující. Existují další přístupy, které je potřeba zvážit.
Jeden scénář, který stojí za zvážení, je to
if (e is not int i) return;
M(i); // is i definitely assigned here?
To dnes nefunguje, protože pro je vzorový výraz, proměnné vzorů jsou považovány za rozhodně přiřazeny pouze tam, kde je vzorový výraz true ("rozhodně přiřazena, když je true").
Podpora by byla jednodušší (z pohledu programátora) než přidání podpory pro příkaz if
s negovanou podmínkou. I když takovou podporu přidáme, programátoři by zajímali, proč výše uvedený fragment kódu nefunguje. Na druhou stranu, stejný scénář v switch
dává méně smysl, protože neexistuje odpovídající bod v programu, kde by byl jednoznačně přiřazen, kdy by false bylo smysluplné. Povolili bychom to ve výrazu vzoru is, ale ne v jiných kontextech, kde jsou vzory povoleny? Zdá se to nepravidelné.
To souvisí s problémem definitivního přiřazení v disjunktivním vzoru .
if (e is 0 or int i)
{
M(i); // is i definitely assigned here?
}
Očekáváme, že i
bude s jistotou přiřazena, když vstup není nulový. Ale vzhledem k tomu, že nevíme, jestli je vstup v bloku nulový nebo ne, i
není rozhodně přiřazen. Co když ale povolíme, aby i
bylo deklarováno v různých vzájemně se vylučujících vzorech?
if ((e1, e2) is (0, int i) or (int i, 0))
{
M(i);
}
Tady je proměnná i
rozhodně přiřazena uvnitř bloku a přebírá hodnotu z druhého prvku řazené kolekce členů při nalezení nulového prvku.
Bylo také navrženo povolit definování proměnných (násobení) v každém případě bloku případu:
case (0, int x):
case (int x, 0):
Console.WriteLine(x);
Abychom mohli tuto práci provést, museli bychom pečlivě definovat, kde je povoleno takové více definic a za jakých podmínek je taková proměnná považována za rozhodně přiřazenou.
Měli bychom se rozhodnout, že tuto práci odložíme až později (což radím), můžeme říci v jazyce C# 9.
- Pod
not
neboor
nesmí být deklarovány proměnné vzoru.
Pak bychom měli čas vyvinout některé zkušenosti, které by poskytovaly přehled o možné hodnotě uvolnění později.
Výsledek: Proměnné vzoru nelze deklarovat pod vzorem not
nebo or
.
Diagnostika, subsumpce a úplnost
Tyto nové vzorové formuláře představují mnoho nových příležitostí pro diagnostikovatelnou chybu programátora. Budeme se muset rozhodnout, jaké druhy chyb budeme diagnostikovat a jak to udělat. Tady je několik příkladů:
case >= 0 and <= 100D:
Tento případ se nikdy neshoduje (protože vstup nemůže být int
i double
). Už máme chybu, když zjistíme případ, který se nemůže nikdy shodovat, ale jeho formulace ("Případ přepínače už byl zpracován předchozím případem" a "Vzor už byl zpracován předchozím ramenem výrazu přepínače") může být zavádějící v nových scénářích. Možná budeme muset upravit formulaci tak, aby se jen říkalo, že se vzor nikdy neshoduje se vstupem.
case 1 and 2:
Podobně by to byla chyba, protože hodnota nemůže být 1
i 2
.
case 1 or 2 or 3 or 1:
Tento případ lze sladit, ale or 1
na konci nepřidává vzoru žádný význam. Navrhuji, abychom měli usilovat o vytvoření chyby, kdykoli nějaká spojka nebo disjunkce složeného vzoru ani nedefinuje proměnnou vzoru, ani neovlivňuje množinu odpovídajících hodnot.
case < 2: break;
case 0 or 1 or 2 or 3 or 4 or 5: break;
V tomto případě 0 or 1 or
k druhému případu nic přidá, protože tyto hodnoty by byly zpracovány prvním případem. To si také zaslouží označení jako chyba.
byte b = ...;
int x = b switch { <100 => 0, 100 => 1, 101 => 2, >101 => 3 };
Výraz přepínače, jako je tento, by se měl považovat za vyčerpávající (zpracovává všechny možné vstupní hodnoty).
V jazyce C# 8.0 je výraz přepínače se vstupem typu byte
považován za vyčerpávající pouze v případě, že obsahuje konečnou paži, jejíž vzor odpovídá všemu (vzor zahození nebo vzor var-pattern). I výraz přepínače, který má rameno pro každou jedinečnou byte
hodnotu, se v jazyce C# 8 nepovažuje za vyčerpávající. Abychom mohli správně zpracovat úplnost relačních vzorů, budeme muset tento případ zpracovat také. Technicky to bude zásadní změna, ale žádný uživatel si pravděpodobně nevšimne.
C# feature specifications