2016/09/21 (水) の朝方、throw 式に関する Pull-Request が master にマージされました!これにより、これまでステートメント (文) としてのみ提供されていた throw
を式として記述できるようになります。
throw 式が使えるところ
パッと確認した範囲では、以下の箇所で使えました。
- 条件演算子 (三項演算子)
- null 結合演算子
- ラムダ形式のメンバー
条件演算子 (三項演算子)
条件演算子で throw
式を使ってみると以下のような感じになります。
//--- C# 7 ではこう書ける static int IntParse(string value) => int.TryParse(value, out var result) ? result : throw new ArgumentException(nameof(value)); //--- C# 6 までの書き方 static int IntParse(string value) { int result; if (int.TryParse(value, out result)) return result; throw new ArgumentException(nameof(value)); }
C# 7 のコードを逆コンパイルしてみると以下のようになります。C# 6 までのコードと同様の展開がされることが分かりますね。
//--- 逆コンパイル結果 private static int IntParse(string value) { int result; if (!int.TryParse(value, out result)) { throw new ArgumentException("value"); } return result; }
null 結合演算子
null 結合演算子に対しても同じように使うことができます。
//--- C# 7 static object Assert(object x) => x ?? throw new ArgumentNullException(nameof(x)); //--- 逆コンパイル結果 private static object Assert(object x) { if (x == null) { throw new ArgumentNullException("x"); } return x; }
ラムダ形式のメンバー
C# 6 で導入された =>
によるプロパティやメソッドの簡易記法 (= ラムダ形式メンバー) でも使えます。例えば下記のように、未実装時の記述がシンプルで楽になりますね。
//--- C# 7 public string Name => throw new NotImplementedException(); public string SayHello() => throw new NotImplementedException(); //--- 逆コンパイル結果 public string Name{ get { throw new NotImplementedException(); }} public string SayHello(){ throw new NotImplementedException(); }
[おまけ] throw null;
みなさん、null
を throw
するとどうなるかご存知ですか?実は NullReferenceException
が飛びます。
try { //--- 以下のふたつは等価 throw null; throw new NullReferenceException(); } catch {}
違う型の例外でも NullRefereneceException
になります。
try { NotSupportedException ex = null; string a = null; var b = a ?? throw ex; //--- NotSupportedException は飛ばない } catch (Exception ex2) { //--- System.NullReferenceException Console.WriteLine(ex2.GetType()); }
throw 式の結合優先度
岩永先生 (@ufcpp) が throw
式の結合優先度を調べて教えてくださいました。まとめてくださった Gist をそのまま拝借するとこんな感じらしいです。
最初どうしてこうなるのか全然わからなかった以下のような例も、結合優先度のおかげで理屈が分かって納得しました。
try { //--- throw null されると思ってたのに、されないで ArgumentNullException が飛んだ NotSupportedException ex = null; string a = null; var b = a ?? throw ex ?? throw new ArgumentNullException(); //--- けど ↑↑ は結合優先度の問題なので、実はこれと一緒 //var b = a ?? throw (ex ?? throw new ArgumentNullException()); } catch (Exception ex2) { //--- System.ArgumentNullException Console.WriteLine(ex2.GetType()); }
上記を逆コンパイルすると以下のように展開されます。
try { NotSupportedException ex = null; string a = null; if (a == null) { NotSupportedException expr_0A = ex; if (expr_0A == null) { throw new ArgumentNullException(); } throw expr_0A; } var b = a; } catch (Exception ex2) { Console.WriteLine(ex2.GetType()); }
throw 式の型
例えば条件演算子の場合、コンパイラはその戻り値の型を決定しなければなりません。これまでは以下のようになっていました。
//--- こんな継承関係があるとする class Base {} class A : Base {} class B : Base {} //--- A は Base になり得るので x は Base 型 var x = true ? new A() : new Base(); //--- コンパイルエラー : 継承元を探してくれたりはしない var x = true ? new A() : new B();
throw
式は条件演算子に書くことができると先に書きました。この場合 throw
式の型はどうなるのかというと、答えは「どんな型にでもなれる」です。こんな型を「bottom 型 (= すべての型から派生している型)」と言います。.NET Framework において双対するのが object
型で、これを「top 型 (= すべての型の基底となる型)」と言うそうです。
//--- x の型は A と推論される var x = true ? new A() : throw new Exception(); //--- x の型は B と推論される var x = true ? throw new Exception() : new B();
つまり条件演算子は throw 式
ではない方の型で推論される動きになるのですが、両方を throw
式にしたらどうなるか。これは (当然) コンパイルエラーになります。
//--- 型推論できないのでエラー //--- Type of conditional expression cannot be determined because there is no implicit conversion between '<throw>' and '<throw>' var x = true ? throw new NotSupportedException() : new ArgumentNullException();