⛳
【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) {...}
}
ジェネリック制約
- 型パラメータ(
T
)の対象を限定し、関数, クラスの使い方を明確にできる - 詳細:ジェネリック制約(where)の使い方と落とし穴
//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));
}
制約がないとできない操作をやろうとしてエラー
- 例:
T
のnew
(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);
}
参考
- Microsoft - Generic classes and methods
- Microsoft - Covariance and Contravariance (C#)
- Microsoft - in (Generic Modifier) (C# Reference)
- Microsoft - out (generic modifier) (C# Reference)
- Microsoft - where (generic type constraint) (C# Reference)
- Microsoft - Compiler Error CS0019
- Microsoft - Compiler Error CS0029
- Microsoft - Compiler Error CS0304
- ++C++ - ジェネリクスの共変性・反変性
Discussion