【C#】ジェネリック制約(where)の使い方と落とし穴【使用例アリ】

に公開
使い方と落とし穴シリーズ一覧

ジェネリック制約を使用するケース

  • 誤った型の使用をコンパイル時に防ぎたいとき
  • 型パラメータの対象を限定し、関数, クラスの使い方を明確にしたいとき

ジェネリック制約の使い方

制約の一覧
制約 概要
struct 値型 (非 Nullable) のみ許可
class, class? 参照型限定。class? は null 許容、class は nullable, 非nullable 両対応
notnull 値型, 参照型を問わず null 非許容
unmanaged 参照型や固定長配列を含まない純粋な値型に限定
new() 引数なしコンストラクタ必須
<基底クラス> / <基底クラス>? 特定の基底クラス派生に限定
<インターフェース> / <インターフェース>? 特定インターフェース実装 or 特定インターフェース自体を要求
allows ref struct 通常渡せない ref struct を許可
System.Enum 列挙型限定
System.Delegate デリゲート型限定

基本

//例1 (型引数が指定基底クラス、またはその派生クラスであることが必要)
public class Test<T> where T : <基底クラス>

//例2 (型引数が指定インターフェースを実装 or 指定インターフェース自体であることが必要)
public void Test<T>() where T : <インターフェース>

//複数の制約を適用することも可能
where T : class, new()
  • 制約に対し、不適切なものを適用するとエラーが発生 (CS0311)
//宣言部分
public class Test
{
    public void TestFunc<T>() where T : BaseClass {}
}

//使用部分
TestFunc<Some>(); //Some クラスが BaseClass を継承していない場合はエラー
  • 以降、比較的私の利用頻度が高いものを取り上げて解説する

new() 制約

  • 引数なしの public なコンストラクターを持つもののみ受け付ける
//宣言部分
public class Factory<T> where T : new()
{
    public T CreateInstance()
    {
        return new T();
    }
}

class Some
{
    public string Name { get; set; } = "default";
}

//実行部分
var factory = new Factory<Some>();
Some s = factory.CreateInstance();
Console.WriteLine(s.Name); //"default"

notnull 制約

  • null 参照を防止
//宣言部分
public class NonNullDictionary<TKey, TValue> where TKey : notnull
{
    private readonly Dictionary<TKey, TValue> _dict = new Dictionary<TKey, TValue>();

    public void Add(TKey key, TValue value)
    {
        // TKey が notnull なので key に null は渡せない
        _dict.Add(key, value);
    }

    public bool TryGetValue(TKey key, out TValue value)
    {
        return _dict.TryGetValue(key, out value);
    }
}

//実行部分
var safeDict = new NonNullDictionary<string, int>();
safeDict.Add("apple", 10);
safeDict.Add(null, 5); //コンパイルエラー (null を許容しない)

Unityでの使用例

  • Unity未経験の方でもわかる内容にしている
/// <param name="configure">フィルターの設定処理注入用</param>
public void ApplyFilter<T>(Action<T> configure) where T : Behaviour
{
    if (filterDict.TryGetValue(typeof(T), out var component) == false)
    {
        component = listener.gameObject.AddComponent<T>();
        filterDict[typeof(T)] = component;
    }

    var filter = component as T;
    filter.enabled = true;
    configure?.Invoke(filter);
}
  • ApplyFilterAudioListener にフィルター(エフェクト)を適用する関数

    • 内部のAddComponent<T>()は、TComponentBehaviourの派生型の必要がある
    • そのため、TBehaviour 継承クラスのみに制限(BehaviorComponent を継承)
    • 結果、不適切な型の使用をコンパイル時に防げる
  • ApplyFilter使用例(リバーブ度合いの変更)

ApplyFilter<AudioReverbFilter>(filter => filter.reverbLevel = Mathf.Clamp(reverbLevel, -100f, 20f));

オイラはこんな落とし穴に出会った

アクセシビリティに一貫性が~

  • アクセシビリティ一貫性云々の文言を見たことがある人は多いと思う (CS0052)
  • ジェネリック制約にも同じようなエラーがある (CS0703)
  • パラメータ側のアクセシビリティを制約型のそれと同じか、高くするとよい
private class BaseClass {}

public class Test
{
    //CS0703
    public void TestFunc<T>() where T : BaseClass {}
}

new() 制約の順序

  • 制約リストの最後に書く必要がある
//エラー (CS0627)
public class BadFactory<T> where T : new(), IDisposable
{
    public T Create() => new T();
}

structNullable<T> の混同

  • struct 制約は型パラメータを「null 非許容値型」に制限する
  • しかし、int? は Nullable<int> 型なのでエラーが発生する
  • Nullable 値型を渡したい場合、制約なしのオーバーロードを用意する
//宣言部分
public class ValueHolder<T> where T : struct
{
    public T Value { get; set; }
}

//実行部分
var holderInt      = new ValueHolder<int>();   //OK
var holderNullable = new ValueHolder<int?>();  //エラー (CS0453)

参考

Discussion