【C#】ジェネリックの使い方と落とし穴【コピペ可】

に公開
使い方と落とし穴シリーズ一覧
サッサと試したい人向けコピペ可コード
using System;
using System.Collections.Generic;

/*************************
宣言部分
*************************/
//ジェネリッククラス定義
public class ListWrapper<T>
{
    private readonly List<T> _items = new();

    public void Add(T item) => _items.Add(item);
    public T Find(Predicate<T> match) => _items.Find(match);
}

//ジェネリックインターフェース定義
public interface ISerializer<T>
{
    string Serialize(T obj);
    T Deserialize(string data);
}

//インターフェース実装例
public class JSONSerializer : ISerializer<User>
{
    public string Serialize(User obj) => $"{{\"Name\":\"{obj.Name}\",\"Age\":{obj.Age}}}";
    public User Deserialize(string data) => new User { Name = "Deserialized", Age = 99 };
}

public class User
{
    public string Name;
    public int Age;
}

public class Character
{
    public string Name = "DefaultCharacter";
}

/*************************
実行部分
*************************/
public static class Program
{
    public static void Main()
    {
        /*************************
        ジェネリック関数の使用例
        *************************/
        void Swap<T>(ref T a, ref T b)
        {
            T temp = a;
            a = b;
            b = temp;
        }

        int    x_i = 1,       y_i = 2;
        float  x_f = 1.1f,    y_f = 2.2f;
        string x_s = "Hello", y_s = "World";
        Swap(ref x_i, ref y_i);
        Swap(ref x_f, ref y_f);
        Swap(ref x_s, ref y_s);
        Console.WriteLine($"x_i = {x_i}, y_i = {y_i}"); // x_i = 2,     y_i = 1
        Console.WriteLine($"x_f = {x_f}, y_f = {y_f}"); // x_f = 2.2f,  y_f = 1.1f
        Console.WriteLine($"x_s = {x_s}, y_s = {y_s}"); // x_s = World, y_s = Hello

        /*************************
        ジェネリッククラスの使用例
        *************************/
        var intList = new ListWrapper<int>();
        intList.Add(10);
        Console.WriteLine($"Find(10): {intList.Find(x => x == 10)}");

        /*************************
        制約の使用例
        *************************/
        //Characterクラス自体 or その継承クラスのみ受け付ける制約
        void PrintCharacter<T>(T c) where T : Character
        {
            Console.WriteLine($"Character Name: {c.Name}");
        }
        PrintCharacter(new Character());
        //PrintCharacter(new ListWrapper<int>()); //CS0311

        //制約なしで new T() は使えない
        //T obj = new T(); //CS0304

        //制約付き new() の例
        T CreateInstance<T>() where T : new()
        {
            return new T();
        }
        var user2 = CreateInstance<User>();
        Console.WriteLine($"Created by new(): {user2.Name}, {user2.Age}");

        /*************************
        共変性, 反変性, 不変の例
        *************************/
        //共変性
        //サブ型 → 親型
        //List<string> は IEnumerable<string> を実装しているため
        //IEnumerable<string> → IEnumerable<object> (共変)として代入可能
        IEnumerable<object> objs = new List<string> { "A", "B" };

        //反変性
        //親型 → サブ型
        //IComparer<in T> により、親型(object)でも文字列(string)同士の比較に適用できる
        //実際は Comparer<object> が object 引数を受け取るため、
        //string も object として比較可能
        IComparer<string> cmp = Comparer<object>.Default;

        //不変 (エラー)
        //List<object> wrong = new List<string>(); //CS0029

        /*************************
        default(T) の null 判定
        *************************/
        //エラー有
        // bool IsNullDefault<T>(T value)
        //{
        //    return value == default(T); //CS0019
        //}

        //エラー無
        //EqualityComparer…の代わりに制約(where : class)もアリ(参照型以外使えなくなるが)
        bool IsNullDefaultFixed<T>(T value)
        {
            return EqualityComparer<T>.Default.Equals(value, default(T));
        }
        Console.WriteLine($"IsNullDefaultFixed<int>(0): {IsNullDefaultFixed(0)}");
        Console.WriteLine($"IsNullDefaultFixed<string>(null): {IsNullDefaultFixed<string>(null)}");
    }
}

一言

  • Unity ユーザーが初めてジェネリックに触れるのは GetComponent<T>() だと思う
  • 私が C# を扱うときは専ら Unity なので他の C#er はどうなんでしょうかね

ジェネリックとは

  • 型をパラメーター化して、クラスやメソッドの定義を抽象化できる仕組み
    • 型引数(T, TKey, TValue など)を指定するまで中身が確定しない
    • 「後から好きな型を当てはめるテンプレ」のようなイメージ

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

  • 型安全なコレクションが欲しい
    • List<T> のように、要素を取り出すときにキャスト不要
  • 再利用性を高めたい
    • 同じロジックの複数型での記述を回避でき、コード量を削減
  • ボクシング, アンボクシングを避けたい

ジェネリックの使い方

クラス定義

  • インスタンス生成時に型を指定し、それに対応したクラスが返る
//定義部分
public class ListWrapper<T>
{
    public void Add(T item) => _items.Add(item);
    public T Find(Predicate<T> match) => _items.Find(match);

    private readonly List<T> _items = new List<T>();
}

//使用部分
var intL = new ListWrapper<int>();
intL.Add(42);
var found = intL.Find(x => x == 42);

関数定義

//定義部分
public static void Swap<T>(ref T a, ref T b)
{
    T temp = a;
    a = b;
    b = temp;
}

//使用部分
int    x_i = 1,       y_i = 2;
float  x_f = 1.0f,    y_f = 2.0f;
string x_s = "Hello", y_s = "World";
Swap(ref x_i, ref y_i); //x_i = 2,       y_i = 1
Swap(ref x_f, ref y_f); //x_f = 2.0f,    y_f = 1.0f
Swap(ref x_s, ref y_s); //x_s = "World", y_s = "Hello"

インターフェース定義

//宣言部分
public interface ISerializer<T>
{
    public string Serialize(T obj);
    public T Deserialize(string data);
}

//使用部分
public class JSONSerializer : ISerializer<User>
{
    public string Serialize(User obj) {...}
    public User Deserialize(string data) {...}
}

ジェネリック制約

//Character クラス自体、または、Character 継承クラスのみ T に受け付ける
public void CalcHP<T>() where T : Character {...}

共変性, 反変性 (out, in)

  • 定義側で使用する (関数等の利用側でこれを用いることはない)
    • インターフェース, デリゲートのみで利用可能なキーワード
  • 共変性(out
    • 出力専用 な型パラメータに付ける
    • サブ型から親型への暗黙的な変換が可能
    • 例:読み取り専用シーケンス IEnumerable<out T>
  • 反変性(in
    • 入力専用 な型パラメータに付ける
    • 親型からサブ型への暗黙的な変換が可能
    • 例:比較器 IComparer<in T>
  • 不変
    • 何も付けない
    • 親型 ⇄ サブ型の変換不可
    • 例:List<T>List<string>List<object> 相互代入不可
//定義部分
//out (共変性)
public interface IAssetProvider<out T>
{
    T GetAsset();
}

//in (反変性)
public interface IProcessor<in T>
{
    void Process(T input);
}

//使用部分
//共変性 (out) サブ型 → 親型
//Object 型で受け取っても Enemy を安全に読み出せる (書き換えは不可)
IAssetProvider<Object> asset = new EnemyProvider(); //EnemyProvider : IAssetProvider<Enemy>

//反変性 (in) 親型 → サブ型
//Object を処理する Processor を、Enemy 用として使える(T は引数専用なので安全)
IProcessor<Enemy> processor = new CharaProcessor(); //CharaProcessor : IProcessor<Object>

//不変(エラー)
//List<Object> wrong = new List<Enemy>(); //CS0029 暗黙的な変換ができない

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

default(T)null 判定

  • default(T) 戻り値を null 判定する場合、T が値型のときに注意
public bool IsNullDefault<T>(T value)
{
    //CS0019  == 演算子は、Tが == を定義していないと使用できない
    return value == default(T);
}

回避策

  • 制約で参照型専用にする (public bool IsNullDefault<T>(T value) where T : class)
  • 値型でも比較したい場合、EqualityComparer<T>.Default.Equals を使用
bool IsNullDefault<T>(T value)
{
    return EqualityComparer<T>.Default.Equals(value, default(T));
}

制約がないとできない操作をやろうとしてエラー

  • 例:Tnew (CS0304)
public void InitializeList<T>()
{
    //CS0304 new()制約がない
    T obj = new T();
}

回避策

  • 制約を適宜追加 (例の場合 public void InitializeList<T>() where T : new())
  • 上記例において、デフォルト値を取りたいだけなら default(T) でok
    • 今度は null の可能性を考える必要が出てくるけども。
public void InitializeList<T>()
{
    T? obj = default(T);
}

参考

Discussion