ジェネリクス関数の制約とnullable

角田 智活 0 評価のポイント
2025-02-19T03:34:13.1+00:00

public static List<T> MyNonNull<T>(this IEnumerable<T?> list) where T :notnull=>[.. list.OfType<T>()];

上記の、nullableのlistをnon nullableなlistに変換する関数を作りましたが、コンパイラがT制約をうまく解決できないようです

nullableList.MyNonNull(); だとCS8714「Tのnull許容性がnotnull制約と一致しません」となり、

nullableList.MyNonNul<対象の型>(); だとCS1061「定義が含まれておらず~」となります

public static List<T> MyNonNull<T>(this IEnumerable<T?> list) where T :struct=>[.. list.OfType<T>()];

public static List<T> MyNonNull<T>(this IEnumerable<T?> list) where T :class=>[.. list.OfType<T>()];

上記2つに分けることで正しく動作しますが、ここでstruct制約とclass制約がなぜnotnull制約にまとめられないのか疑問に思いました

notnullにまとめられるように改善を提案したいです

それとも、最初の関数のよりよい書き方がありますでしょうか?

(分かりにくいかもなのであとで追記します)

C#
C#
C 言語ファミリをルーツとし、コンポーネント指向プログラミングのサポートを含む、オブジェクト指向およびタイプセーフのプログラミング言語。
41 件の質問
{count} 件の投票

承認済みの回答
  1. gekka 11,056 評価のポイント MVP
    2025-02-20T10:56:25.2666667+00:00

    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等修正


2 件の追加の回答

並べ替え方法: 最も役に立つ
  1. motosan 0 評価のポイント
    2025-02-20T00:57:41.6633333+00:00

    テストしてみましたがビルドでエラーは発生しませんでした

    ※ 型が良くわかりませんが int の場合、角田 智活 さんのコメントにあるとようなエラーになりました。

    error CS0029: 型 'System.Collections.Generic.List<int?>' を 'System.Collections.Generic.List<int>' に暗黙的に変換できません
    
    warning CS8714: 型 'int?' を、ジェネリック型または メソッド 'A.MyNonNull<T>(IEnumerable<T?>)' 内で型パラメーター 'T' として使用することはできません。型引数 'int?' の Null 許容性が 'notnull'  制約と一致しません。
    

    環境

    Visual Studio Community 2022(17.13.0)

    テストコード

    ※ nullableList を追加しました。

    ※ int の場合、少し冗長ですが下記の MyNonNull2 のようにするとエラーになりません。

    ※ コードが変わっていなかったので修正しました、

    ※ KeyValuePair を追加しました。

    List<string?> nullableList = new ();
    List<string> list= nullableList.MyNonNull<string>();
    List<int?> nullableList2 = new ();
    List<int> list2 = nullableList2.MyNonNull2<int, int?>();
    List<KeyValuePair<string,string>?> nullableList3 = new();
    var list3 = nullableList3.MyNonNull2<KeyValuePair<string,string>,KeyValuePair<string,string>?>();
    Console.WriteLine($"nullableList={nullableList.GetType()}");
    Console.WriteLine($"list={list.GetType()}");
    Console.WriteLine($"nullableList2={nullableList2.GetType()}");
    Console.WriteLine($"list2={list2.GetType()}");
    Console.WriteLine($"nullableList3={nullableList3.GetType()}");
    Console.WriteLine($"list3={list3.GetType()}");
    
    
    static class A
    {
        public static List<T> MyNonNull<T>(this IEnumerable<T?> list) where T :notnull=>[.. list.OfType<T>()];
        public static List<T1> MyNonNull2<T1,T2>(this IEnumerable<T2> list) where T1 :notnull=>[.. list.OfType<T1>()];
    }
    

    ビルドエラーの確認用に作成しました。

    ビルド結果です。

    12:25 で再構築が開始されました... 
    1>------ すべてのリビルド開始: プロジェクト:ConsoleSample2, 構成: Debug Any CPU ------ C:\Users\xxxxxxxx\source\repos\net 9\ConsoleSample2\ConsoleSample2\ConsoleSample2.csproj を復元しました (4 ミリ秒)。 
    1>ConsoleSample2 -> C:\Users\motoyama\source\repos\net 9\ConsoleSample2\ConsoleSample2\bin\Debug\net9.0\ConsoleSample2.dll ========== すべて再構築: 1 正常終了、0 失敗、0 スキップ ========== =========== リビルド は 12:25 で完了し、01.214 秒 掛かりました ==========
    
    

    実行結果です。

    nullableList=System.Collections.Generic.List`1[System.String]
    list=System.Collections.Generic.List`1[System.String]
    
    nullableList2=System.Collections.Generic.List`1[System.Nullable`1[System.Int32]]
    list2=System.Collections.Generic.List`1[System.Int32]
    
    nullableList3=System.Collections.Generic.List`1[System.Nullable`1[System.Collections.Generic.KeyValuePair`2[System.String,System.String]]]
    list3=System.Collections.Generic.List`1[System.Collections.Generic.KeyValuePair`2[System.String,System.String]]
    

  2. SurferOnWww 3,796 評価のポイント
    2025-02-20T07:54:51.5566667+00:00

    ここでstruct制約とclass制約がなぜnotnull制約にまとめられないのか疑問に思いました

    質問者さんの notnull を使って「まとめる」という考え方がどういう事なのかよくわかりませんが・・・

    今回の問題は、値型(KeyValuePair も値型)でなくて null 許容型(System.Nullable<T>)を使ったことが「notnull制約」の制約に引っかかって下の画像のような警告が出たということに過ぎないと思いますが?

    enter image description here

    上の画像の MyNonNull2 メソッドのように「notnull制約」を外せば警告は出なくなります。これで不都合はありますか?

    参考:

    where (ジェネリック型制約) (C# リファレンス)

    "where 句には、notnull 制約を含めることができます。 notnull 制約では、型パラメーターを null 非許容型に制限します。 型には、値の型または null 非許容参照型を指定できます"


お客様の回答

回答は、質問作成者が [承諾された回答] としてマークできます。これは、ユーザーが回答が作成者の問題を解決したことを知るのに役立ちます。