Null非許容
先週、「C#にもNull非許容な型が欲しい」という話をされたものの「うん、欲しいね」としか返せなかったり。要望は昔からあるけども、実際入れようと思うと結構大変。という話、説明しておいた方がいいんだろうなぁと思ったので、ブログにしてみることに。
先に結論の要旨だけ書くと、以下のような感じ。
- C#だけで完結する問題じゃなく、.NETレベルで対応するのは今からだときつい
- Code Contracts使って契約プログラミングするのがいいよ
以下詳細。
公式の見解
Anders Hejlsberg も、「もしもの話」として、1からの再設計が許されるなら C#/.NET をどうしたいかという質問に対して「Null 許容性の見直し」を挙げています。
一時期、結構頻繁にそういっていたと思います。少し検索して出てきたのでいうと、以下の Q&A インタビューの、1:00:00 からのくだり。
言っていることは以下のような話です。
-
参照型/値型の区分と、Null 許容/非許容 の概念は独立させておくべきだった
- 今、値型に関しては、Nullable<T> によって、Null 許容な値型が作れるけども、参照型は常に Null 許容になってしまう
- 参照型も、Null 非許容な参照型が欲しい
これは本当に「もしもの話」。1から作り直すのであればどうするか、逆にいうと、直したいけども今から直すのはかなり厳しいという話だったりします。
C#/.NETは、個人的な予想としておそらく最低あと10年は後方互換性を維持しながらの発展になると思うので、この「もしも」はあったとしても10年以上先の話。
C#だけの問題でなく
今、値型に対する Null 許容は、値型 T に対して、T? と書くことでできるわけです。
int? x = null;
これの逆で、参照型 T に対して、例えば、T! とか書いて、Null 非許容を表すのはどうでしょう。
T! x = new
T(); // OK
T! y = null; // エラー
インスタンスが常にnew演算子に作られる分にはこれでいいんですが、問題はメソッドをまたぐ場合。例えば、ファクトリを介するだけで破たんします。
var factory = new
TFactory();
T! x = factory.Create(); // こいつが null を返さない保証は誰がするの?
T! y = T.Parse(Console.ReadLine()); // 同上
この例の、CreateやParseの戻り値が T! なら万事解決かというとそうでもなく。2点ほど問題があります。
-
既存コードはどうする?
- 「全部を T! に書き換える、さもなくば T! は使えない」という状況になるけども
-
T! はどうコンパイルされるべき?
- C#で閉じている分にはまだしも、他の言語ではどうする?
C#だけなら、属性を付けるとか、あるいは、以下のような構造体に置き換えるようなコード生成で対処できるかもしれない。
struct
NonNullable<T>
where T : class
{
public T Value { get; private set; }
public NonNullable(T value)
{
if (value == null)
throw new ArgumentNullException();
Value = value;
}
}
でも、それだと結局、虚偽申告(実際はnullを返すクセに非null属性を付ける)ができたり、実行時チェックとかにしかできない(ビルド時のチェック効かない)ので、あまり有用にならず。
有用さがいまいちなのに中途半端な妥協実装をしても、後々足かせになる(将来的にもっといい方法があっても入れれなくなる&その他破壊的変更の原因になりかねない)という問題もあります。
Code Contracts
現状のC#で非null保証をしたければ、Code Contractsというのを使って、契約プログラミングします。Contractクラス(System.Diagnostics.Contracts名前空間)を使って、契約表明できます。
public static T Parse(string s)
{
Contract.Requires(s != null);
Contract.Ensures(Contract.Result<T>() != null);
return new T(); // 仮実装
}
これで、Parseメソッドがnullを返さないことを保証します。
さてここで、「なんだ、それで万事解決じゃない」と素直にならないところがまた… 現状のBestではあるものの、まだちょっとめんどくさい感じ。
このCode Contracts、以下のような仕組みで動いています。
-
このRequiresやEnsuresメソッドは、実際には何もしない
- Conditional属性が付いていて、コンパイル後に消える
-
静的検証
-
Visual Studio 2010に関しては、Code Contractsの別途インストールが必要
- 有償エディションにしか入らなかったはず
- その上で、コード解析オプションをOnにしないと働かない
- 静的コード解析、そこそこ時間がかかる
-
-
動的検証
-
静的解析しきれない部分を検証するために、動的検証コードを入れるオプションもある
- 契約の表明はあくまで自己申告なので、やろうと思えば虚偽申告できる
- Code Contractsに対応していないライブラリを使うと、必然的に何らかの動的検証が必要
-
この場合、(C#のコンパイル結果の)ILコードを書き換えて、検証コードを埋め込んでる
- 契約違反は実行時例外発生
- もちろん、検証コード分、実行時の負担になる
-
ちなみに、.NETの標準ライブラリには、.NET 4から、Code Contractsによる契約が表明されています。また、書いた契約をドキュメントに反映する仕組みも持っています。
まあでも、説明中に、なかなかに警戒心を抱くキーワードが見受けられるわけです。「有償エディション」「時間がかかる」「自己申告」「IL書き換え」等々。Contractクラス自体はすでに標準ライブラリに含まれているので積極的に使っておいて損はないんですが、仕組みを知らない人が「あれ、Contract使ってもチェックしてもらえないんだけど?何これ?使えねー」と言ってるのを何度も目にしたりしてます。Ensuresの書き方が結構めんどくさいのも嫌な感じ…
> 参照型 T に対して、例えば、T! とか書いて、Null 非許容を表す
Spec# だと T! がありましたっけ。
Spec# はとっとと Code Contracts で置き換えてほしい。
> 既存コードはどうする?
Code Contracts を使っても、Ensures(Result != null) がついていないメソッドからの戻り値を Requres(arg != null) な引数に渡すと警告が出るので、この点はどっちもどっちな気が。
強引に黙らせることはできますけど。
あとは、FxCop と組み合わせると、Code Contracts で CA1062 が抑止できないのはとっとと直してほしかったり。
yield return との組み合わせでデバッグ時にイテレーター内の変数が追えなかったりするのも困りもの。
αετος (@aetos382)
2012年4月10日 at 10:38
> 既存コード、どっちもどっち
要するに、Code Contracts でいう Assume と動的検証が必要ですよねという話でして。
ufcpp
2012年4月10日 at 13:27
Null非許容 « ++C++; // 未確認飛行 C ブログ…
素敵なエントリーの登録ありがとうございます – .NET Clipsからのトラックバック…
.NET Clips
2012年4月12日 at 01:04