syghの新フラグメント置き場

プログラミングTipsやコード断片の保管場所です。お絵描きもときどき載せます。

C# 6.0コンパイラーの興味深い挙動

C#言語はnullに関連する演算子が豊富です。

null合体演算子

null合体演算子は左側オペランドがnullでない場合は左側オペランドを返し、nullの場合は右側オペランドを返します。参照型のほか、Null許容型(Nullable)にも適用可能です。C# 2.0で追加されました。

x ?? alt

三項演算子で書くと以下のようになります。

x != null ? x : alt

null条件演算子

null条件演算子オペランドがnullでない場合はメンバーアクセス(.もしくは[])を実行します。参照型のほか、Null許容型(Nullable)にも適用可能です。C# 6.0で追加されました。

x?.SomeMethod();

if文で書くと以下のようになります。

if (x != null) { x.SomeMethod(); }

もしここでSomeMethod()が値型Tの戻り値を持つ場合、x?.SomeMethod()の評価結果はNull許容型T?となります。三項演算子で書くと以下のようになります。

x != null ? x.SomeMethod() : default(T?)

応用

これらの演算子を応用して、「引数がnullでない場合はToString()を呼び出し、nullの場合は代替のリテラル文字列"N/A"を返す」という処理をスマートに書いてみます。

// (1)
public static string ConvertToNAIfNull<T>(T x)
{
  return x?.ToString() ?? "N/A";
}

もしこれらの演算子を使わずに書くとすれば、以下のようになります。

// (2)
public static string ConvertToNAIfNull<T>(T x)
{
  return x != null ? x.ToString() : "N/A";
}

いずれにせよ1行で書けますが、(2)はxが2回出現して冗長なので、特にメソッドではなくインラインで書く際に不便そうですね。厳密に言うと、(1)と(2)は完全互換ではありませんが、通例ToString()は空文字列""を返却することはあってもnullを返却することはないので実用上は問題ないはずです。

ここで興味深いのが、(1)のジェネリクスには参照型やNull許容型だけでなく、通常の値型を渡しても実体化できるということです。csc 2015 (Visual C# 2015) でもgmcs 4.6.2でも動作します。

Console.WriteLine(ConvertToNAIfNull(new int?(10)));
Console.WriteLine(ConvertToNAIfNull(default(int?)));
Console.WriteLine(ConvertToNAIfNull(10));

値型に対する?.演算子呼び出しは無効なので、一見コンパイルエラーになってしまう気がしますが、(1)のジェネリクスC#コンパイラーが内部で以下のように展開するせいで、値型を渡してもコンパイルエラーにはならず実体化できる、というからくりになっているようです。

// (3)
public static string ConvertToNAIfNull<T>(T x)
{
  if (x != null) {
    var t = x.ToString();
    if (t != null) {
      return t;
    }
  }
  return "N/A";
}

値型に対してはx != nullコンパイルエラーにはならず常に真なので、(2)や(3)のジェネリクスに値型を渡せるのは当然です。いずれにせよ、ConvertToNAIfNull()に値型を渡すと冗長なメソッドになってしまいますが。