写像したくない?
C#でNullableな値を写像をするための追加定義をするために次のようなコードを書く。
public static class NullableExtensions
{
public static U? Map<T, U>(this T? value, Func<T, U> func) where T : struct where U : notnull
{
return value is null ? default(U?) : func(value.Value);
}
public static U? Map<T, U>(this T? value, Func<T, U> func) where T : class where U : notnull
{
return value is null ? default(U?) : func(value);
}
}このコードを書くと次のような演算が可能となり、三項演算子を消すことができる。配列に対する写像はよく見られるが、Nullableに対しての写像もこれはこれで非常に便利なものとなる。
var a = (int?)42;
var b = a.Map(x => x * 2);ふたつの定義が必要なのは this が struct のときと class のときがあるからだ。C#ではNullableな値型は Nullable<T> として管理されるので、場合分けが必要なのである。
このコードは一見うまく動作するように見えるが、値型への写像の際にワナがあり、それは default(T?) が default(T) として評価されることだ。そのためメソッドの返値は U? にも関わらず値型なら0相当の値になので、intなら 0 になるし、DateTimeOffsetなら 01/01/0001 00:00:00 +00:00 である。一方でクラス型なら期待通り null になる。
同じような疑問を持ったユーザーの投稿がStack Overflowにあったので引用しておく。
どうしてそうなるの
値型では T? は Nullable<T> where T : struct として評価される。つまり Nullable<T> も値型だ。値型の既定値は0相当の値なので、default(T?) == default(T) である。
で、どうする?
これはC#がNullableな値型を Nullable<T> として管理していることが原因で、これまでの資産と整合性を保つために致し方なかった対応ではあるものの、Dartのような透過的なNull許容性を提供できれば、こうした種々の問題が発生しなかったのではないかとも感じる。
こうした問題を回避する方法もある。C#にRust-likeなOptionパターンを導入していて、T を Option<U> に写像するなら Option<U> は値型もクラス型も格納できるので、Nullableにまつわる課題を解消しやすくなる。
しかし、C#はパターンマッチングが導入されつつあるが、それが得意な言語からすればまだまだ機能不足ではあるので、Optionパターンを導入したところで値の取り回しに苦労する点は否めない。これからの言語の発展に期待したい。