概要
C# 7.0~9.0 に掛けて、パターン マッチングをはじめとして、変数宣言を拡張するような機能が入っています。
C# 6.0 までの変数宣言と違って、以下のような性質があります。
- 式の途中でも変数宣言できる
- 複数の値のうち一部だけを受け取り、残りを破棄したいことがある
式中の変数宣言
C# 7.0 以降の構文に特有な点の1つとして、式の途中で変数を宣言できるようになるという点があります。
// C# 6.0 以前は、この x のように単独の変数宣言しかなかった。objectx = 1;// C# 7.0 以降、この y とか z とかのように式の途中で宣言される変数が増えた。if (xisinty)Console.WriteLine(y);if (int.TryParse("1",outvarz))Console.WriteLine(z);ちなみに、案としてはここからさらに発展して、任意の式の中で変数を宣言できるような話も出ています。この機能を変数宣言式(variable declaration expression)といいます。例えば以下のように書けるようになるかもしれません。(優先度低めとされていて、この機能が入る期待はそれほどしない方がいいです。代わりに、Expression blocksのような機能が入るみたいな話もありますが、こちらもそれほど高い優先度は付いていません。)
// (草案。このままの文法が採用されるとは限らない)staticint X(string s) => (int x =int.Parse(s)) * x;(int x = int.Parse(s)) の部分の戻り値は、xに代入された値です。結局、以下のコードと同じ意味ですが、これが「式」として書けます。
staticint X(string s){int x =int.Parse(s);return x * x;}式中で変数宣言があり得ることによって、変数のスコープに関するルールがいくつか追加されています。詳しくは「C# 7での新しいスコープ ルール」で説明します。
値の破棄
型スイッチや分解では、変数を宣言しつつ何らかの値を受け取るわけですが、特に受け取る必要のない余剰の値が生まれたりします。
例えば、分解の場合、複数の値のうち、1つだけを受け取りたい場合があったとします。そういう場面が複数並んでしまった場合、以下のようなコードになりがちです。
staticvoid 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,outvar remainder), remainder);「しょうがないから」感がひどく、どう見ても不格好です。
こういう時に使うのが、値の破棄(discard)です。以下のように、_を書くことで値を無視できます。
{// _ を書いたところでは、値を受け取らずに無視するvar (q,_) = DivRem(123, 11);// _ は変数にはならないので、スコープを汚さない。別の場所でも再び _ を書ける// また、本来「var x」とか変数宣言を書くべき場所にも _ だけを書ける (_,var r) = DivRem(123, 11);}1つ目の例では一見、_という名前の変数を定義しているようにも見えますが、別の挙動になります。変数は作らず、スコープ内の別の場所でも再び_を使うことができます(先ほどの例みたいに_1みたいな変な名前を作らなくて済む)。
また、2つ目の例のように、「型名 変数名」みたいに書くべき場所でも、var _ではなく、_だけでOKです。
同様に、出力変数宣言でも_を破棄の意味で使えます。
// 欲しいのは戻り値だけであって、out 引数で受け取った値は要らないstaticbool CanParse(string s) =>int.TryParse(s,out _);型スイッチでも同様です。
staticint TypeSwitch(object obj){switch (obj) {caseint[] x:return x.Length;caselong[] x:return 2 * x.Length;// int でさえあれば値は問わないcaseint_:return 1;// 同、longcaselong_:return 2;casenull:return 0;// 以下の行をコメントアウトするとエラーに// 今のところ、case _ は未実装(将来的に予定はあり)//case _:default:thrownewArgumentOutOfRangeException(); }}_ が破棄の意味になる場合
_という記号は、元々のC#では識別子として有効な名前です。すなわち、以下のコードは有効なC#コードです。
var _ = 10;Console.WriteLine(_);// 10 が表示される_を破棄の意味で使うということは、_の使い方を変えるということになります。なので、以下のように、文脈によって_ の意味が変わります。
- C# 7から導入される新しい構文の中では、
_が常に破棄の意味になる - それ以前の構文では、1つも参照がなかった場合だけ
_を破棄の意味で扱う(予定)
分解、出力引数宣言、型スイッチなど、C# 7から導入された構文の中では、_が常に破棄の意味になります。_という名前の変数は作られません。
staticvoid Deconstruct1(){// 要らないので適当な変数 x とかで受けるvar (q, x) = DivRem(123, 11);// 要らないと言いつつ、参照できてしまうConsole.WriteLine(x);// 要らないものは _ で破棄var (_, r) = DivRem(123, 11);// 分解の中に書いた _ は変数にはならない// 以下の行でコンパイル エラーになる(_ は存在しない)Console.WriteLine(_);}ちなみに、既存の構文に対しては破棄は使えません。_は普通に変数扱いされます。
例えば、引数に対して_ を使っても破棄の意味にはなりません。以下のコードはコンパイル エラーになります。(同名の引数が2つある状態。)
staticvoidM(int_,int_){}ラムダ式の引数
Ver. 9
既存の構文で破棄を使いたいものの代表例は、ラムダ式の引数でしょう。C# 8.0 までは破棄の意味で_を使えず、「_1」みたいな名前が必要でした。
staticvoid Subscribe(INotifyPropertyChanged source){// C# 8.0 以前、2個目の _ が「同じ名前被ってる」エラーになる source.PropertyChanged += (_,_) =>Console.WriteLine("property changed");}C# 9.0 でこの場合に対応しました。ただし、既存コードを壊さないように、2個以上の引数を_ にした時だけ破棄の意味になるようにしています。
すなわち、以下のようなコードが書ける予定です。
staticvoid Subscribe(INotifyPropertyChanged source){// 2回以上 _ を使かったら破棄扱い source.PropertyChanged += (_,_) => { };// _ が1回だけの場合は引数扱い。この場合普通に変数参照できる source.PropertyChanged += (_,_1) =>Console.WriteLine(_);}