C# 9.0 のパターン一致の変更Pattern-matching changes for C# 9.0

自然な相乗効果を備え、一般的なプログラミング上の問題に対処するためにうまく機能する、C# 9.0 のパターンマッチングに対するいくつかの拡張を検討しています。We are considering a small handful of enhancements to pattern-matching for C# 9.0 that have natural synergy and work well to address a number of common programming problems:

  • https://github.com/dotnet/csharplang/issues/2925 型パターンhttps://github.com/dotnet/csharplang/issues/2925 Type patterns
  • https://github.com/dotnet/csharplang/issues/1350 新しい連結子の優先順位を適用または強調するための、かっこで囲まれたパターンhttps://github.com/dotnet/csharplang/issues/1350 Parenthesized patterns to enforce or emphasize precedence of the new combinators
  • https://github.com/dotnet/csharplang/issues/1350and2 つの異なるパターンの両方を必要とする接続パターンhttps://github.com/dotnet/csharplang/issues/1350 Conjunctive and patterns that require both of two different patterns to match;
  • https://github.com/dotnet/csharplang/issues/1350or2 つの異なるパターンが一致する必要がある選言パターンhttps://github.com/dotnet/csharplang/issues/1350 Disjunctive or patterns that require either of two different patterns to match;
  • https://github.com/dotnet/csharplang/issues/1350not指定されたパターンが一致し ない ことを必要とする否定パターンhttps://github.com/dotnet/csharplang/issues/1350 Negated not patterns that require a given pattern not to match; and
  • https://github.com/dotnet/csharplang/issues/812 入力値が指定された定数よりも小さいか、同じか、または等しいかを必要とするリレーショナルパターン。https://github.com/dotnet/csharplang/issues/812 Relational patterns that require the input value to be less than, less than or equal to, etc a given constant.

かっこで囲まれるパターンParenthesized Patterns

かっこで囲まれたパターンでは、プログラマは任意のパターンをかっこで囲むことができます。Parenthesized patterns permit the programmer to put parentheses around any pattern. これは、C# 8.0 の既存のパターンでは役に立ちませんが、新しいパターン連結子によって、プログラマがオーバーライドする可能性のある優先順位が導入されます。This is not so useful with the existing patterns in C# 8.0, however the new pattern combinators introduce a precedence that the programmer may want to override.

primary_pattern
    : parenthesized_pattern
    | // all of the existing forms
    ;
parenthesized_pattern
    : '(' pattern ')'
    ;

型パターンType Patterns

パターンとして型を許可します。We permit a type as a pattern:

primary_pattern
    : type-pattern
    | // all of the existing forms
    ;
type_pattern
    : type
    ;

これにより、既存の 型指定式 は、パターンが 型パターン である というパターン式 になります。ただし、コンパイラによって生成される構文ツリーは変更されません。This retcons the existing is-type-expression to be an is-pattern-expression in which the pattern is a type-pattern, though we would not change the syntax tree produced by the compiler.

1つの微妙な実装の問題は、この文法があいまいであることです。One subtle implementation issue is that this grammar is ambiguous. などの文字列は、 a.b 修飾名 (型コンテキストの場合) または点線の式 (式のコンテキストの場合) として解析できます。A string such as a.b can be parsed either as a qualified name (in a type context) or a dotted expression (in an expression context). コンパイラは、のような処理を行うために、修飾名をドット式と同じように扱うことが e is Color.Red できます。The compiler is already capable of treating a qualified name the same as a dotted expression in order to handle something like e is Color.Red. コンパイラのセマンティック分析はさらに拡張され、このコンストラクトをサポートするためにバインドされた型パターンとして処理するために (構文的な式など) を型としてバインドできるようになります。The compiler's semantic analysis would be further extended to be capable of binding a (syntactic) constant pattern (e.g. a dotted expression) as a type in order to treat it as a bound type pattern in order to support this construct.

この変更が完了すると、次のように記述できるようになります。After this change, you would be able to write

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
    }
}

リレーショナルパターンRelational Patterns

リレーショナルパターンでは、定数値と比較したときに、入力値がリレーショナル制約を満たす必要があることをプログラマが表現できます。Relational patterns permit the programmer to express that an input value must satisfy a relational constraint when compared to a constant value:

    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,
    };

リレーショナルパターンでは、 < <= > >= 式内で同じ型の2つのオペランドを持つ2つのオペランドを使用して、このような二項関係演算子をサポートするすべての組み込み型に対して、、、およびの関係演算子がサポートされています。Relational patterns support the relational operators <, <=, >, and >= on all of the built-in types that support such binary relational operators with two operands of the same type in an expression. 具体的には、、、、、、、、、、、、、、およびのすべてのリレーショナルパターンをサポート sbyte byte short ushort int して uint long ulong char float double decimal nint nuint います。Specifically, we support all of these relational patterns for sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, nint, and nuint.

primary_pattern
    : relational_pattern
    ;
relational_pattern
    : '<' relational_expression
    | '<=' relational_expression
    | '>' relational_expression
    | '>=' relational_expression
    ;

式は、定数値に評価される必要があります。The expression is required to evaluate to a constant value. 定数値がまたはの場合、エラーになり double.NaN float.NaN ます。It is an error if that constant value is double.NaN or float.NaN. 式が null 定数の場合、エラーになります。It is an error if the expression is a null constant.

入力が、オペランドとして入力に適用される適切な組み込みバイナリ関係演算子が定義されている型、およびその右オペランドとして指定された定数は、その演算子の評価がリレーショナルパターンの意味として取得されます。When the input is a type for which a suitable built-in binary relational operator is defined that is applicable with the input as its left operand and the given constant as its right operand, the evaluation of that operator is taken as the meaning of the relational pattern. それ以外の場合は、明示的な null 許容型変換またはボックス化解除変換を使用して、入力を式の型に変換します。Otherwise we convert the input to the type of the expression using an explicit nullable or unboxing conversion. このような変換が存在しない場合、コンパイル時エラーになります。It is a compile-time error if no such conversion exists. 変換に失敗した場合、パターンは一致しないと見なされます。The pattern is considered not to match if the conversion fails. 変換が成功した場合、パターン一致演算の結果は、式を評価した結果として返され e OP v e ます。ここで、は変換された入力、 OP は関係演算子、は v 定数式です。If the conversion succeeds then the result of the pattern-matching operation is the result of evaluating the expression e OP v where e is the converted input, OP is the relational operator, and v is the constant expression.

パターン連結子Pattern Combinators

パターン 連結 者は、(ditto) を使用する2つの異なるパターン、またはを使用したパターンの否定のいずれかで、を使用して2つの異なるパターンの両方を一致 and させることができます (これは、の繰り返し使用によって任意の数のパターンに拡張できます and ) or notPattern combinators permit matching both of two different patterns using and (this can be extended to any number of patterns by the repeated use of and), either of two different patterns using or (ditto), or the negation of a pattern using not.

連結子の一般的な使用法は、A common use of a combinator will be the idiom

if (e is not null) ...

このパターンは、現在の表現方法よりも読みやすく e is object 、null 以外の値をチェックしていることを明確に表しています。More readable than the current idiom e is object, this pattern clearly expresses that one is checking for a non-null value.

と連結子は、 and or 値の範囲をテストするために役立ちます。The and and or combinators will be useful for testing ranges of values

bool IsLetter(char c) => c is >= 'a' and <= 'z' or >= 'A' and <= 'Z';

この例では、が and より高い解析優先順位を持つことを示しています (つまり、より密接にバインドされます) orThis example illustrates that and will have a higher parsing priority (i.e. will bind more closely) than or. プログラマは、かっこで 囲ま れたパターンを使用して、優先順位を明示的に設定できます。The programmer can use the parenthesized pattern to make the precedence explicit:

bool IsLetter(char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z');

すべてのパターンと同様に、これらの連結子は、パターンが想定されている任意のコンテキストで使用できます。これには、入れ子になったパターン、 パターン式switch 式、switch ステートメントの case ラベルのパターンなどが含まれます。Like all patterns, these combinators can be used in any context in which a pattern is expected, including nested patterns, the is-pattern-expression, the switch-expression, and the pattern of a switch statement's case label.

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
    ;

7.5.4.2 文法のあいまいさを修正するChange to 7.5.4.2 Grammar Ambiguities

型パターン の導入により、ジェネリック型をトークンの前に記述できる可能性があり => ます。Due to the introduction of the type pattern, it is possible for a generic type to appear before the token =>. したがって => 、型引数リストを開始するの非不明瞭を許可するために、 7.5.4.2 文法のあいまい さに記載されているトークンのセットにを追加し < ます。We therefore add => to the set of tokens listed in 7.5.4.2 Grammar Ambiguities to permit disambiguation of the < that begins the type argument list. 参照 https://github.com/dotnet/roslyn/issues/47614.See also https://github.com/dotnet/roslyn/issues/47614.

提案された変更に関する問題を開くOpen Issues with Proposed Changes

関係演算子の構文Syntax for relational operators

andor 、および not 何らかのコンテキストキーワードですか。Are and, or, and not some kind of contextual keyword? その場合は、互換性に影響する変更があります (たとえば、 宣言パターン での指定子としての使用と比較します)。If so, is there a breaking change (e.g. compared to their use as a designator in a declaration-pattern).

関係演算子のセマンティクス (例: 型)Semantics (e.g. type) for relational operators

関係演算子を使用して、式で比較できるプリミティブ型をすべてサポートすることを想定しています。We expect to support all of the primitive types that can be compared in an expression using a relational operator. 単純なケースの意味は明確です。The meaning in simple cases is clear

bool IsValidPercentage(int x) => x is >= 0 and <= 100;

しかし、入力がそのようなプリミティブ型ではない場合、どのような型に変換しようとしていますか。But when the input is not such a primitive type, what type do we attempt to convert it to?

bool IsValidPercentage(object x) => x is >= 0 and <= 100;

入力型が既に同等のプリミティブである場合は、比較の型であることが提案されています。We have proposed that when the input type is already a comparable primitive, that is the type of the comparison. ただし、入力が同等のプリミティブではない場合は、暗黙的な型のテストを含むリレーショナルを、リレーショナルの右側の定数の型に処理します。However, when the input is not a comparable primitive, we treat the relational as including an implicit type test to the type of the constant on the right-hand-side of the relational. 複数の入力型をサポートする必要がある場合は、明示的に実行する必要があります。If the programmer intends to support more than one input type, that must be done explicitly:

bool IsValidPercentage(object x) => x is
    >= 0 and <= 100 or    // integer tests
    >= 0F and <= 100F or  // float tests
    >= 0D and <= 100D;    // double tests

左から右への型情報のフロー andFlowing type information from the left to the right of and

連結子を記述するときに and 、最上位の型について説明した型情報を右側に渡すことをお勧めします。It has been suggested that when you write an and combinator, type information learned on the left about the top-level type could flow to the right. 次に例を示します。For example

bool isSmallByte(object o) => o is byte and < 100;

ここでは、2番目のパターンへの 入力型 が、の左側の 型縮小 の要件によって絞り込まれてい and ます。Here, the input type to the second pattern is narrowed by the type narrowing requirements of left of the and. 次のように、すべてのパターンの型の縮小セマンティクスを定義します。We would define type narrowing semantics for all patterns as follows. パターンの限定された は、次のように P 定義されます。The narrowed type of a pattern P is defined as follows:

  1. Pが型パターンの場合、限定された は、型パターンの型の型になります。If P is a type pattern, the narrowed type is the type of the type pattern's type.
  2. Pが宣言パターンの場合、限定された は宣言パターンの型の型になります。If P is a declaration pattern, the narrowed type is the type of the declaration pattern's type.
  3. Pが明示的な型を提供する再帰的なパターンの場合、限定された はその型になります。If P is a recursive pattern that gives an explicit type, the narrowed type is that type.
  4. Pの規則によって一致 ITuple する場合、限定された は型に System.Runtime.CompilerServices.ITuple なります。If P is matched via the rules for ITuple, the narrowed type is the type System.Runtime.CompilerServices.ITuple.
  5. 定数が P null 定数ではなく、式の 入力型 への 定数式の変換 がない定数パターンの場合、限定された は定数の型になります。If P is a constant pattern where the constant is not the null constant and where the expression has no constant expression conversion to the input type, the narrowed type is the type of the constant.
  6. が定数式に P 入力型 への 定数式の変換 がないリレーショナルパターンの場合、限定された は定数の型になります。If P is a relational pattern where the constant expression has no constant expression conversion to the input type, the narrowed type is the type of the constant.
  7. Pがパターンの場合 or 、このような共通の型が存在する場合、限定された は、サブパターンの縮小された の共通型です。If P is an or pattern, the narrowed type is the common type of the narrowed type of the subpatterns if such a common type exists. このため、共通型アルゴリズムでは、id、ボックス化、および暗黙の参照変換だけが考慮され、パターンのシーケンスのすべてのサブパターンが考慮さ or れます (かっこで囲まれたパターンは無視されます)。For this purpose, the common type algorithm considers only identity, boxing, and implicit reference conversions, and it considers all subpatterns of a sequence of or patterns (ignoring parenthesized patterns).
  8. Pがパターンの場合 and 、限定された は、適切なパターンの限定された です。If P is an and pattern, the narrowed type is the narrowed type of the right pattern. さらに、左のパターンの限定された は、右側のパターンの 入力型 です。Moreover, the narrowed type of the left pattern is the input type of the right pattern.
  9. それ以外の場合は、の縮小された PP 入力型になります。Otherwise the narrowed type of P is P's input type.

変数の定義と明確な代入Variable definitions and definite assignment

パターンとパターンの追加により、 or not パターン変数と明確な代入に関する興味深い新しい問題が発生します。The addition of or and not patterns creates some interesting new problems around pattern variables and definite assignment. 変数は通常、一度に1回だけ宣言できるので、パターンの一方の側で宣言されたパターン変数は or 、パターンが一致したときに確実に割り当てられるとは限りません。Since variables can normally be declared at most once, it would seem any pattern variable declared on one side of an or pattern would not be definitely assigned when the pattern matches. 同様に、パターン内で宣言された変数は、 not パターンが一致したときに確実に代入されるとは限りません。Similarly, a variable declared inside a not pattern would not be expected to be definitely assigned when the pattern matches. これに対処する最も簡単な方法は、これらのコンテキストでのパターン変数の宣言を禁止することです。The simplest way to address this is to forbid declaring pattern variables in these contexts. ただし、これは制限が厳しすぎる可能性があります。However, this may be too restrictive. 他にも考慮すべき方法があります。There are other approaches to consider.

考慮するべきシナリオの1つは、One scenario that is worth considering is this

if (e is not int i) return;
M(i); // is i definitely assigned here?

現時点では、これは機能しません。これは、のパターン の場合、パターン変数が 確実に代入 されると見なされるのは、pattern が true の場合 ("true の場合は確実に割り当てられている") だけです。This does not work today because, for an is-pattern-expression, the pattern variables are considered definitely assigned only where the is-pattern-expression is true ("definitely assigned when true").

これをサポートすることは、(プログラマの観点から) 単純化された条件ステートメントのサポートを追加することとは異なり if ます。Supporting this would be simpler (from the programmer's perspective) than also adding support for a negated-condition if statement. このようなサポートを追加したとしても、プログラマは、上記のスニペットが動作しない理由を不思議にします。Even if we add such support, programmers would wonder why the above snippet does not work. 一方、の同じシナリオでは、偽の switch 場合に明確に割り当てら れたプログラムには対応するポイントがないため、の方が意味がありません。On the other hand, the same scenario in a switch makes less sense, as there is no corresponding point in the program where definitely assigned when false would be meaningful. これは、パターン式で 許可されますが、パターンが許可される他のコンテキストでは許可されませんか。Would we permit this in an is-pattern-expression but not in other contexts where patterns are permitted? これは不規則に見えます。That seems irregular.

これに関連するのは、 選言パターン での明確な代入の問題です。Related to this is the problem of definite assignment in a disjunctive-pattern.

if (e is 0 or int i)
{
    M(i); // is i definitely assigned here?
}

i入力がゼロでない場合にのみ、確実に割り当てられます。We would only expect i to be definitely assigned when the input is not zero. しかし、入力が0であるかブロック内にないかがわからないため、 i は確実に割り当てられません。But since we don't know whether the input is zero or not inside the block, i is not definitely assigned. ただし、 i 相互に排他的なさまざまなパターンで宣言することが許可されている場合はどうでしょうか。However, what if we permit i to be declared in different mutually exclusive patterns?

if ((e1, e2) is (0, int i) or (int i, 0))
{
    M(i);
}

ここでは、変数は i ブロック内で確実に割り当てられ、ゼロ要素が見つかった場合にはタプルの他の要素から値を受け取ります。Here, the variable i is definitely assigned inside the block, and takes it value from the other element of the tuple when a zero element is found.

また、case ブロックのすべてのケースで変数を (乗算) することを許可することも提案されています。It has also been suggested to permit variables to be (multiply) defined in every case of a case block:

    case (0, int x):
    case (int x, 0):
        Console.WriteLine(x);

このような作業を行うには、このような複数の定義が許可されている場所を慎重に定義し、そのような変数が確実に割り当てられていると見なされる条件に基づいて定義する必要があります。To make any of this work, we would have to carefully define where such multiple definitions are permitted and under what conditions such a variable is considered definitely assigned.

このような作業は、後で (私がアドバイスする) まで遅らせることができます。 C# 9 では、Should we elect to defer such work until later (which I advise), we could say in C# 9

  • notまたはの下 or では、パターン変数を宣言することはできません。beneath a not or or, pattern variables may not be declared.

次に、後で緩和の可能性のある価値を理解できるような、いくつかのエクスペリエンスを開発する時間があります。Then, we would have time to develop some experience that would provide insight into the possible value of relaxing that later.

Diagnostics、subsumption、および exhaustivenessDiagnostics, subsumption, and exhaustiveness

これらの新しいパターンフォームでは、プログラマの関連のエラーに対する多くの新しい機会が導入しています。These new pattern forms introduce many new opportunities for diagnosable programmer error. 診断するエラーの種類とその方法を決定する必要があります。We will need to decide what kinds of errors we will diagnose, and how to do so. 次に例をいくつか示します。Here are some examples:

case >= 0 and <= 100D:

この大文字と小文字は一致しません (入力がとの両方になることはできません int double )。This case can never match (because the input cannot be both an int and a double). 一致しない可能性があるケースを検出したときには、既にエラーが発生しています ("スイッチケースは既に前のケースで処理されています" と "以前の arm によって既に処理されています")、新しいシナリオでは誤解を招く可能性があります。We already have an error when we detect a case that can never match, but its wording ("The switch case has already been handled by a previous case" and "The pattern has already been handled by a previous arm of the switch expression") may be misleading in new scenarios. 場合によっては、パターンが入力と一致しなくなるように、表現を変更する必要があります。We may have to modify the wording to just say that the pattern will never match the input.

case 1 and 2:

同様に、値にとの両方を指定することはできないため、エラーになり 1 2 ます。Similarly, this would be an error because a value cannot be both 1 and 2.

case 1 or 2 or 3 or 1:

この例は一致する可能性がありますが、 or 1 末尾のはパターンに意味を追加しません。This case is possible to match, but the or 1 at the end adds no meaning to the pattern. 複合パターンの一部の結合または disjunct でパターン変数が定義されていない場合、または一致する値のセットに影響を与えない場合は、エラーを生成することをお勧めします。I suggest we should aim to produce an error whenever some conjunct or disjunct of a compound pattern does not either define a pattern variable or affect the set of matched values.

case < 2: break;
case 0 or 1 or 2 or 3 or 4 or 5: break;

ここでは、 0 or 1 or これらの値が最初のケースによって処理されるため、2番目のケースには何も追加しません。Here, 0 or 1 or adds nothing to the second case, as those values would have been handled by the first case. これにより、エラーが発生する可能性があります。This too deserves an error.

byte b = ...;
int x = b switch { <100 => 0, 100 => 1, 101 => 2, >101 => 3 };

このような switch 式は、すべて 網羅 したものと見なす必要があります (すべての可能な入力値を処理します)。A switch expression such as this should be considered exhaustive (it handles all possible input values).

C# 8.0 では、型の入力を含むスイッチ式 byte は、パターンがすべて ( 破棄パターン または var パターン) に一致する最終 arm が含まれている場合にのみ、完全なものと見なされます。In C# 8.0, a switch expression with an input of type byte is only considered exhaustive if it contains a final arm whose pattern matches everything (a discard-pattern or var-pattern). 個別の値ごとに arm を持つ switch 式でも、 byte C# 8 では完全には見なされません。Even a switch expression that has an arm for every distinct byte value is not considered exhaustive in C# 8. リレーショナルパターンの exhaustiveness を適切に処理するには、このケースも処理する必要があります。In order to properly handle exhaustiveness of relational patterns, we will have to handle this case too. これは技術的には重大な変更になりますが、ユーザーには気付かない可能性があります。This will technically be a breaking change, but no user is likely to notice.