読者です 読者をやめる 読者になる 読者になる

xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

throw 式

C#

2016/09/21 (水) の朝方、throw 式に関する Pull-Request が master にマージされました!これにより、これまでステートメント (文) としてのみ提供されていた throw を式として記述できるようになります。

f:id:xin9le:20160924225231p:plain

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;

みなさん、nullthrow するとどうなるかご存知ですか?実は 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();