Help us understand the problem. What is going on with this article?

C#8.0までに再帰パターンに馴染んでおく

More than 1 year has passed since last update.

やりたいこと

C#8.0 の再帰パターンで色々な書き方ができるようになったので、今のうちに書きなれておきたい、というのがこの記事の趣旨です。

気になる点があれば、遠慮なくコメントしてください。

再帰パターン

再帰パターンパターンマッチング(C#7.0)の拡張です。
が、実際のところはC#7.0のパターンマッチングは機能としてはごくごく限定的で、C#8.0で完全版になったというのが正直なところだったりします。

関連する機能

  • 既存の機能
    • タプル
    • 分解
    • is式
    • switch文
  • 追加された機能
    • switch式

is式で試す

is式でできることが、switch文やswitch式でできるようになっている、と考えられそうです。
なので、まずはis式で色々なパターンを書いてみます。

これまでできたこと

おさらいとして、is式でこれまでのバージョンでもできたことを挙げておきます。

// [C#1.0]
// 昔ながらのis式
if(obj is MyClass) {
}
// [C#7.0]
// is式の拡張
// 後続の変数に null が代入されることはない。
if (obj is MyClass my) {
    // コンパイラがフローのチェックを行う
    my.ToString();
}
else {
    // コンパイラがフローのチェックを行うので、
    // ここでは変数 my が未割当と判断される
    // my.ToString(); //ここではmyが未割当
}
// [C#7.0]
// 定数と照合することもできる
if (obj is 1) {
}
if (obj is null) {
}
// [C#7.3]までは、isの右辺にTuple定数を書くことはできない
// if (obj is (1, 2)) {
// }
// [C#7.0]
// var を指定した場合、後続の変数に null も代入される。
if (obj is var my) {
    my.ToString(); // nullの場合もここを通る
}

C#8.0でできるようになったこと

一言でいうと、obj is [型名] [変数名] の型名と変数名の間にパターンを差し込むことができるようになった、ということです。
つまりobj is [型名] [パターン] [変数名]と書けるようになりました。
この[パターン]の部分こそがパターンマッチングの主役で、このパターンが再帰的に書けるので再帰パターンと呼ばれるわけです。

パターンを書くために、以下のクラスを用意しました。

用意したクラス
/// <summary>格子上の点</summary>
public class Point {
    public long X { get; set; }
    public long Y { get; set; }
    public long AbsSquare => X * X + Y * Y;
    public Point(long x, long y) 
        => (X, Y) = (x, y);
    public void Deconstruct(out long x, out long y) 
        => (x, y) = (X, Y);
}

パターンマッチングの基本形

パターンには位置パターンプロパティパターンの2種類があり、またこれらを組み合わせることができます。

まずはシンプルに定数とマッチさせるパターンを書いてみます。

// 単純な位置パターン
if (point is Point(1, 2) p) {
}

// 名前を書いても良い、この名前はDeconstruct関数の引数名に一致させる
if (point is Point(x: 1, y: 2) p) {
}

// 単純なプロパティパターン
if (point is Point { AbsSquare: 10 } p) {
}

// 型名は、自明な場合に省略できる
// 変数は、使用しない場合省略できる
if (point is (1, 2)) {
}
if (point is { AbsSquare: 10 }) {
}

// 位置パターンとプロパティパターンは共存できる。
if (point is (1, 3) { AbsSquare: 10 }) {
}

位置パターンの特徴

Deconstruct()が定義された型で、出力引数の順番に評価されます。
自然な順序が定義できる型であれば、この書き方が簡便で便利そうです。

System.Runtime.CompilerServices.ITupleを実装したクラスでも位置パターンは利用できますが、Deconstruct()メソッドによるものと少し考え方が違うようです。

プロパティパターンの特徴

プロパティ名(または変数名)に一致したものが評価されます。
使用するのに特別なクラスの造りが必要なく、気軽な使い方ができそうです。

変数を使ったパターンマッチング

定数と一致させる以外に、その場で宣言した変数で受け取ることもできます。

// xの変数宣言も兼ねるのでlongは必要。
if (point is (long x, 3) p) {
}
// 破棄(_)では型名のlongは不要
if (point is (_, 3) p) {
}
// 変数宣言も兼ねるので型名は必要。(この条件は常にtrueになる)
if (point is (long x, long y)) {
}
// もちろん、プロパティパターンでも変数宣言可能
if (point is (_, 3) { AbsSquare: long abs }) {
}

そして再帰へ

再帰パターンで書く前に以下のクラスを追加します。

用意したクラスその2
/// <summary>線分</summary>
public class LineSegment {
    public Point Start { get; set; }
    public Point End { get; set; }
    public long NormSquare 
        => (Start.X - End.X) * (Start.X - End.X) 
        + (Start.Y - End.Y) * (Start.Y - End.Y);
    public LineSegment(Point start, Point end) 
        => (Start, End) = (start, end);
    public void Deconstruct(out Point start, out Point end) 
        => (start, end) = (Start, End);
}

そして再帰パターンで書いてみます。

// 全部定数にマッチさせる
if (line is ((1, 2), (3, 4))) {
}
// 一部だけマッチさせて、その場で宣言した変数に代入する
if (line is (Point(1, _) p1, Point(long x2, 4) p2)) {
}

全部混ぜこぜにした場合、かなり複雑な見た目のコードになります。
位置パターンプロパティパターンと途中で変数宣言ができることなどが一通りが分かっていれば読めなくはない、といったところでしょうか。

// これもパターンマッチングの構文として正しい
if (line is ((1, _) { AbsSquare: 101 } p1, (long x2, 4) { AbsSquare: 80 } p2) { NormSquare: 85 } l) {
}

switch文とswitch式

再帰パターンはswitch文とC#8.0で追加されるswitch式でも利用できます。is式でパターンに慣れておけば、戸惑うことは少ないはずです。

多値switch

地味ですが嬉しいものがこれです。
switchを使う機会が割と増えるかも、と思っていたりします。
タプル(C#7.0)の機能が存分に生かされています。

// switch文の対象に複数の値を書くことができるようになった。
// 実際には単なる一時的なタプルの構築が行われる。
switch (x, str) {
    case (0, string s):
        break;
    case (int n, string s) when s.Length > 0:
        break;
    default:
        break;
}

switch式

case毎の値を返すだけのswitchを書きたいとき、既存のswitch文はかなりまどろっこしい構文になっていると思います。
そこで便利なのがC#8.0で導入されるswitch式です。
casebreakdefaultなどのキーワードが不要なだけでも、かなり嬉しい構文になっていることがわかります。

var result = (x, str) switch {
    (int n, _) when n < 0 => -1,
    (0, _) => 0,
    (int n, string { Length: int len }) => n * len + 2,
    _ => throw new ArgumentNullException(),
};

// うまく使えば、エルビス演算子`A?B:C`の組み合わせよりわかりやすいかも
var result = (b1, b2) switch {
    (true, true) => 1,
    (true, false) => 2,
    (false, _) => 3,
};

まとめ

  • パターンには、位置パターンプロパティパターンの2種類がある。
    • 位置パターンにはDeconstruct()メソッドが使用される。
  • パターンは再帰的に組み合わせられる。
  • 新しく導入されたswitch式はかなり使いどころがありそう

※C#8.0は現在Preview版です。記事の内容と異なる構文になる可能性もありますのでご了承ください。

今回書けなかったこと
  • 型名を書くことによるキャストの挙動
  • System.Runtime.CompilerServices.ITupleを実装したクラスの再帰パターンの挙動
  • 0-ple ,1-ple の挙動
muniel
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした