Zmiany dopasowania wzorca dla języka C# 9.0
Notatka
Ten artykuł jest specyfikacją funkcji. Specyfikacja służy jako dokument projektowy dla funkcji. Zawiera proponowane zmiany specyfikacji wraz z informacjami wymaganymi podczas projektowania i opracowywania funkcji. Te artykuły są publikowane do momentu sfinalizowania proponowanych zmian specyfikacji i włączenia ich do obecnej specyfikacji ECMA.
Mogą wystąpić pewne rozbieżności między specyfikacją funkcji a ukończoną implementacją. Te różnice są przechwytywane w odpowiednich spotkania projektowego języka (LDM).
Więcej informacji na temat procesu wdrażania specyfikacji funkcji można znaleźć w standardzie języka C# w artykule dotyczącym specyfikacji .
Rozważamy niewielką liczbę ulepszeń dopasowywania wzorców dla języka C# 9.0, które mają naturalną synergię i działają dobrze, aby rozwiązać szereg typowych problemów programistycznych:
- wzorce typów https://github.com/dotnet/csharplang/issues/2925
- https://github.com/dotnet/csharplang/issues/1350 wzorce z nawiasów, aby wymusić lub podkreślić pierwszeństwo nowych kombinatorów
-
https://github.com/dotnet/csharplang/issues/1350 wzorce spójnikowe
and
, które wymagają dopasowania obu różnych wzorców; -
https://github.com/dotnet/csharplang/issues/1350 Wzorce
or
disjunktywne, które wymagają dopasowania jednego z dwóch różnych wzorców; -
https://github.com/dotnet/csharplang/issues/1350 Negowane
not
wzorce, które wymagają, aby dany wzorzec nie pasował, i - https://github.com/dotnet/csharplang/issues/812 Wzorce relacyjne wymagające, aby wartość wejściowa była mniejsza niż, mniejsza lub równa danej stałej, itp.
Wzorce z nawiasami
Wzorce nawiasów umożliwiają programisty umieszczanie nawiasów wokół dowolnego wzorca. Nie jest to aż tak przydatne w przypadku istniejących wzorców w języku C# 8.0, jednak nowe kombinatory wzorców wprowadzają pierwszeństwo, które programista może chcieć zastąpić.
primary_pattern
: parenthesized_pattern
| // all of the existing forms
;
parenthesized_pattern
: '(' pattern ')'
;
Wzorce typów
Dopuszczamy użycie typu jako wzorca.
primary_pattern
: type-pattern
| // all of the existing forms
;
type_pattern
: type
;
To przekształca istniejące wyrażenie jest-wyrażeniem-typu na jest-wyrażeniem-wzorca, w którym wzorzec jest wzorcem-typu, chociaż nie zmienilibyśmy drzewa składni generowanego przez kompilator.
Jednym z subtelnych problemów z implementacją jest to, że ta gramatyka jest niejednoznaczna. Ciąg, taki jak a.b
, można przeanalizować jako kwalifikowaną nazwę (w kontekście typu) lub wyrażenie kropkowane (w kontekście wyrażenia). Kompilator jest już w stanie traktować kwalifikowaną nazwę tak samo jak wyrażenie kropkowane w celu obsługi czegoś takiego jak e is Color.Red
. Semantyczna analiza kompilatora zostałaby dodatkowo rozszerzona, aby móc powiązać wzorzec stały (składniowy) (np. wyrażenie kropkowane) jako typ, aby traktować go jako wzorzec typu powiązanego w celu obsługi tej konstrukcji.
Po tej zmianie będzie można napisać
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
}
}
Wzorce relacyjne
Wzorce relacyjne umożliwiają programistom wyrażenie, że wartość wejściowa musi spełniać ograniczenie relacyjne w porównaniu z wartością stałą:
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,
};
Wzorce relacyjne obsługują operatory relacyjne <
, <=
, >
i >=
na wszystkich wbudowanych typach, które obsługują takie operatory relacyjne binarne z dwoma operandami tego samego typu w wyrażeniu. W szczególności obsługujemy wszystkie te wzorce relacyjne dla sbyte
, byte
, short
, ushort
, int
, uint
, long
, ulong
, char
, float
, double
, decimal
, nint
i nuint
.
primary_pattern
: relational_pattern
;
relational_pattern
: '<' relational_expression
| '<=' relational_expression
| '>' relational_expression
| '>=' relational_expression
;
Wyrażenie jest wymagane do obliczenia wartości stałej. Jest to błąd, jeśli wartość stała jest double.NaN
lub float.NaN
. Jest to błąd, jeśli wyrażenie jest stałą null.
Gdy dane wejściowe są typem, dla którego zdefiniowany jest odpowiedni wbudowany operator relacyjny binarny, mający zastosowanie z danymi wejściowymi jako lewym operatorem, a daną stałą jako prawym operatorem, wynik działania tego operatora jest traktowany jako znaczenie wzorca relacyjnego. W przeciwnym razie konwertujemy dane wejściowe na typ wyrażenia przy użyciu jawnej konwersji dopuszczającej wartość null lub rozpatłaniającej. Jest to błąd czasu kompilacji, jeśli taka konwersja nie istnieje. Wzorzec jest uznawany za niezgodny, jeśli konwersja nie powiedzie się. Jeśli konwersja powiedzie się, wynikiem operacji dopasowywania wzorca jest ocena wyrażenia e OP v
gdzie e
jest przekonwertowanym wejściem, OP
jest operatorem relacyjnym, a v
jest wyrażeniem stałym.
Kombinatory wzorców
Kombinatory wzorca pozwalają na dopasowanie obu różnych wzorców przy użyciu and
(można to rozszerzyć na dowolną liczbę wzorców przez wielokrotne użycie and
), jednego z dwóch różnych wzorców przy użyciu or
(tak samo), lub negacji wzorca przy użyciu not
.
Typowym zastosowaniem kombinatora będzie idiom
if (e is not null) ...
Bardziej czytelny niż obecny idiom e is object
, ten wzorzec jasno pokazuje, że chodzi o sprawdzenie, czy wartość jest różna od null.
Kombinatory and
i or
będą przydatne do testowania zakresów wartości
bool IsLetter(char c) => c is >= 'a' and <= 'z' or >= 'A' and <= 'Z';
W tym przykładzie pokazano, że and
będzie mieć wyższy priorytet analizowania (tj. będzie wiązać się ściślej) niż or
. Programista może użyć wzorca nawiasów, aby jawnie określić pierwszeństwo:
bool IsLetter(char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z');
Podobnie jak wszystkie wzorce, te kombinatory mogą być używane w dowolnym kontekście, w którym oczekiwano wzorca, w tym zagnieżdżone wzorce, wyrażenie-wzorzec-wzorzec, wyrażenie-przełącznikai wzorzec etykiety wielkości liter instrukcji 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
;
Przejdź do 6.2.5 Niejasności gramatyczne
Ze względu na wprowadzenie wzorca typu , typ ogólny może pojawić się przed tokenem =>
. Dodajemy więc =>
do zestawu tokenów wymienionych w §6.2.5 Niejednoznaczności gramatyczne, aby umożliwić rozważenie niejednoznaczności <
rozpoczynającej listę argumentów typu. Zobacz również https://github.com/dotnet/roslyn/issues/47614.
Otwarte problemy z proponowanymi zmianami
Składnia dla operatorów relacyjnych
Czy and
, or
i not
jakiś kontekstowe słowo kluczowe? Jeśli tak, istnieje zmiana wprowadzająca niezgodność (np. w porównaniu z ich użyciem jako znacznik w wzorcu deklaracyjnym).
Semantyka (np. typ) dla operatorów relacyjnych
Oczekujemy obsługi wszystkich typów pierwotnych, które można porównać w wyrażeniu przy użyciu operatora relacyjnego. Znaczenie w prostych przypadkach jest jasne
bool IsValidPercentage(int x) => x is >= 0 and <= 100;
Ale jeśli dane wejściowe nie są takim typem pierwotnym, jaki typ próbujemy przekonwertować na?
bool IsValidPercentage(object x) => x is >= 0 and <= 100;
Zaproponowaliśmy, aby gdy typ danych wejściowych jest już porównywalnym elementem pierwotnym, jest to typ porównania. Jeśli jednak dane wejściowe nie są porównywalnym elementem pierwotnym, traktujemy wyrażenie relacyjne jako zawierające niejawny test typu w stosunku do typu stałej po prawej stronie wyrażenia. Jeśli programista zamierza obsługiwać więcej niż jeden typ danych wejściowych, należy to zrobić jawnie:
bool IsValidPercentage(object x) => x is
>= 0 and <= 100 or // integer tests
>= 0F and <= 100F or // float tests
>= 0D and <= 100D; // double tests
Wynik: relacyjna operacja zawiera niejawne sprawdzanie typu stałej po prawej stronie wyrażenia relacyjnego.
Przepływające informacje o typie od lewej do prawej strony and
Sugerowano, że podczas pisania kombinatora and
informacje o typie najwyższego poziomu, poznane po lewej stronie, mogłyby przepływać na prawą stronę. Na przykład
bool isSmallByte(object o) => o is byte and < 100;
Wejściowy typ do drugiego wzorca jest zawężany przez wymagania zawężające typu po lewej stronie and
. Zdefiniowalibyśmy semantykę zawężania typu dla wszystkich wzorców w następujący sposób.
zawęziony typ wzorca P
jest definiowany w następujący sposób:
- Jeśli
P
jest wzorcem typu, zawężony typ jest typem typu wzorca typu. - Jeśli
P
jest wzorcem deklaracji, zawężony typ jest typem typu wzorca deklaracji. - Jeśli
P
jest wzorcem rekursywnym, który daje jawny typ, zawęziony typ jest tym typem. - Jeśli
P
jest dopasowywana za pośrednictwem regułITuple
, zawężonego typu jest typemSystem.Runtime.CompilerServices.ITuple
. - Jeśli
P
jest stałym wzorcem, w którym stała nie jest stałą o wartości null i wyrażenie nie ma konwersji wyrażenia stałego do typu wejściowego , to typ zawężony jest typem stałej. - Jeśli
P
jest wzorcem relacyjnym, w którym wyrażenie stałe nie ma konwersji wyrażenia stałego do typu wejściowego , zawężony typ jest typem tej stałej. - Jeśli
P
jest wzorcemor
, zawężony typ jest wspólnym typem dla zawężonego typu podwzorców, jeśli taki wspólny typ istnieje. W tym celu algorytm wspólnego typu uwzględnia tylko konwersje tożsamości, boksu i niejawnych odwołań oraz uwzględnia wszystkie podwzory sekwencji wzorcówor
(ignorując wzorce nawiasów). - Jeśli
P
jest wzorcemand
, to zawężony typ jest zawężonym typem dla właściwego wzorca. Ponadto zawężony typ wzorca po lewej stronie jest typem wejściowym wzorca po prawej stronie . - W przeciwnym razie zawężony typ
P
jest typem wejściowymP
.
Wynik: Powyższe zawężenie semantyki zostało zaimplementowane.
Definicje zmiennych i określone przypisanie
Dodanie wzorców or
i not
tworzy kilka interesujących nowych problemów dotyczących zmiennych wzorca i określonego przypisania. Ponieważ zmienne mogą być deklarowane zwykle co najwyżej raz, wydaje się, że każda zmienna wzorca zadeklarowana po jednej stronie wzorca or
nie jest jednoznacznie przypisana, gdy wzorzec pasuje. Podobnie zmienna zadeklarowana wewnątrz wzorca not
nie powinna być zdecydowanie przypisana, gdy wzorzec pasuje. Najprostszym sposobem rozwiązania tego problemu jest zakaz deklarowanie zmiennych wzorca w tych kontekstach. Jednak może to być zbyt restrykcyjne. Istnieją inne podejścia do rozważenia.
Jednym z scenariuszy, które warto wziąć pod uwagę, jest to
if (e is not int i) return;
M(i); // is i definitely assigned here?
Nie działa to dzisiaj, ponieważ dla wyrażenia-wzorca zmienne tego wzorca są uznawane za zdecydowanie przypisane jedynie tam, gdzie wyrażenie-wzorzec jest prawdziwe ("zdecydowanie przypisane, gdy spełnione").
Obsługa tej instrukcji byłaby prostsza (z perspektywy programisty) niż również dodanie obsługi negowanego warunku if
. Nawet jeśli dodamy taką obsługę, programiści zastanawialiby się, dlaczego powyższy fragment kodu nie działa. Z drugiej strony, ten sam scenariusz w switch
ma mniej sensu, ponieważ nie ma odpowiedniego punktu w programie, w którym zdecydowanie przypisane, gdy fałszywe byłoby znaczące. Czy zezwolimy na to w wyrażeniach z wzorcem -, ale nie w innych kontekstach, gdzie wzorce są dozwolone? Wydaje się to nieregularne.
Związane z tym jest problem określonego przypisania w disjunctive-pattern.
if (e is 0 or int i)
{
M(i); // is i definitely assigned here?
}
Spodziewalibyśmy się, że i
na pewno zostanie przypisana, gdy dane wejściowe nie są zerowe. Ale ponieważ nie wiemy, czy dane wejściowe są zerowe, czy nie wewnątrz bloku, i
nie jest zdecydowanie przypisane. Co jednak zrobić, jeśli zezwolimy na deklarowanie i
w różnych wzajemnie wykluczających się wzorcach?
if ((e1, e2) is (0, int i) or (int i, 0))
{
M(i);
}
W tym miejscu zmienna i
jest jednoznacznie przypisana wewnątrz bloku i przyjmuje wartość z innego elementu krotki, kiedy zostanie znaleziony element zerowy.
Zaleca się również zezwolenie na definiowanie zmiennych (pomnożenie) w każdym przypadku bloku przypadku:
case (0, int x):
case (int x, 0):
Console.WriteLine(x);
Aby wykonać dowolną z tych czynności, musimy dokładnie zdefiniować, gdzie takie definicje są dozwolone i w jakich warunkach taka zmienna jest uważana za zdecydowanie przypisaną.
Jeśli zdecydujemy się odroczyć taką pracę na później (co doradzam), moglibyśmy powiedzieć w kontekście C# 9
- pod
not
lubor
nie można zadeklarować zmiennych wzorca.
Następnie mielibyśmy czas, aby zdobyć doświadczenie, które umożliwiłoby zrozumienie możliwej wartości poluzowania tego później.
Wynik: zmiennych wzorca nie można zadeklarować pod wzorcem not
ani or
.
Diagnostyka, subsumpcja i kompletność
Te nowe wzorce otwierają wiele nowych możliwości błędów programisty do diagnozowania. Musimy zdecydować, jakie rodzaje błędów będziemy diagnozować i jak to zrobić. Oto kilka przykładów:
case >= 0 and <= 100D:
Ten przypadek nigdy nie może być zgodny (ponieważ dane wejściowe nie mogą być zarówno int
, jak i double
). Mamy już błąd, gdy wykryjemy przypadek, który nigdy nie może być zgodny, ale jego sformułowanie ("Przypadek przełącznika został już obsłużony przez poprzedni przypadek" i "Wzorzec został już obsłużony przez poprzednie ramię wyrażenia przełącznika") może być mylący w nowych scenariuszach. Możemy zmodyfikować sformułowanie, aby po prostu powiedzieć, że wzorzec nigdy nie będzie pasować do danych wejściowych.
case 1 and 2:
Podobnie jest to błąd, ponieważ wartość nie może być 1
i 2
.
case 1 or 2 or 3 or 1:
Ten przypadek jest możliwy do dopasowania, ale or 1
na końcu nie dodaje znaczenia do wzorca. Proponuję, abyśmy dążyli do wykrywania błędu zawsze, gdy jakiś spójnik lub rozłącznik wzorca złożonego nie definiuje zmiennej wzorca ani nie wpływa na zestaw dopasowanych wartości.
case < 2: break;
case 0 or 1 or 2 or 3 or 4 or 5: break;
Tutaj 0 or 1 or
nie dodaje nic do drugiego przypadku, ponieważ te wartości byłyby obsłużone już przez pierwszy przypadek. To także zasługuje na uznanie za błąd.
byte b = ...;
int x = b switch { <100 => 0, 100 => 1, 101 => 2, >101 => 3 };
Wyrażenie przełącznika, takie jak to, należy uznać za wyczerpujące (obsługuje wszystkie możliwe wartości wejściowe).
W języku C# 8.0 wyrażenie przełącznika z danymi wejściowymi typu byte
jest uznawane za wyczerpujące tylko wtedy, gdy zawiera ostateczne ramię pasujące do wszystkiego (wzorzec odrzucania lub wzorzec zmiennej ). Nawet wyrażenie przełącznika, które ma ramię dla każdej odrębnej wartości byte
, nie jest uważane za wyczerpujące w języku C# 8. Aby poprawnie obsługiwać pełnię wzorców relacyjnych, będziemy musieli również obsłużyć ten przypadek. Technicznie rzecz biorąc, będzie to zmiana powodująca niezgodność, ale prawdopodobnie żaden użytkownik tego nie zauważy.
C# feature specifications