ジェネリックとは?

ジェネリック(Generics)とは、プログラミング言語において重要な概念で、型に依存しない汎用的なコードを記述するための仕組みです。

ジェネリックを使いこなすことで、コードの再利用性が増し、型安全性が確保されます。

ジェネリックは、クラスやインターフェイス、メソッドに対して使用できます。

ジェネリックの制約

ジェネリック型に対して、特定の方やメンバーが使用可能出ることを保証したい場合、

制約を設けることが出来ます。

値型(構造体)が必要な場合

where T : struct を指定します。

  • Tは値の型(int, float, doubleなど)でなければなりません。
  • 値型にはnullを割り当てることが出来ないため、Tがnullになることはありません。
public class Constraint1<T> where T : struct
{
	public T Default()
	{
		// Tで指定された値型のデフォルト値を返す
		return default(T);
	}
}

classが必要な場合

where T : classを指定します。

  • Tは参照型(string, List<>やクラスなど)でなければなりません。
  • 参照型はnullを許容するので、Tがnullになることがあります。
public class Constraint2<T> where T : class
{
	public T Value { get; set; }
	
	public void Print()
	{
		Debug.Log(Value != null ? Value, "Null");
	}
}

特定の型、または基底クラスが必要な場合

where T : BaseClass を指定します。BaseClassには特定のクラス名を記述します。

  • Tは指定されたクラスを継承しているか、同じクラスでなければなりません。
  • これによる基底クラスのメンバーにアクセス出来るようになります。
public abstract class ItemBase
{
	public abstract bool IsConsumable { get; }
}

public class EquipItem : ItemBase
{
	public override bool IsConsumable => false;
}

public class MagicItem : ItemBase
{
	public override bool IsConsumable => true;
}

public class Constraint3<T> where T : ItemBase
{
	public void PrintItem(T item)
	{
		Debug.Log($"Consumable : {item.IsConsumable}.");
	}
}

インターフェイスが必要な場合

where T : IInterfaceを指定します。IInterfaceには特定のインターフェイス名を記述します。

  • Tは指定されたインターフェイスを継承していなければなりません。
  • これによりインターフェイスのメンバーを使用出来ることが保証されます。
public abstract class ItemBase : IItem
{
	public abstract bool IsConsumable { get; }
}

public interface IItem
{
	bool IsConsumable { get; }
}

public class EquipItem : ItemBase
{
	public override bool IsConsumable => false;
}

public class MagicItem : ItemBase
{
	public override bool IsConsumable => true;
}

public class Constraint4<T> where T : IItem
{
	public void PrintItem(T item)
	{
		// TはIItemインターフェイスのメンバーにしかアクセス出来ない
		Debug.Log($"Consumable : {item.IsConsumable}.");
	}
}

コンストラクタが必要な場合

where T : new()を指定します。

  • Tには、引数のないpublicなコンストラクタが必要です。
  • この制約により、ジェネリック型のインスタンスを作成できます。
public class Item()
{
	public Item()
	{
	}
}

public class ItemGenerator<T> where T : new()
{
	public T CreateItem()
	{
		return new Item();
	}
}

複数の制約を組み合わせる

制約を組み合わせて使用することも出来ます。

下記の例では、Tは以下の条件を満たしている必要があります。

  • ItemBaseを継承している。
  • IItemインターフェイスを実装している。
  • 引数のないpublicコンストラクタを持っている。
public class ItemGenerator<T>
	where T : ItemBase, IItem, new()
{
	public T CreateItem()
	{
		return new T();
	}
}

public interface IItem
{
	bool IsConsumable { get; }
}

public abstract class ItemBase : IItem
{
	public abstract bool IsConsumable { get; }
}

public class EquipItem : ItemBase
{
	public override bool IsConsumable => false;
}

制約を指定するメリット

ちょっとわかりにくい制約ですが以下ようなメリットがあります。

  • 型安全性
    • 指定した型が持つべき特性を保証できる
  • 柔軟性
    • ジェネリック型を使用する範囲を制限ながら、特定の動作を強制できる
  • コードを簡潔に記述できる
    • 明確な制約により、より直感的になる

まとめ

前回に続いて、ジェネリックを扱う際の制約について解説しました。

慣れないとなかなか難しいかもしれませんが、

ジェネリック制約を理解することで、コードの再利用性や型安全性が飛躍的に向上します。

特に、複雑なデータ構造や柔軟な設計が求められる場面では、ジェネリックとその制約を適切に活用することが大きな力となります。

機会があったら活用してみてください!

🔗関連ページ