概要
C# 7.0~9.0 に掛けて、 パターン マッチングをはじめとして、 変数宣言を拡張するような機能が入っています。
C# 6.0 までの変数宣言と違って、以下のような性質があります。
- 式の途中でも変数宣言できる
- 複数の値のうち一部だけを受け取り、残りを破棄したいことがある
式中の変数宣言
C# 7.0 以降の構文に特有な点の1つとして、式の途中で変数を宣言できるようになるという点があります。
// C# 6.0 以前は、この x のように単独の変数宣言しかなかった。
object x = 1;
// C# 7.0 以降、この y とか z とかのように式の途中で宣言される変数が増えた。
if (x is int y) Console.WriteLine(y);
if (int.TryParse("1", out var z)) Console.WriteLine( z);
ちなみに、案としてはここからさらに発展して、任意の式の中で変数を宣言できるような話も出ています。 この機能を変数宣言式(variable declaration expression)といいます。 例えば以下のように書けるようになるかもしれません。 (優先度低めとされていて、この機能が入る期待はそれほどしない方がいいです。 代わりに、Expression blocksのような機能が入るみたいな話もありますが、こちらもそれほど高い優先度は付いていません。)
// (草案。このままの文法が採用されるとは限らない)
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;
}
式中で変数宣言があり得ることによって、 変数のスコープに関するルールがいくつか追加されています。 詳しくは「C# 7での新しいスコープ ルール」で説明します。
値の破棄
型スイッチや分解では、変数を宣言しつつ何らかの値を受け取るわけですが、 特に受け取る必要のない余剰の値が生まれたりします。
例えば、分解の場合、複数の値のうち、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(_);
}
ちなみに、既存の構文に対しては破棄は使えません。
_
は普通に変数扱いされます。
例えば、引数に対して _
を使っても破棄の意味にはなりません。
以下のコードはコンパイル エラーになります。
(同名の引数が2つある状態。)
static void M(int _, int _)
{
}
ラムダ式の引数
Ver. 9
既存の構文で破棄を使いたいものの代表例は、ラムダ式の引数でしょう。
C# 8.0 までは破棄の意味で_
を使えず、「_1
」みたいな名前が必要でした。
static void Subscribe(INotifyPropertyChanged source)
{
// C# 8.0 以前、2個目の _ が「同じ名前被ってる」エラーになる
source.PropertyChanged += (_, _) => Console.WriteLine("property changed");
}
C# 9.0 でこの場合に対応しました。
ただし、既存コードを壊さないように、2個以上の引数を _
にした時だけ破棄の意味になるようにしています。
すなわち、以下のようなコードが書ける予定です。
static void Subscribe(INotifyPropertyChanged source)
{
// 2回以上 _ を使かったら破棄扱い
source.PropertyChanged += (_, _) => { };
// _ が1回だけの場合は引数扱い。この場合普通に変数参照できる
source.PropertyChanged += (_, _1) => Console.WriteLine(_);
}