nullableの内部実装は、
- null非許容の参照型とnull許容の参照型は同じ型です。(属性の有無の違い)
- null非許容の値型とnull許容の値型は異なる型です。(シンタックスシュガーで隠れているが実際にはSystem.Nullable<T>型になる)
コンパイラの挙動は、
- コンパイル時にnull許容な変数にはnull許容とする属性を付与します。
(引数
及び戻り値
に対してはILに埋め込み、ローカル変数は埋め込まない) - コンパイラはnull許容の属性のついていない変数(非許容)に対して許容の型を代入していないかを、
コンパイル時
にチェックするのみで、実行時はチェックはしてない - ジェネリックの制約もコンパイル時にチェックが行われて、実行時にはチェックしてない。
(制約はILに埋め込み)
今回問題となっているコードでは戻り値にあるジェネリックパラメーターと引数に渡しているジェネリックパラメーターのどちらも同じTを使っています。
値型の方の型に実際に必要なのは、戻り値はList<T>で、引数の方はIEnumerable<Nullable<T>>です。
異なる型になるためにどちらの型を使用するのかを決定できません。
推測ですが、
- 参照型では実際にはどちらも同じ型になるので矛盾しない(型は確定)
- 値型では異なる型になるため矛盾する(型は非確定)
となって、エラーになるのかも。
なので、戻り値と引数とで使用しているジェネリックパラメーターを別扱いにして、戻り値の方にだけnotnull制約をつけるならば、この矛盾には抵触しないので回避はできます。
できますが、型推論ができないので、単純な使用はできないですね。
せいぜい実装を1か所にまとめられるくらい。
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
namespace CSConsoleCore
{
class Program
{
struct T { }
class C { }
static void Main(string[] args)
{
IEnumerable<int?> ni = new List<int?>(new int?[] { null, 1 }); //ValueTypeは値型
IEnumerable<T?> nt = new List<T?>(new T?[] { null, new T() }); //structは値型
IEnumerable<C?> nc = new List<C?>(new C?[] { null, new C() }); //classは参照型
IEnumerable<string?> ns = new List<string?>(new string?[] { null, "あ" }); //stringは参照型
List<string> s1 = ns.MyNonNull();
List<int> i1 = ni.MyNonNull();
List<T> t1 = nt.MyNonNull();
List<C> c1 = nc.MyNonNull();
List<string> s2 = ns.MyNonNull<string, string?>();
List<int> i2 = ni.MyNonNull<int, int?>();
List<T> t2 = nt.MyNonNull<T, T?>();
List<C> c2 = nc.MyNonNull<C, C?>();
}
}
static partial class Ext
{
// 値型の?はNullable<T>のシンタックスシュガーなので、戻り値と引数の型パラメーターは異なる型の変換が必要
// public static List<T> MyNonNull<T>(this IEnumerable<T?> list) where T : struct => MyNonNull<T, T?>(list);
public static List<T> MyNonNull<T>(this IEnumerable<Nullable<T>> list) where T : struct => MyNonNull<T, T?>(list);
// 参照型の?はコンパイル時にチェックがかかるだけなので、実行時の型変換は起こらない
public static List<C> MyNonNull<C>(this IEnumerable<C?> list) where C : notnull => MyNonNull<C, C?>(list);
// パラメーターを分けて、戻り値のジェネリックパラメーターにのみnotnull制約がかかるなら問題はない。しかし型推論はできない
public static List<U> MyNonNull<U, T>(this IEnumerable<T> list) where U : notnull => [.. list.OfType<U>()];
//// 残念ながらTをU?型とする制約をつけると失敗する
//public static List<U> MyNonNull<U, T>(this IEnumerable<T> list) where U : notnull where T : U? => [.. list.OfType<U>()];
}
}
# KeyValuePair<T,U>は構造体なので値型
# 属性付与が永続される対象を追記
# 代入チェックの説明の修正とtype等修正