null 条件演算子を式ツリーデータに変換することができない
C# 6 の機能である null 条件演算子を式ツリーに入れようとしたら、図のようにコンパイルエラーが発生しました。メッセージは、「式ツリーのラムダに null 伝搬演算子を含めることはできません。」です。
「ラムダ式から式ツリーの変換って何か制約あったけ?」
「どうして三項演算子での書き方(図の後者の記述)だとOKなのにこれはだめなんだろう?」
といろいろ分からなかったので整理しました。
調べた結果、このコンパイルエラーの原因は次のことが原因でした。
- null 条件演算子は式ツリーデータに変換できない
- null 条件演算子と例の三項演算子は、同じことではない
- ラムダ式のすべてが式ツリーデータに変換できるわけではない
- また、null 条件演算子を式ツリーデータに変換するように対応すれば幸せ、という話でもない
言葉について
ここまで画像と文章にいろいろな言葉がでてきますが、参考リンクを置いておきます。このページには、後述するエラーとなる原因について説明があります。
- ラムダ式 http://ufcpp.net/study/csharp/sp3_lambda.html
- 式ツリー http://ufcpp.net/study/csharp/sp3_lambda.html#expression
- null条件演算子(null 伝搬演算子というのはVisual Studio上のみの表記) http://ufcpp.net/study/csharp/ap_ver6.html#null-conditional
ラムダ式から式ツリーの変換って何か制約あったけ?
ラムダ式から式ツリーへの代入には制約があります。null 伝搬演算子もその制約の1つでした。
式ツリーにできるのは前者(1文だけのラムダ式)だけです。
そうなると、結構強い制約がかかってきます。 例えば、for, while, switch などの制御構文や、x = 0 といったような代入式は式ツリーにできません。 あと、インクリメント・デクリメントも、実質的には加減算+代入なので、式ツリーにできません。 また、ラムダ式内でローカル変数を定義できません。
ラムダ式 http://ufcpp.net/study/csharp/sp3_lambda.html より
制約がある、というのは、式ツリーデータを表す Expression 型のクラス(厳密には、Expression 型の派生クラス)が用意されていなければいけない、ということです。対応する Expression 型の派生クラスがないと、変換できません。また、Expression が登場した.NET Framework 3.5 の時点で存在する機能しか、ラムダ式から Expression 型への変換はできません。.NET 3.5 以降にも Expression の派生クラスは追加されていますが、“ラムダ式からExpression 型への変換”は対応していないようです。
では、どれが使えてどれが使えないか、を判断するにはどこを見ればいいかというと、○×表みたいなのは見つからず、式ツリー(Expression Trees) - C# によるプログラミング入門 | ++C++; // 未確認飛行 C このページ等を見るのが良いかなと思います。
null 条件演算子と例の三項演算子は、同じことではない
最初に、「どうして三項演算子での書き方だとOKなのにこれはだめなんだろう?」と書きました。これは分解すると、「三項演算子が式ツリーデータに変換できるなら、null条件演算子も式ツリーデータとして書けていいのでは?だって同じようなことしてるでしょ」ということになります(何て傲慢な)。
これに対する答えは、「同じじゃない」になります。三項演算子は、プロパティに2回アクセスしていますが、null条件演算子の方は、プロパティへのアクセスは1回のみです。
三項演算子:
hanako.Status == null ? false : hanako.Status.HasShield;
hanako.Status に2回アクセスする可能性がある。
null 条件演算子:
hanako.Status?.HasShield
コンパイル後のコードをC#で書くと次のようになる。
var status = hanako.Status; if (status == null) { return null; } return status.HasShield;
hanako.Status に1回アクセスし、変数に代入している。
このように処理の内容が違うため、「三項演算子での書き方だとOKならnull 条件演算子でもOK?」という意見は通りません。
また、ここでは式ツリーに変換できない理由も確認できます。ラムダ式から式ツリーへの変換の制約の1つに、複数行で書くようなブロックタイプは式ツリーに変換できない、というのがあります。null 条件演算子はこのブロックのうちに該当するため、式ツリーへ変換できません。
null 条件演算子を式ツリーデータに変換するように対応すれば幸せ、という話でもない
じゃあ、null条件演算子を Expression 型派生クラスに変換できるようにすればいいのでは?結構使いそう。 .NET 4 以降に追加されたという Expression 型派生クラスも、ラムダ式から式ツリーへの変換に対応すればいいのでは? と思います。が、現状はいろいろな理由があって採用されていないようです。
要望は既に出されていました。
CodePlexなとき:Null-propagating operator ?. in Expression Trees: spec v1
案としては、演算子用のExpression型派生クラスを作成する・既存のExpressionで組み立てられるようにする、があるようです。
roslyn/2015-04-14 C# Design Meeting.md at master · dotnet/roslyn
問題としては、上記のページに
This operator could not be used in expression in .Net 3.5/4.0/4.5/4.5.1/4.5.1 because the class will not exist.
LINQ providers will need to be updated to take advantage of this operator.
とあります。Linq to SQL や既存ライブラリのことを考えると、簡単な話ではないよう。
null 条件演算子以外でも、ある機能を式ツリーサポートするか否かの議論がでています。
例
ちまみにこのコンパイルエラーがでてきたケースは、AutoMapper ライブラリや ASP.NET MVC の .cshtml の HTML Helper を使ったときです。