Build Insiderオピニオン:岩永信之(2)

Build Insiderオピニオン:岩永信之(2)

次期C# 7: 式の新機能 ― throw式&never型/式形式のswitch(match式)/宣言式/シーケンス式
―― さまざまな式が書けて、もっと便利になるC# ――

2015年12月24日

C# 6が出てまだ間もないが、すでに次バージョン「C# 7」についての議論が進んでいる。その中で提案されている「式」に関する新機能を取り上げる。

岩永 信之
  • このエントリーをはてなブックマークに追加

 C# 7として提案されている文法の中には大小さまざまなものがある。今回紹介するのはその中では小さめの機能になる。「式を増やす」という、利便性の向上を目的としたものである。

 C#に限らず、プログラミング言語の文法要素には大きく分けてexpression)とステートメントstatement: 文法の文脈では「平叙文」。断定形の文章)がある。これらには、表1に示すような性質がある。

ステートメント
どこにでも書ける ブロックの中にしか書けない
短く書ける場合が多い 相対的に長い
戻り値が必須 戻り値がない
表1 式とステートメントの性質

 C#における例をいくつか表2に示す。

式の例ステートメントの例
・四則演算
x + y
・ifステートメント
if (x < 0)
{
}
・配列アクセス
a[0]
・変数宣言
var x = 10;
・型変換
(int)obj
・throwステートメント
throw new InvalidOperationException();
表2 C#の式とステートメントの例

 ひと言でいうと、戻り値を返せるなら式になっている方が便利である。その結果、新しい言語ほど、ステートメントを減らして式を増やす傾向にある。関数型言語にカテゴライズされるようなプログラミング言語ではもともとその傾向が強かったが、近年ではどんなパラダイムの言語であろうと式を好むようになっている。

 C#も例にもれず、C# 3.0やC# 6で、いくつか式として書けるものが増えている。いくつかの例を表3に挙げよう。

 式にできる書き方ステートメントでの書き方
初期化
new Point
{
  X = 10,
  Y = 20,
};
var p = new Point();
p.X = 10;
p.Y = 20;
null検査
s?.Length
int? len;
if (s != null) len = s.Length;
else len = null;
データ列の加工
input.Select(x => x * x)
foreach (var x in input)
{
  yield return x * x;
}
表3 C#のバージョンアップで追加された式

 そして、C# 7でも、式にできるものが増える予定である。

式への抵抗感

 式はどこにでも書けて便利と説明したが、その一方で、どこにでも書けすぎるために、読みづらいコードも書けてしまう。それを嫌って、式が増えることに抵抗を感じる開発者もいまだに見受けられる(ベテランの方に多い傾向に見える)。

 それでも、式しか書けない場所が多い以上、ステートメントとしてしか書けない構文が多いことはよくないというのが筆者の意見だ。嫌なコードを書けてしまう問題よりも、そもそも書く手段が全くないことの方が問題は大きい。

 「書けてしまうから嫌」というなら、ステートメントだってひどいものである。書く人が書けば、{}の入れ子でソースコードはどんどん右側にずれていく。例として、ちょっとした総当たり計算を挙げよう。少々懐かしいネタではあるが、「send+more=money」という覆面算を解く問題がはやったことがある。そのとき、クエリ式(他のプログラミング言語だと、リストモナドやリスト内包)を使ったものと、ステートメントを使ったものの比較がよく取り上げられた。クエリ式を使うと以下のように書ける。

C#
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);
クエリ式を使った覆面算の解法

 一方、ステートメントで書くと以下のようになるだろう。もちろん、{}を省略したり、無理やりインデントをなくしたりすれば多少はすっきりするが、コード成型ツールをかけるたびに再びこのインデントがかかったりする。

C#
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ステートメントで書くほどのものではないだろう。

C#
var result =
  x < 1000 ? "" :
  x < 1000000 ? "k" :
  x < 1000000000 ? "M" :
  "G";
「?:」演算子でスッキリと記述

C# 7で提案されている式

 前振りが長くなったがここからが本題で、C# 7での提案についての話に入る。以下のようなものが提案されている。

  • throw
  • switch
  • 宣言式
  • シーケンス式

 それぞれ説明していこう。

throw

 1つ目は、例外のthrowを式にしようというものである。前述の通り、式にするためには戻り値が必要である。つまり、throwの戻り値がどうなるべきかという話に帰結する。

throw式

 ここでまず思い起こしてほしいのは、戻り値の型によらず、throwステートメントをもってメソッドを抜けられるということである。例えば、以下のメソッドはいずれもコンパイルできる。

C#
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になる。

 もっとも、throwが式になってうれしい場面は限られていて、以下の4つである。

  1. 条件演算子?:の第2オペランドと第3オペランド:
    x >= 0 ? x : throw new ArgumentException();
  2. null結合演算子??の第2オペランド:
    x ?? throw new ArgumentException();
  3. 式形式の関数メンバー:
    void NeverReturn() => throw new NotSupportedException();
  4. (後述する)switch式の中

 C# 7では、この4カ所でのみ、throwを式として書けるようにする方向で検討が進んでいる。

 ちなみに、「どんな型にでも変換可能な型」を「bottom型」(最下部の型、全ての型の派生扱い)と呼ぶことがある。この逆は「どんな型からでも変換可能な型」で、「top型」(最上部の型、全ての型の基底扱い)と呼び、C#の場合はobject型がこれに当たる。つまり、throw式が返す値の型はobject型の対極ということになる。

never型

 throw式から一歩進めて、「必ず例外をthrowするメソッドの戻り値」を考えてみよう。

 例外には、任意のメソッドを抜けるのに使えるという性質に加えて、throwステートメントから後ろは実行されなくなる(returnステートメントやyield breakステートメントと同じ)性質がある。この性質はコンパイラーも理解していて、適切なコード診断をしてくれる。

C#
void M()
{
  throw new NotSupportedException();

  // ここは実行されない
  // コンパイラーも理解していて、警告などを出す
}
throwステートメントより後ろは実行されない

 これに対して、例外をthrowする部分をメソッドに切り出すことを考えよう。現状だと以下のようになる。NeverReturnメソッドは常に例外をthrowするメソッドなので、そのメソッド呼び出しから後ろは実行されないはずだが、コンパイラーにはそれが分からず、各種診断が働かない。

C#
// 常に例外をthrowするメソッド
void NeverReturn() { throw new NotSupportedException(); }

void M()
{
  NeverReturn();

  // ここは実行されないはず
  // でも、コンパイラーにはそれを判断できない
}
NeverReruntメソッドは常に例外をthrowするが、コンパイラーにはそれが分からない

 この問題の解決策として、「常に例外をthrowするから、そこから後ろは実行されない」ということを表す型が欲しい。こういう型として、neverという提案が出ている。never型とは以下のようなものである。

  • 常に例外を返す式の戻り値を表す
  • 任意の型に変換可能
  • この型を返すメソッドを呼び出すと、そこから後ろは実行されない

 これを使うと、先ほどのコードは以下のように書き換えられる。

C#
// 常に例外をthrowするメソッド
never NeverReturn() { throw new NotSupportedException(); }

void M()
{
  NeverReturn();

  // ここが実行されないことを、コンパイラーが判断できる
}
メソッドの戻り値型がnever型であれば、そのメソッド呼び出しより後にあるコードは実行されないことが判断できる

 他のプログラミング言語では、ScalaがNothing型という名前で同様の型を持っている。C#への導入に当たっても、neverの他にbottomnothingなどの名前が検討されたが、「後ろが実行されない」という性質が名前に現れるよう、neverが最有力となっている。

 never型の実現方法は2種類ある。

  1. voidのメソッドに、NeverReturnといったような名前の属性を付ける
  2. .NETランタイム側にnever型を表す特別な型を足してもらう

 1のやり方であれば、C#コンパイラーだけで完結していて、近い将来に実装できる可能性が高い。一方で、ジェネリックの変性に対応できないなどの問題があり、1の方式での実装は見送られることになりそうだ。

 従って2のやり方が有力ではあるが、これには.NETランタイムのレベルで手を入れる必要がある。.NETランタイム側にはこれよりも優先度の高いタスクが山積みで、never型の実装に取り掛かれるのはだいぶ先になるだろう。前節のthrow式だけでも十分な利便性を得られるということもあり、never型はしばらく日の目を見なさそうである。

式形式のswitch(match式)

 続いてはswitchの式化だ。これまでのC#でも、if-elseステートメントやループに相当する処理を式で書ける構文がある。表4に示すように、条件演算子?:やLINQがそれに当たる。

式にできる書き方ステートメントでの書き方
var sign = x >= 0 ? "+" : "-";
string sign;
if (x >= 0) sign = "+";
else sign = "-";
var norm = input.Sum(x => x * x);
double norm = 0;
foreach (var x in input)
{
  norm += x * x;
}
表4 if-elseステートメントやループステートメントに相当の式の記述

 一方で、switch相当の式はこれまでのC#にはなかった。また、次回以降で説明するパターンマッチングが登場すると、switchの利用頻度が増えることが予想される。そこで、switch相当の式が提案されている。現状の最有力候補の文法例は以下のようになっている。

C#
var result = x switch (
  case 1: "one",
  case 2: "two",
  case 3: "three",
  default: "other");
switch式の構文の候補

 他の候補としては、以下のような案が出ている。

  • ステートメントとの弁別のために、switchの代わりにmatchキーワードを使う
  • switchの後ろの()を必須にするかどうか
  • caseの区切りに, を使うかどうか
完備性

 このswitch式(あるいはmatch式)を導入するに当たって、問題となるのは値の「完備性」(completeness)である。ここでいう完備というのは、「全てのパターンを網羅している」というような意味である。C#は完備性のチェックが緩く、switch(今後導入されるswitch式に限らず、既存のswitchステートメントでも)を使う上でもたびたび問題となる。

 例えば、C#では、bool型はtrueもしくはfalseのいずれかの値しか取らない。にもかかわらず、以下のswitchステートメントはコンパイルエラーとなる。

C#
string 和名(bool x)
{
  switch (x)
  {
    case true: return "真";
    case false: return "偽";
  }
}
パラメーターxが取る値はtrueかfalseのどちらかだけ

 truefalse以外のパターンがくると、returnで値を返すことなくメソッドを抜けてしまうので、「値を返さないコード パスがあります」というエラーを起こす。しかし、そんなパターンは本来あり得ないはずである。すなわち、

  • bool型はtruefalseの2つの値で完備(全パターン網羅している)
  • 完備性のチェックが正しく機能していれば、上記のコードはコンパイルできるはず:
    • コンパイルできないのは完備性のチェックが緩いから

ということだ。bool型以上に問題となるのは列挙型だろう。C#の列挙型は特に完備性のチェックが難しい。例えば以下のようなコードを見てもらえれば、このことが分かるだろう。

C#
enum Color
{
  Red = 0,
  Green = 1,
  Blue = 2,
}

static void M()
{
  // RedでもGreenでもBlueでもない値を代入可能
  Color c = (Color)100;
}
C#の列挙型は完備性のチェックが難しい

 列挙型の定義時に用意したのとは別の値をいくらでも無制限に使えてしまうため、全パターンの網羅は不可能である。

 残念ながら、今のところこの完備性チェックの緩さは解決できず、default句を必ず書いてしのぐことになる。例えば、上記コードは以下のように書き直す必要がある。

C#
string 和名(bool x)
{
  switch (x)
  {
    case true: return "真";
    case false: return "偽";
    default: throw new InvalidOperationException();
  }
}
xの値がtrue、false以外の場合は、何か問題となる操作が行われている

 switch式の場合はどうだろうか。繰り返しになるが、式には戻り値が必須である。幸い、ちょうど前節で説明した通り、throwも式になり戻り値を返せる(返したものと見なせる)ようになったので、これをさっそく使うことになる。

C#
var result = x switch (
  case true: "真",
  case false: "偽",
  default: throw new InvalidOperationException());
switch式内でthrow式を使うことで、式が必ず戻り値を持つようになる

 throw式でごまかすのではなく、完備性チェック自体についても一応検討課題には挙がっている。ただ、bool型はともかく、列挙型への対応は難しそうだ。

宣言式

 次の「式化」は、変数宣言である。変数を宣言すると同時に変数の値をそのまま返す、宣言式(declaration expressions)というものが提案されている。例えば以下のようなコードを書ける。

C#
static int Square(string s) => (var x = int.Parse(s)) * x;
宣言式の使用例(その1)

Square関数は、宣言式を使ってローカル変数xを宣言すると共にその初期値をint.Parse(s)した上で、そのxの二乗を戻り値とする。

 要するに、ちょっとした一時変数を式の中で宣言でき、このためだけにステートメントを書く必要がなくなる。

 宣言式は、一度はC# 6向けに検討されていた。しかしその後、C# 6のリリース直前になって、パターンマッチング(次回以降で説明)のアイデアが生まれ、それと一緒にC# 7以降に仕切り直すことになった。

 パターンマッチングが入る前には、以下のようなコードで宣言式が有用ではないかといわれていた。

C#
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();
}
宣言式の使用例(その2)

メソッドMは、xの実際の型に応じて、返す値が変わる。

 しかしこの用途での宣言式はパターンマッチングに置き換えられることになる。詳細は次回以降で説明するが、以下のように書けるようになるだろう。

C#
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パラメーター
int.TryParse(s, out var x) ? x : 0; 
int x;
int.TryParse(s, out x) ? x : 0;
番兵型ループ
while ((var line = ReadLine()) != null)
  ... ;
string line;
while ((line = ReadLine()) != null)
  ...  ;
while ((var c = s.ReadByte()) != -1)
  ;
int c;
while ((c = s.ReadByte()) != -1)
  ;
表5 宣言式の主な用途

 この例を見てもらえれば、単に変数宣言ステートメントが1つ減るだけではないことが分かるだろう。従来の書き方では、型推論(varの利用)ができないのに対して、宣言式を使えばそれができる。また、変数のスコープも短くできる。

 ちなみに、現状のディスカッションの様子を見るに、もしかすると、outパラメーターに対する宣言式(通称「out var」)だけ先に実装するかもしれないそうだ。前述の通り、宣言式の主な用途の1つがパターンマッチングに置き換わり、番兵型ループはそうめったに書くものではないことから、残る主な用途はout varのみとなるからである。

 ただ、outパラメーターからタプル(次回以降で説明)への自動変換も検討項目に上がっていて、それがあればout varすら要らなくなる可能性がある。例えば、int.TryParseであれば、以下のようなコードが認められるようになるかもしれない。

C#
(bool success, int value) result = int.TryParse(s);
タプル型の変数resultは、int.TryParseメソッドの戻り値とoutパラメーターの値を格納する

 あるいはパターンマッチングを使った以下のような書き方もできるようになるかもしれない。

C#
let (var success, var value) = int.TryParse(s);
パターンマッチングによって、int.TryParseメソッドの戻り値とoutパラメーターの値をsuccess変数、value変数に代入

 ということで、実はそれほど大きな需要は残らないのだが、入って困ることもそう多くなく、取りあえずはC# 7に入りそうである。

宣言式で定義した変数のスコープ

 宣言式では、変数のスコープは原則として、「直近のステートメント内」となる予定だ。例えば以下のコードでは、3つのステートメント内のいずれにもxという変数があるが、それぞれ別の変数となる。

C#
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側からは使えない。先ほど「パターンマッチングに置き換えられそう」だと言ってお見せした例をあらためて出すが、以下のようなコードを書きたくなることがあるだろう。

C#
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を返すような何かではない)。例えば以下のようなコードを書ける。

C#
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行に何でも詰め込んだようなひどいコードが生産されるという懸念もあるが、手短なコードを書く手段がないよりはいいだろう。

岩永 信之(いわなが のぶゆき)

岩永 信之(いわなが のぶゆき)

 

 

 ++C++; の中の人。C# 1.0がプレビュー版だった頃からC#によるプログラミング入門を公開していて、C#とともに今年で15年になる。最近の自己紹介は「C#でググれ」。

 

 

 

 

※以下では、本稿の前後を合わせて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#の次バージョンでは「パターンマッチング」が追加される。

5. 次期C# 7: パターンマッチングの内部的な仕組み

C#の次バージョンに追加される「パターンマッチング」の挙動(パターンマッチングがどう展開されるのかや、その実現方法)について説明する。

イベント情報(メディアスポンサーです)

サイトからのお知らせ

Twitterでつぶやこう!