C#7でパターンマッチングを調べる

C#7には、ついにパターンマッチングと呼ばれる待望の機能があります 。 F#などの関数型言語に精通している場合、現在の形式ではこの機能に少しがっかりするかもしれませんが、今日でもさまざまなシナリオでコードを簡素化できます。



各新機能には、パフォーマンスが重要なアプリケーションで作業する開発者にとって危険が伴います。 新しいレベルの抽象化は優れていますが、それらを効果的に使用するには、内部で何が起こっているのかを知る必要があります。 今日は、パターンマッチングの内部を調べて、これがどのように実装されているかを理解します。

C#言語は、is式およびswitchステートメントの caseブロック内で使用できるパターンの概念を導入しました。



テンプレートには3つのタイプがあります。





is-expressionsのパターンマッチング



public void IsExpressions(object o) { // Alternative way checking for null if (o is null) Console.WriteLine("o is null"); // Const pattern can refer to a constant value const double value = double.NaN; if (o is value) Console.WriteLine("o is value"); // Const pattern can use a string literal if (o is "o") Console.WriteLine("o is \"o\""); // Type pattern if (o is int n) Console.WriteLine(n); // Type pattern and compound expressions if (o is string s && s.Trim() != string.Empty) Console.WriteLine("o is not blank"); }
      
      





is式は値が定数であるかどうかをチェックでき、型チェックはさらにパターン変数を作成できます



is式のパターンマッチングに関連する興味深い側面がいくつか見つかりました。





最初に、最初の2つのケースを確認します。



 public void ScopeAndDefiniteAssigning(object o) { if (o is string s && s.Length != 0) { Console.WriteLine("o is not empty string"); } // Can't use 's' any more. 's' is already declared in the current scope. if (o is int n || (o is string s2 && int.TryParse(s2, out n))) { Console.WriteLine(n); } }
      
      





最初のifステートメントは変数s 導入し、変数はメソッド全体の内側に表示されます。 これは合理的ですが、同じブロック内の他のifステートメントが同じ名前を再利用しようとすると、ロジックが複雑になります。 この場合、衝突を避けるために別の名前を使用する必要があります。



is式で導入された変数は、述語がtrueの場合にのみ完全に定義されます 。 これは、2番目のifステートメントの変数nが右側のオペランドで定義されていないことを意味しますが、この変数は既に宣言されているのでint.TryParseメソッドでout変数として使用できます。



上記の3番目の側面が最も重要です。 次のコードを検討してください。



 public void BoxTwice(int n) { if (n is 42) Console.WriteLine("n is 42"); }
      
      





ほとんどの場合、is式はobject.Equals(constValue、variable)に変換されます(仕様で、==演算子をプリミティブ型に使用するように指定されている場合でも):



 public void BoxTwice(int n) { if (object.Equals(42, n)) { Console.WriteLine("n is 42"); } }
      
      





このコードにより2つのボクシングが発生し、クリティカルなアプリケーションパスで使用するとパフォーマンスに重大な影響を与える可能性があります。 oがnull表現になるとパッケージ化が発生し( eの準最適コードを参照)、現在の動作もすぐに修正されることを期待しています(githubに対応するチケットがあります)。



n-変数のタイプがobjectの場合、 oが42の 場合 、1つのメモリ割り当てが発生します(リテラル42をパックするため)。ただし、スイッチに基づく同様のコードはメモリ割り当てを行いません。



is-式のvarパターン



varパターンは、1つの重要な違いがある型パターンの特殊なケースです。値がnullであっても、パターンは任意の値に一致します



 public void IsVar(object o) { if (o is var x) Console.WriteLine($"x: {x}"); }
      
      





oはオブジェクトがtrueである場合、 oは nullはありませんが、 oはvar xは常にtrueです。 コンパイラーはこれを認識しており、リリース(*)モードではifコンストラクトが完全に削除され、単にコンソールメソッドが呼び出されたままになります。 残念ながら、コンパイラは次の場合にコードが到達不能であることを警告しません。

if(!(oはvar x))Console.WriteLine( "Unreachable") 。 これも修正されることを願っています。



(*)動作がリリースモードでのみ異なる理由は明らかではありません。 しかし、私はすべての問題に1つの性質があると思います:機能の最初の実装は最適ではありません。 しかし Neil Gafter によるこのコメントに基づいて、これは次のように変更されます。 ここでお探しの改善のほとんどは、新しいコードで「無料」になると思います。」



nullチェックがないため、このケースは非常に特殊で潜在的に危険です。 しかし、正確に何が起こっているかを知っている場合は、この一致オプションが役立つことがあります。 式内に一時変数を導入するために使用できます。



 public void VarPattern(IEnumerable<string> s) { if (s.FirstOrDefault(o => o != null) is var v && int.TryParse(v, out var n)) { Console.WriteLine(n); } }
      
      





IS式とエルビス演算子



もう1つ、非常に便利なケースがあります。 型パターンは、値がnullでない場合にのみ値に一致します 。 この「フィルタリング」ロジックをヌル伝播演算子と共に使用して、コードをより読みやすくすることができます。



 public void WithNullPropagation(IEnumerable<string> s) { if (s?.FirstOrDefault(str => str.Length > 10)?.Length is int length) { Console.WriteLine(length); } // Similar to if (s?.FirstOrDefault(str => str.Length > 10)?.Length is var length2 && length2 != null) { Console.WriteLine(length2); } // And similar to var length3 = s?.FirstOrDefault(str => str.Length > 10)?.Length; if (length3 != null) { Console.WriteLine(length3); } }
      
      





同じテンプレートを値型と参照型の両方に使用できることに注意してください。



パターンマッチングスイッチブロック



C#7はswitchステートメントを拡張して、 caseブロックでパターンを使用します。



 public static int Count<T>(this IEnumerable<T> e) { switch (e) { case ICollection<T> c: return c.Count; case IReadOnlyCollection<T> c: return c.Count; // Matches concurrent collections case IProducerConsumerCollection<T> pc: return pc.Count; // Matches if e is not null case IEnumerable<T> _: return e.Count(); // Default case is handled when e is null default: return 0; } }
      
      





この例は、switchステートメントの最初の変更セットを示しています。



  1. switchステートメントは、任意のタイプの変数を使用できます。
  2. ケース節はパターンを示す場合があります。
  3. 場合の文の順序は重要です。 前のケースがベースタイプと一致し、次のケースが派生タイプと一致する場合、コンパイラはエラーをスローします。
  4. すべてのcaseブロックには、暗黙的なnull (**)チェックが含まれています 。 前の例では、引数がnullでない場合にのみ機能するため、最後のcaseブロックは正しいです。


(**)最後のケースブロックは、C#7で追加された、「 廃棄 」パターンと呼ばれる別の機能を示しています。 名前_は特別で、変数が不要であることをコンパイラーに伝えます。 case句の型テンプレートには変数名が必要です。使用しない場合は、 _を使用して無視できます。



次のスニペットは、 スイッチに基づくパターンマッチングの別の機能、つまり述語を使用する機能を示しています。



 public static void FizzBuzz(object o) { switch (o) { case string s when s.Contains("Fizz") || s.Contains("Buzz"): Console.WriteLine(s); break; case int n when n % 5 == 0 && n % 3 == 0: Console.WriteLine("FizzBuzz"); break; case int n when n % 5 == 0: Console.WriteLine("Fizz"); break; case int n when n % 3 == 0: Console.WriteLine("Buzz"); break; case int n: Console.WriteLine(n); break; } }
      
      





スイッチには、同じタイプの複数のケースブロックを含めることができます。 この場合、コンパイラは1つのブロックですべての型チェックを組み合わせて、冗長な計算を回避します。



 public static void FizzBuzz(object o) { // All cases can match only if the value is not null if (o != null) { if (o is string s && (s.Contains("Fizz") || s.Contains("Buzz"))) { Console.WriteLine(s); return; } bool isInt = o is int; int num = isInt ? ((int)o) : 0; if (isInt) { // The type check and unboxing happens only once per group if (num % 5 == 0 && num % 3 == 0) { Console.WriteLine("FizzBuzz"); return; } if (num % 5 == 0) { Console.WriteLine("Fizz"); return; } if (num % 3 == 0) { Console.WriteLine("Buzz"); return; } Console.WriteLine(num); } } }
      
      





ただし、次の2つの点に注意してください。



  1. コンパイラは、同じタイプの連続したcaseブロックのみを組み合わせます。異なるタイプのブロックを混在させると、コンパイラは最適性の低いコードを生成します。



     switch (o) { // The generated code is less optimal: // If o is int, then more than one type check and unboxing operation // may happen. case int n when n == 1: return 1; case string s when s == "": return 2; case int n when n == 2: return 3; default: return -1; }
          
          





    コンパイラは次のように変換します。



     if (o is int n && n == 1) return 1; if (o is string s && s == "") return 2; if (o is int n2 && n2 == 2) return 3; return -1;
          
          





  2. コンパイラは、 ケースブロックの順序が正しくないという典型的な問題を防ぐために、可能なすべてのことを行います。



     switch (o) { case int n: return 1; // Error: The switch case has already been handled by a previous case. case int n when n == 1: return 2; }
          
          





    しかし、コンパイラーは、ある述部が別の述部よりも強力であることを認識していないため、実際、次のブロックを達成できません。



     switch (o) { case int n when n > 0: return 1; // Will never match, but the compiler won't warn you about it case int n when n > 1: return 2; }
          
          





パターン101マッチング






All Articles