型パラメーターの制約条件(where T : 〇〇)

もくじ
https://tera1707.com/entry/2022/02/06/144447

やりたいこと

あるとき、ジェネリクスで、

  • T型の戻り値をもつメソッドを引数にとって、
  • 引数で受けたメソッドを実行する。
  • 実行したときに例外なく実行出来たら、そのまま引数で受けたメソッドが返した値を返す
  • もし例外が起きたら、nullを返す

というメソッドを作ろうとした。

具体的には、下記のコードの中のMyMethod1()がそれ。

namespace ConsoleApp2
{
    internal class Program
    {
        enum MyEnum
        {
            Value1,
            Value2,
            Value3,
        }

        static void Main(string[] args)
        {
            MyEnum enm = MyEnum.Value1;

            var a = MyMethod1(() =>
            {
                return enm;
            });
        }

        private static T? MyMethod1<T>(Func<T> action)
        {
            try
            {
                return action.Invoke();
            }
            catch (Exception ex)
            {
                return null;// ←★ここでエラーが出た
            }
        }
    }
}

そうすると、下記のようなエラーが出た。

エラーの中では、'default(T)'を使うように言っているが、 上の例で'MyMethod1()'が呼ばれた場合だと、Tは'MyEnum'になるので、'default(T)'は、MyEnum.Value1になって、nullにはなってくれない。

どうにかしてnullを返す方法はあるか?調べる。

解決方法

「型パラメーターの制約条件」を使う。

具体的には、'MyMethod1()'を下記のように変える。

private static T? MyMethod1<T>(Func<T> action) where T : struct // ←★コレを付ける!
{
    try
    {
        return action.Invoke();
    }
    catch (Exception ex)
    {
        return null;
    }
}

'where T : struct'を付けると、'T'が値型であることになり、エラーが解消される。

なんでエラーが解消されるのか?

以下、私の理解。

まず、enumは、値型である。→参考

値型を、上のMyMethod1()に渡すという前提で、NGなパターン、OKなパターンについて考えてみた。

NGパターン①:戻り値をTにして(戻り値に?はナシ)、制約をつけていない場合

private static T MyMethod1<T>(Func<T> action)

というパターンについて、

制約(where)を付けていないと、MyMethod1<T>()を使う側で、Tには

  • null許容の値型
  • null非許容の値型

のどちらも入れることができる。 →参照

null許容の値型がTに入ってきた場合は、nullを返せるのでreturn nullはおかしくないが、

null非許容の場合は、Tにはnullを入れられないので、上に挙げたエラーになる。

NGパターン②:戻り値をT?にして、制約をつけていない場合

private static T? MyMethod1<T>(Func<T> action)

というパターンについても、パターン①と同じエラーが出る。(Null非許容の値型である可能性があるため・・・)

これが、ずっとなんでエラーになるのかがわからなかったのだが、こちらのページの、

https://ufcpp.net/study/csharp/sp2_nullable.html

「null許容型にできるのは、null許容型を除く値型のみ」「"多重にnull許容"はできない」というのを見て、なるほどと思った。

つまり、Tにnull許容の値型(例えばint?とか)を入れてしまうと、T?int??的な扱い(そんなことは書けないが)になるが、それはC#では不可なので、エラーになる。

と、理解した。(あってるかな...)

OKパターン:戻り値をT?にして、struct(値型)制約をつける場合

「解決方法」のところに書いたやつ。これで、思ったように動く。

private static T? MyMethod1<T>(Func<T> action) where T : struct

Tにstruct制約を付けると、Tは確定で「null非許容の値型」になる。

なので、NGパターン②で挙げたような多重null許容になることはなく、「null許容の値型」を返すことができる。と理解。

これで、今回やりたいことはできた。

思ったこと

「null許容」というものができた経緯は、いろいろ歴史があったようで、そのためか理解するのも個人的に結構難しい。まだ理解しきれてない感じがする。

少しずつ勉強しようと思う。

「型パラメータ制約」のまとめ

下記のMS公式に、制約の種類と意味が載っている。

https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/constraints-on-type-parameters

where T : structは、「構造体」縛りではなくて「値型」で、

where T : classは、「クラス」縛りではなくて「参照型」の様子。

うまく使えば、ジェネリクスで作ったメソッドなどの使用範囲を適切に絞って、変な使い方されてバグる、みたいなのを減らせそう。

参考

C#8.0世代の総称型制約 ジェネリクスとnull許容性の関係

https://qiita.com/muniel/items/daa819c64ca66f152a39

MS公式 型パラメーターの制約 (C# プログラミング ガイド)

https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/constraints-on-type-parameters

値型と参照型

https://ufcpp.net/study/csharp/oo_reference.html

C#ジェネリック メソッドから NULL を返すにはどうすればよいですか?

https://stackoverflow.com/questions/302096/how-can-i-return-null-from-a-generic-method-in-c