【C#】ジェネリックと値型とnull許容／非許容の謎

ジェネリック関数の実型パラメータに値型を指定したときに、null 許容／非許容が期待通りに動かなくて困った。

---

まず、分かりやすいように、次の参照型と値型を定義しておく。

```c#
class Class { }
struct Struct { }
```

また、それらの型それぞれの null 許容型／非許容型のプロパティを定義しておく。

```C#
static Class   NonNullable_Class  => new Class();
static Class?  Nullable_Class     => new Class();
static Struct  NonNullable_Struct => new Struct();
static Struct? Nullable_Struct    => new Struct();
```

そして、型パラメータ `T` に対して
第1引数に `T` 型を
第2引数に `T?` 型を取る、
ジェネリック関数を用意する。

```C#
static void Fun<T>(T nonNullable, T? nullable) { }
```

さて、この関数を、型パラメータ `T` と2つの引数を様々に組み合わせて呼び出してみよう。

なお `OK` はコンパイルエラーや警告がないこと、`NG` はそれらがあることを表す。

まず、`T` を参照型にした場合。

```C#
Fun<Class?>(NonNullable_Class,   Nullable_Class);     // OK
Fun<Class?>(NonNullable_Class,   NonNullable_Class);  // OK
Fun<Class?>(Nullable_Class,      Nullable_Class);     // OK
Fun<Class?>(Nullable_Class,      NonNullable_Class);  // OK
                 
Fun<Class>( NonNullable_Class,   Nullable_Class);     // OK
Fun<Class>( NonNullable_Class,   NonNullable_Class);  // OK
Fun<Class>( Nullable_Class,      Nullable_Class);     // NG　第1引数が NG
Fun<Class>( Nullable_Class,      NonNullable_Class);  // NG　第1引数が NG
```

これは期待通りだ。

続いて `T` を null 許容の値型にした場合。

```C#
Fun<Struct?>(NonNullable_Struct, Nullable_Struct);    // OK
Fun<Struct?>(NonNullable_Struct, NonNullable_Struct); // OK
Fun<Struct?>(Nullable_Struct,    Nullable_Struct);    // OK
Fun<Struct?>(Nullable_Struct,    NonNullable_Struct); // OK
```

これも期待通り。

最後に `T` を null 非許容の値型にした場合。

```C#
Fun<Struct>(NonNullable_Struct,  Nullable_Struct);    // NG　なぜか第2引数が NG
Fun<Struct>(NonNullable_Struct,  NonNullable_Struct); // OK
Fun<Struct>(Nullable_Struct,     Nullable_Struct);    // NG　第1引数が NG、なぜか第2引数も NG
Fun<Struct>(Nullable_Struct,     NonNullable_Struct); // NG　第1引数が NG
```

これが期待通りでない。
`T` を `Struct` にすると第2引数の `T?` は `Struct?` になることを期待したのだが、
なぜか `T?` が null 非許容の `Struct` になっているようだ。
（関数呼び出しの関数名にポインターを乗せると IDE が表示する関数のシグネチャーもそうなっている。）

## 対策

次のように、参照型と値型で関数を分けてみた。

```C#
static void Fun_Class<T>( T nonNullable, T? nullable) where T : class { }
static void Fun_Struct<T>(T nonNullable, T? nullable) where T : struct { }
```

そうすると期待通りの結果になった。

```C#
Fun_Class<Class>(  NonNullable_Class,  Nullable_Class);     // OK
Fun_Class<Class>(  NonNullable_Class,  NonNullable_Class);  // OK
Fun_Class<Class>(  Nullable_Class,     Nullable_Class);     // NG　第1引数が NG
Fun_Class<Class>(  Nullable_Class,     NonNullable_Class);  // NG　第1引数が NG

Fun_Struct<Struct>(NonNullable_Struct, Nullable_Struct);    // OK
Fun_Struct<Struct>(NonNullable_Struct, NonNullable_Struct); // OK
Fun_Struct<Struct>(Nullable_Struct,    Nullable_Struct);    // NG　第1引数が NG
Fun_Struct<Struct>(Nullable_Struct,    NonNullable_Struct); // NG　第1引数が NG
```

なお、
`where T : class` は `T` を null **非許容**参照型に、
`where T : struct` は `T` を null **非許容**値型に制限するため、
`T` を `Class?` や `Struct?` にするとコンパイルエラーになる。

## 補足：ジェネリック型制約 `where` とオーバーロード

通常、ジェネリック型制約が異なるだけの関数はオーバーロードして認められず、コンパイルエラーになる。

```C#
static void Fun<T>() where T : class { }
static void Fun<T>() where T : struct { } // NG
```

そのため、「対策」で述べたように参照型用と値型用で関数を分ける場合は、それぞれ異なる関数名にする必要がある。

ただし引数に `T?` 型が含まれている場合、
ジェネリック関数に制約 `where T : struct` を付けたものはオーバーロードとして認められる。

```C#
static void Fun<T>(T? arg) where T : class { }
static void Fun<T>(T? arg) where T : struct { } // OK
```

`T` が値型の場合、`T?` は実際には `Nullable<T>` となる。
そのため上で述べたコードは実際には次のようになり、オーバーロードと見なせるのだろう。

```C#
static void Fun<T>(T?          arg) where T : class { }
static void Fun<T>(Nullable<T> arg) where T : struct { } // OK
```

しかし、`class`（参照型）であってかつ `struct`（値型）でもある型はないのだから、
この節の最初に挙げた次のコードもオーバーロードとして扱ってほしいものだ。

```C#:再掲
static void Fun<T>() where T : class { }
static void Fun<T>() where T : struct { } // NG
```

／以上