目次

キーワード

概要

C# 7のいくつかの機能は、将来的にパターン マッチング(pattern matching)という機能に発展する予定です。

パターン マッチングは結構大きな言語機能ですが、 小さく切り出していった結果が型スイッチや分解になります。 これらは元々の大きな目標から切り出されたものだけあって、以下のような共通する性質があったりします。

  • 変数を宣言しつつ受け取れる
    • 式の途中でも変数宣言できる
  • 複数の値のうち一部だけを受け取り、残りを破棄したいことがある

変数宣言式

これらに共通する点の1つは、式の途中で変数を宣言できるようになるということです。

ここから発展して、任意の式の中で変数を宣言できるようにする予定があって、この機能を変数宣言式(variable declaration expression)といいます。 例えば以下のように書けます。

// (予定。C# 7では書けない) C# 8?
static int X(string s) => (int x = int.Parse(s)) * x;

(int x = int.Parse(s)) の部分の戻り値は、xに代入された値です。結局、以下のコードと同じ意味ですが、これが「式」として書けます。

static int X(string s)
{
    int x = int.Parse(s);
    return x * x;
}

値の破棄

型スイッチや分解では、変数を宣言しつつ何らかの値を受け取るわけですが、 特に受け取る必要のない余剰の値が生まれたりします。

例えば、分解の場合、複数の値のうち、1つだけを受け取りたい場合があったとします。 そういう場面が複数並んでしまった場合、以下のようなコードになりがちです。

static void Deconstruct()
{
    // 商と余りを計算するメソッドがあるけども、ここでは商しか要らない
    // 要らないので適当な変数 x とかで受ける
    var (q, x) = DivRem(123, 11);

    // 逆に、余りしか要らない
    // 要らないから再び適当な変数 x で受けたいけども、x はもう使ってる
    // しょうがないから x1 とかにしとくか…
    var (x1, r) = DivRem(123, 11);
}

static (int quotient, int remainder) DivRem(int dividend, int divisor)
    => (Math.DivRem(dividend, divisor, out var remainder), remainder);

「しょうがないから」感がひどく、どう見ても不格好です。

こういう時に使うのが、値の破棄(discard)です。 以下のように、_を書くことで値を無視できます。

{
    // _ を書いたところでは、値を受け取らずに無視する
    var (q, _) = DivRem(123, 11);

    // _ は変数にはならないので、スコープを汚さない。別の場所でも再び _ を書ける
    // また、本来「var x」とか変数宣言を書くべき場所にも _ だけを書ける
    (_, var r) = DivRem(123, 11);
}

1つ目の例では一見、_という名前の変数を定義しているようにも見えますが、別の挙動になります。 変数は作らず、スコープ内の別の場所でも再び_を使うことができます(先ほどの例みたいに_1みたいな変な名前を作らなくて済む)。

また、2つ目の例のように、「型名 変数名」みたいに書くべき場所でも、var _ではなく、_だけでOKです。

同様に、出力変数宣言でも_を破棄の意味で使えます。

// 欲しいのは戻り値だけであって、out 引数で受け取った値は要らない
static bool CanParse(string s) => int.TryParse(s, out _);

型スイッチでも同様です。

static int TypeSwitch(object obj)
{
    switch (obj)
    {
        case int[] x: return x.Length;
        case long[] x: return 2 * x.Length;
        // int でさえあれば値は問わない
        case int _: return 1;
        // 同、long
        case long _: return 2;
        case null: return 0;
        // 以下の行をコメントアウトするとエラーに
        // 今のところ、case _ は未実装(将来的に予定はあり)
        //case _:
        default: throw new ArgumentOutOfRangeException();
    }
}

_ が破棄の意味になる場合

_という記号は、元々のC#では識別子として有効な名前です。 すなわち、以下のコードは有効なC#コードです。

var _ = 10;
Console.WriteLine(_); // 10 が表示される

_を破棄の意味で使うということは、_の使い方を変えるということになります。

  • C# 7から導入される新しい構文の中では、_が常に破棄の意味になる
  • それ以前の構文では、1つも参照がなかった場合だけ_を破棄の意味で扱う(予定)

分解、出力引数宣言、型スイッチなど、C# 7から導入された構文の中では、 _が常に破棄の意味になります。 _という名前の変数は作られません。

static void Deconstruct1()
{
    // 要らないので適当な変数 x とかで受ける
    var (q, x) = DivRem(123, 11);

    // 要らないと言いつつ、参照できてしまう
    Console.WriteLine(x);

    // 要らないものは _ で破棄
    var (_, r) = DivRem(123, 11);

    // 分解の中に書いた _ は変数にはならない
    // 以下の行でコンパイル エラーになる(_ は存在しない)
    Console.WriteLine(_);
}

現状(C# 7のリリース候補版の時点)では、既存の構文に対しては破棄は使えません。 _は普通に変数扱いされます。

既存の構文で破棄を使いたいものの代表例は、ラムダ式の引数でしょう。 残念ながら今のところは破棄の意味で_を使えず、「_1」みたいな名前がいまだ必要になります。 例えば、以下のコードはコンパイル エラーになります。

static void Subscribe(INotifyPropertyChanged source)
{
    // 2個目の _ が「同じ名前被ってる」エラーになる
    source.PropertyChanged += (_, _) => Console.WriteLine("property changed");
}

ただし、今後の計画としては、既存の文法に対しても破棄の意味で _ を使えるようにしたいようです。 この場合、既存のC#コードを壊さないように、以下のような方針を取ります。

  • 宣言だけして1回も _ を使わなかったら破棄扱い
  • 1回でも使っていたら普通の変数・引数扱い

すなわち、以下のようなコードが書ける予定です。

static void Subscribe(INotifyPropertyChanged source)
{
    // (予定)C# 8?
    // 1回も _ を使わなかったら破棄扱い
    source.PropertyChanged += (_, _) => { };

    // _ を使っているのでこれは引数扱い。同名の別引数は作れない
    source.PropertyChanged += (_, _1) => Console.WriteLine(_);
}

更新履歴

ブログ