Build Insiderオピニオン:岩永信之(2)
次期C# 7: 式の新機能 ― throw式&never型/式形式のswitch(match式)/宣言式/シーケンス式
―― さまざまな式が書けて、もっと便利になるC# ――
C# 6が出てまだ間もないが、すでに次バージョン「C# 7」についての議論が進んでいる。その中で提案されている「式」に関する新機能を取り上げる。
C# 7として提案されている文法の中には大小さまざまなものがある。今回紹介するのはその中では小さめの機能になる。「式を増やす」という、利便性の向上を目的としたものである。
式
C#に限らず、プログラミング言語の文法要素には大きく分けて式(expression)とステートメント(statement: 文法の文脈では「平叙文」。断定形の文章)がある。これらには、表1に示すような性質がある。
式 | ステートメント |
---|---|
どこにでも書ける | ブロックの中にしか書けない |
短く書ける場合が多い | 相対的に長い |
戻り値が必須 | 戻り値がない |
C#における例をいくつか表2に示す。
式の例 | ステートメントの例 | ||||
---|---|---|---|---|---|
・四則演算
|
・ifステートメント
|
||||
・配列アクセス
|
・変数宣言
|
||||
・型変換
|
・throwステートメント
|
ひと言でいうと、戻り値を返せるなら式になっている方が便利である。その結果、新しい言語ほど、ステートメントを減らして式を増やす傾向にある。関数型言語にカテゴライズされるようなプログラミング言語ではもともとその傾向が強かったが、近年ではどんなパラダイムの言語であろうと式を好むようになっている。
C#も例にもれず、C# 3.0やC# 6で、いくつか式として書けるものが増えている。いくつかの例を表3に挙げよう。
式にできる書き方 | ステートメントでの書き方 | |||||
---|---|---|---|---|---|---|
初期化 |
|
|
||||
null検査 |
|
|
||||
データ列の加工 |
|
|
そして、C# 7でも、式にできるものが増える予定である。
式への抵抗感
式はどこにでも書けて便利と説明したが、その一方で、どこにでも書けすぎるために、読みづらいコードも書けてしまう。それを嫌って、式が増えることに抵抗を感じる開発者もいまだに見受けられる(ベテランの方に多い傾向に見える)。
それでも、式しか書けない場所が多い以上、ステートメントとしてしか書けない構文が多いことはよくないというのが筆者の意見だ。嫌なコードを書けてしまう問題よりも、そもそも書く手段が全くないことの方が問題は大きい。
「書けてしまうから嫌」というなら、ステートメントだってひどいものである。書く人が書けば、{}
の入れ子でソースコードはどんどん右側にずれていく。例として、ちょっとした総当たり計算を挙げよう。少々懐かしいネタではあるが、「send+more=money」という覆面算を解く問題がはやったことがある。そのとき、クエリ式(他のプログラミング言語だと、リストモナドやリスト内包)を使ったものと、ステートメントを使ったものの比較がよく取り上げられた。クエリ式を使うと以下のように書ける。
var digits = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
var solve =
from s in digits where s != 0
from e in digits where e != s
from n in digits where n != s && n != e
from d in digits where d != s && d != e && d != n
from m in digits where m != s && m != e && m != n && m != d && m != 0
from o in digits where o != s && o != e && o != n && o != d && o != m
from r in digits where r != s && r != e && r != n && r != d && r != m && r != o
from y in digits where y != s && y != e && y != n && y != d && y != m && y != o && r != o
let send = s * 1000 + e * 100 + n * 10 + d
let more = m * 1000 + o * 100 + r * 10 + e
let money = m * 10000 + o * 1000 + n * 100 + e * 10 + y
where send + more == money
select new { send, more, money };
foreach (var item in solve)
Console.WriteLine(item);
|
一方、ステートメントで書くと以下のようになるだろう。もちろん、{}
を省略したり、無理やりインデントをなくしたりすれば多少はすっきりするが、コード成型ツールをかけるたびに再びこのインデントがかかったりする。
foreach (var s in digits)
{
if(s != 0)
{
foreach (var e in digits)
{
if(e != s)
{
foreach (var n in digits)
{
if(n != s && n != e)
{
foreach (var d in digits)
{
if (d != s && d != e && d != n)
{
foreach (var m in digits)
{
if(m != e && m != n && m != d && m != 0)
{
foreach (var o in digits)
{
if(o != s && o != e && o != n && o != d && o != m)
{
foreach (var r in digits)
{
if (r != s && r != e && r != n && r != d && r != m && r != o)
{
foreach (var y in digits)
{
if (y != s && y != e && y != n && y != d && y != m && y != o && r != o)
{
var send = s * 1000 + e * 100 + n * 10 + d;
var more = m * 1000 + o * 100 + r * 10 + e;
var money = m * 10000 + o * 1000 + n * 100 + e * 10 + y;
if(send + more == money)
{
Console.WriteLine($"send = {send}, more = {more}, money = {money}");
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
|
ステートメントなら良いというものではない。
「式は読みづらい」というのも、それは「式の書き方を知らないせい」ということだってある。よくやり玉に挙げられるのが条件演算子?:
だが、これだって、以下のように書けば意外ときれいなものである。条件 ? 値 :
を繰り返すのだ。これなら、if-else
ステートメントで書くほどのものではないだろう。
var result =
x < 1000 ? "" :
x < 1000000 ? "k" :
x < 1000000000 ? "M" :
"G";
|
C# 7で提案されている式
前振りが長くなったがここからが本題で、C# 7での提案についての話に入る。以下のようなものが提案されている。
throw
式switch
式- 宣言式
- シーケンス式
それぞれ説明していこう。
throw
1つ目は、例外のthrowを式にしようというものである。前述の通り、式にするためには戻り値が必要である。つまり、throw
の戻り値がどうなるべきかという話に帰結する。
throw式
ここでまず思い起こしてほしいのは、戻り値の型によらず、throw
ステートメントをもってメソッドを抜けられるということである。例えば、以下のメソッドはいずれもコンパイルできる。
private int A() { throw new Exception(); }
private string B() { throw new Exception(); }
private DateTime C() { throw new Exception(); }
private Action D() { throw new Exception(); }
|
このことから、例外のthrow
は「どんな型にでも変換可能な型」を戻り値に持つと考えて差し支えない。この考え方を認めれば、これまでthrow
ステートメントだったものは、そのままでthrow
式になる。
もっとも、throw
が式になってうれしい場面は限られていて、以下の4つである。
- 条件演算子
?:
の第2オペランドと第3オペランド:
x >= 0 ? x : throw new ArgumentException();
- null結合演算子
??
の第2オペランド:
x ?? throw new ArgumentException();
- 式形式の関数メンバー:
void NeverReturn() => throw new NotSupportedException();
- (後述する)
switch
式の中
C# 7では、この4カ所でのみ、throw
を式として書けるようにする方向で検討が進んでいる。
ちなみに、「どんな型にでも変換可能な型」を「bottom型」(最下部の型、全ての型の派生扱い)と呼ぶことがある。この逆は「どんな型からでも変換可能な型」で、「top型」(最上部の型、全ての型の基底扱い)と呼び、C#の場合はobject
型がこれに当たる。つまり、throw
式が返す値の型はobject
型の対極ということになる。
never型
throw
式から一歩進めて、「必ず例外をthrowするメソッドの戻り値」を考えてみよう。
例外には、任意のメソッドを抜けるのに使えるという性質に加えて、throw
ステートメントから後ろは実行されなくなる(return
ステートメントやyield break
ステートメントと同じ)性質がある。この性質はコンパイラーも理解していて、適切なコード診断をしてくれる。
void M()
{
throw new NotSupportedException();
// ここは実行されない
// コンパイラーも理解していて、警告などを出す
}
|
これに対して、例外をthrowする部分をメソッドに切り出すことを考えよう。現状だと以下のようになる。NeverReturnメソッドは常に例外をthrowするメソッドなので、そのメソッド呼び出しから後ろは実行されないはずだが、コンパイラーにはそれが分からず、各種診断が働かない。
// 常に例外をthrowするメソッド
void NeverReturn() { throw new NotSupportedException(); }
void M()
{
NeverReturn();
// ここは実行されないはず
// でも、コンパイラーにはそれを判断できない
}
|
この問題の解決策として、「常に例外をthrowするから、そこから後ろは実行されない」ということを表す型が欲しい。こういう型として、never
型という提案が出ている。never
型とは以下のようなものである。
- 常に例外を返す式の戻り値を表す
- 任意の型に変換可能
- この型を返すメソッドを呼び出すと、そこから後ろは実行されない
これを使うと、先ほどのコードは以下のように書き換えられる。
// 常に例外をthrowするメソッド
never NeverReturn() { throw new NotSupportedException(); }
void M()
{
NeverReturn();
// ここが実行されないことを、コンパイラーが判断できる
}
|
他のプログラミング言語では、ScalaがNothing
型という名前で同様の型を持っている。C#への導入に当たっても、never
の他にbottom
、nothing
などの名前が検討されたが、「後ろが実行されない」という性質が名前に現れるよう、never
が最有力となっている。
never
型の実現方法は2種類ある。
void
のメソッドに、NeverReturn
といったような名前の属性を付ける- .NETランタイム側に
never
型を表す特別な型を足してもらう
1のやり方であれば、C#コンパイラーだけで完結していて、近い将来に実装できる可能性が高い。一方で、ジェネリックの変性に対応できないなどの問題があり、1の方式での実装は見送られることになりそうだ。
従って2のやり方が有力ではあるが、これには.NETランタイムのレベルで手を入れる必要がある。.NETランタイム側にはこれよりも優先度の高いタスクが山積みで、never
型の実装に取り掛かれるのはだいぶ先になるだろう。前節のthrow
式だけでも十分な利便性を得られるということもあり、never
型はしばらく日の目を見なさそうである。
式形式のswitch(match式)
続いてはswitch
の式化だ。これまでのC#でも、if-else
ステートメントやループに相当する処理を式で書ける構文がある。表4に示すように、条件演算子?:
やLINQがそれに当たる。
式にできる書き方 | ステートメントでの書き方 | ||||
---|---|---|---|---|---|
|
|
||||
|
|
一方で、switch
相当の式はこれまでのC#にはなかった。また、次回以降で説明するパターンマッチングが登場すると、switch
の利用頻度が増えることが予想される。そこで、switch
相当の式が提案されている。現状の最有力候補の文法例は以下のようになっている。
var result = x switch (
case 1: "one",
case 2: "two",
case 3: "three",
default: "other");
|
他の候補としては、以下のような案が出ている。
- ステートメントとの弁別のために、
switch
の代わりにmatch
キーワードを使う switch
の後ろの()
を必須にするかどうか- 各
case
の区切りに,
を使うかどうか
完備性
このswitch
式(あるいはmatch
式)を導入するに当たって、問題となるのは値の「完備性」(completeness)である。ここでいう完備というのは、「全てのパターンを網羅している」というような意味である。C#は完備性のチェックが緩く、switch
(今後導入されるswitch
式に限らず、既存のswitch
ステートメントでも)を使う上でもたびたび問題となる。
例えば、C#では、bool
型はtrueもしくはfalseのいずれかの値しか取らない。にもかかわらず、以下のswitch
ステートメントはコンパイルエラーとなる。
string 和名(bool x)
{
switch (x)
{
case true: return "真";
case false: return "偽";
}
}
|
true、false以外のパターンがくると、return
で値を返すことなくメソッドを抜けてしまうので、「値を返さないコード パスがあります」というエラーを起こす。しかし、そんなパターンは本来あり得ないはずである。すなわち、
bool
型はtrueとfalseの2つの値で完備(全パターン網羅している)- 完備性のチェックが正しく機能していれば、上記のコードはコンパイルできるはず:
- コンパイルできないのは完備性のチェックが緩いから
ということだ。bool
型以上に問題となるのは列挙型だろう。C#の列挙型は特に完備性のチェックが難しい。例えば以下のようなコードを見てもらえれば、このことが分かるだろう。
enum Color
{
Red = 0,
Green = 1,
Blue = 2,
}
static void M()
{
// RedでもGreenでもBlueでもない値を代入可能
Color c = (Color)100;
}
|
列挙型の定義時に用意したのとは別の値をいくらでも無制限に使えてしまうため、全パターンの網羅は不可能である。
残念ながら、今のところこの完備性チェックの緩さは解決できず、default
句を必ず書いてしのぐことになる。例えば、上記コードは以下のように書き直す必要がある。
string 和名(bool x)
{
switch (x)
{
case true: return "真";
case false: return "偽";
default: throw new InvalidOperationException();
}
}
|
switch
式の場合はどうだろうか。繰り返しになるが、式には戻り値が必須である。幸い、ちょうど前節で説明した通り、throw
も式になり戻り値を返せる(返したものと見なせる)ようになったので、これをさっそく使うことになる。
var result = x switch (
case true: "真",
case false: "偽",
default: throw new InvalidOperationException());
|
throw
式でごまかすのではなく、完備性チェック自体についても一応検討課題には挙がっている。ただ、bool
型はともかく、列挙型への対応は難しそうだ。
宣言式
次の「式化」は、変数宣言である。変数を宣言すると同時に変数の値をそのまま返す、宣言式(declaration expressions)というものが提案されている。例えば以下のようなコードを書ける。
static int Square(string s) => (var x = int.Parse(s)) * x;
|
Square
関数は、宣言式を使ってローカル変数x
を宣言すると共にその初期値をint.Parse(s)
した上で、そのx
の二乗を戻り値とする。
要するに、ちょっとした一時変数を式の中で宣言でき、このためだけにステートメントを書く必要がなくなる。
宣言式は、一度はC# 6向けに検討されていた。しかしその後、C# 6のリリース直前になって、パターンマッチング(次回以降で説明)のアイデアが生まれ、それと一緒にC# 7以降に仕切り直すことになった。
パターンマッチングが入る前には、以下のようなコードで宣言式が有用ではないかといわれていた。
class Base { }
class A : Base { public int X { get; set; } }
class B : Base { public int Y { get; set; } }
class C : Base { public int Z { get; set; } }
static int M(Base x)
{
if ((var a = x as A) != null) return a.X;
if ((var b = x as B) != null) return b.Y;
if ((var c = x as C) != null) return c.Z;
throw new InvalidOperationException();
}
|
メソッドM
は、x
の実際の型に応じて、返す値が変わる。
しかしこの用途での宣言式はパターンマッチングに置き換えられることになる。詳細は次回以降で説明するが、以下のように書けるようになるだろう。
static int M(Base x)
=> x switch (
case A a: a.X,
case B b: b.Y,
case C c: c.Z,
default: throw new InvalidOperationException());
|
私見として、この他で有用と思われる用途は、out
パラメーターと番兵型ループの2つである。表5にこれらの例を示そう。
宣言式を使った書き方 | 従来の書き方 | |||||
---|---|---|---|---|---|---|
outパラメーター |
|
|
||||
番兵型ループ |
|
|
||||
|
|
この例を見てもらえれば、単に変数宣言ステートメントが1つ減るだけではないことが分かるだろう。従来の書き方では、型推論(var
の利用)ができないのに対して、宣言式を使えばそれができる。また、変数のスコープも短くできる。
ちなみに、現状のディスカッションの様子を見るに、もしかすると、out
パラメーターに対する宣言式(通称「out var
」)だけ先に実装するかもしれないそうだ。前述の通り、宣言式の主な用途の1つがパターンマッチングに置き換わり、番兵型ループはそうめったに書くものではないことから、残る主な用途はout var
のみとなるからである。
ただ、out
パラメーターからタプル(次回以降で説明)への自動変換も検討項目に上がっていて、それがあればout var
すら要らなくなる可能性がある。例えば、int.TryParse
であれば、以下のようなコードが認められるようになるかもしれない。
(bool success, int value) result = int.TryParse(s);
|
あるいはパターンマッチングを使った以下のような書き方もできるようになるかもしれない。
let (var success, var value) = int.TryParse(s);
|
ということで、実はそれほど大きな需要は残らないのだが、入って困ることもそう多くなく、取りあえずはC# 7に入りそうである。
宣言式で定義した変数のスコープ
宣言式では、変数のスコープは原則として、「直近のステートメント内」となる予定だ。例えば以下のコードでは、3つのステートメント内のいずれにもx
という変数があるが、それぞれ別の変数となる。
var a = int.TryParse(ReadLine(), out var x) ? x : 0;
var b = double.TryParse(ReadLine(), out var x) ? x : 0;
var c = bool.TryParse(ReadLine(), out var x) ? x : 0;
|
「原則として」といったのは、if-else
ステートメントだけ例外となるからだ。if-else
ステートメントでは、if
の条件式内で宣言した変数は、if
側のブロックでだけ使え、else
側からは使えない。先ほど「パターンマッチングに置き換えられそう」だと言ってお見せした例をあらためて出すが、以下のようなコードを書きたくなることがあるだろう。
static int M(Base x)
{
if ((var y = x as A) != null) return y.X;
else if ((var y = x as B) != null) return y.Y;
else if ((var y = x as C) != null) return y.Z;
throw new InvalidOperationException();
}
|
この例のように、if
ごとに別スコープを持ちたいという要求の方が、else
も同スコープにする要求よりも多かったようだ。
シーケンス式
最後に紹介するのはシーケンス式(sequence expressions)だ。少し名前が分かりづらい(将来的に名称変更の可能性も結構あると思われる)が、「1つの()
内に、;
区切りで複数の式を並べて、最後の式の値を返す」というものだ(名前から想像するかもしれないが、IEnumerable
を返すような何かではない)。例えば以下のようなコードを書ける。
static int M(string s) => (var x = int.Parse(s); M(x, x));
static int M(int x, int y) => x * y;
|
この構文は、要するにC言語のコンマ演算子相当の機能だ。区切り文字に,
ではなく;
を使うのは、パラメーターリストやタプル(次回以降で説明)との区別のためである。
前節の通り、宣言式で定義した変数のスコープはかなり短い。そのため、このような1つの式内に複数の式を並べられる構文が必要になる。
まとめ
近年、プログラミング言語の構文はステートメントよりも式を好む傾向にある。C#もその例にもれず、どんどん式を増やしている。C# 7では、以下のようなものが提案されている。
throw
式switch
式- 宣言式
- シーケンス式
これらには、パターンマッチングやタプルなど、他の構文との兼ね合いもあって、そちらが安定してから調整が入る可能性も高い。
式が増えることによって、手短に書けるコードが増えるだろう。便利すぎることで1行に何でも詰め込んだようなひどいコードが生産されるという懸念もあるが、手短なコードを書く手段がないよりはいいだろう。
岩永 信之(いわなが のぶゆき)
※以下では、本稿の前後を合わせて5回分(第1回~第5回)のみ表示しています。
連載の全タイトルを参照するには、[この記事の連載 INDEX]を参照してください。
1. オープンソースのC#/Roslynプロジェクトで見たこと、感じた教訓
日本を代表する「C#(でぐぐれ)」の人、岩永信之氏によるコラムが遂に登場。今回はオープンソースで開発が行われているC#と開発者の関わり方について。
2. 【現在、表示中】≫ 次期C# 7: 式の新機能 ― throw式&never型/式形式のswitch(match式)/宣言式/シーケンス式
C# 6が出てまだ間もないが、すでに次バージョン「C# 7」についての議論が進んでいる。その中で提案されている「式」に関する新機能を取り上げる。
3. 次期C# 7: 複数データをまとめるための言語機能、タプル型
メソッドが複数の値を戻す場合など、複数のデータを緩くまとめて、扱いたい場合はよくある。C#の次バージョンではこれを簡潔に記述するための機構として「タプル型」が導入される。
4. 次期C# 7: 型に応じた分岐や型の分解機能、パターンマッチング
オブジェクトの型を階層的に調べて分岐処理を行いたい場合がある。そのための機能として、C#の次バージョンでは「パターンマッチング」が追加される。