ゲームを作っていくと型だけ違って、内部的には同じ挙動をする関数を複数作った経験はありませんか?別にそれでもいいですが、ただそれをやるとプログラムの保守性が悪くなり、仕様変更があった場合にそのすべての関数に対して同じ修正をしないといけなくなってしまいます。そのようなことを防ぐには「ジェネリック関数」という仕組みがあります。今回はそのジェネリック関数について紹介したいと思います。
ジェネリック関数とは
ジェネリック関数とは結論からいうとつまり、同じ挙動の関数を一つだけ作ってしまえば、後は渡す型だけ変更して使い回せる関数のことです。
言葉だけだとちょっと分かりづらい所はあるかと思いますので使い方を見てみましょう。
ジェネリック関数の使い方
例えば「〇〇武器が△個持っています!」、「〇〇アイテムが△個持っています!」といったようなケースを考えてみましょう。
それぞれ武器情報を扱うクラスWeaponInfoとアイテム情報を扱うクラスItemInfoがあるとします。
// 武器の種類
public enum e_WeaponType
{
none,
bow,
sword,
}
// アイテムの種類
public enum e_ItemType
{
none,
key,
magnet,
}
// 武器の情報を扱うクラス
public class WeaponInfo : IEquipObjectInfo
{
private e_WeaponType weapontype;
private int weaponCount;
// コンストラクタ
public WeaponInfo(e_WeaponType type, int count)
{
weapontype = type;
weaponCount = count;
}
// 武器名を返却する
public string GetName()
{
return weapontype.ToString();
}
// 所持数を返却する
public int GetCount()
{
return weaponCount;
}
}
// アイテムの情報を扱うクラス
public class ItemInfo : IEquipObjectInfo
{
e_ItemType itemtype;
int itemCount;
// コンストラクタ
public ItemInfo(e_ItemType type, int count)
{
itemtype = type;
itemCount = count;
}
// アイテム名を返却する
public string GetName()
{
return itemtype.ToString();
}
// 所持数を返却する
public int GetCount()
{
return itemCount;
}
}
// 装備中のオブジェクトの情報を持つインターフェース
public interface IEquipObjectInfo
{
string GetName();// 名前の取得
int GetCount();// 所持数の取得
}
ジェネリックを使わないやり方
ジェネリック関数を使わないでやると下記のようにWeaponInfo型とItemInfo型と、それぞれの型に対応した関数を作らないといけないです。
それはそれでいいですが、しかし内部の処理は同じなので万が一仕様変更があったり、実装ミスがあった場合、それぞれの関数を修正しないといけないのでコストが掛かる上、保守性が悪いです。
public class NoGenericFunctionTest : MonoBehaviour
{
private void Start()
{
WeaponInfo weapon = new WeaponInfo(e_WeaponType.bow, 1);
ShowEquipWeaponInfo(weapon);// 出力結果:bowが1個持っています!
ItemInfo item = new ItemInfo(e_ItemType.key, 10);
ShowEquipItemInfo(item);// 出力結果:keyが10個持っています!
}
// 装備中の武器情報をログに出力
void ShowEquipWeaponInfo(WeaponInfo weapon)
{
Debug.Log(string.Format("{0}が{1}個持っています!", weapon.GetName(), weapon.GetCount()));
}
// 装備中のアイテム情報をログに出力
void ShowEquipItemInfo(ItemInfo item)
{
Debug.Log(string.Format("{0}が{1}個持っています!", item.GetName(), item.GetCount()));
}
}
ジェネリックを使うやり方
ジェネリックを使うと下記のようにできます。
public class GenericFunctionTest : MonoBehaviour
{
private void Start()
{
WeaponInfo weapon = new WeaponInfo(e_WeaponType.bow, 1);
ShowEquipObjInfo<WeaponInfo>(weapon);// 出力結果:bowが1個持っています!
ItemInfo item = new ItemInfo(e_ItemType.key, 10);
ShowEquipObjInfo<ItemInfo>(item);// 出力結果:keyが10個持っています!
}
/// <summary>
/// ジェネリック関数
/// TはWeaponInfo型やItemInfo型を指定する
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="equipObj"></param>
void ShowEquipObjInfo<T>(T equipObj) where T : IEquipObjectInfo
{
Debug.Log(string.Format("{0}が{1}個持っています!", equipObj.GetName(), equipObj.GetCount()));
}
}
このように上記のジェネリック関数を作っておくと下記のように呼び出して使うことができます。
WeaponInfo weapon = new WeaponInfo(e_WeaponType.bow, 1);
ShowEquipObjInfo<WeaponInfo>(weapon);// 出力結果:bowが1個持っています!
ItemInfo item = new ItemInfo(e_ItemType.key, 10);
ShowEquipObjInfo<ItemInfo>(item);// 出力結果:keyが10個持っています!
どうでしょうか?異なる型で内部が同じ処理をする場合はジェネリック関数を使うとシンプルに纏められます。
ジェネリック関数の型制約(where)
型さえ渡してしまえば、同じ内容の処理ができるのはジェネリック関数のメリットだとお伝えしました。しかし何でもかんでも指定していいというわけではありません。例えば上記の例でGameObject型を渡すとコンパイルエラーになります。なぜでしょうか?
ジェネリック関数で受け取る型(T)は予め、どんな型が渡されるかを制限することで、その型で使える機能が保証されます。
WeaponInfo型とItemInfo型はともにIEquipObjectInfoインターフェースを継承しています。インターフェースで定義された関数は必ず継承先で実装しないといけない特性があります。
where T : IEquipObjectInfoで限定することで、渡ってくる型は必ず下記の関数が実装されていることが保証されます。
string GetName();// 名前の取得
int GetCount();// 所持数の取得
同じ機能が使えるからこそ、型が違っても同じ処理を書けるわけです。
インターフェースについて下記の記事で詳しく解説しています。
まとめ
- 型が違うが同じ処理をする場合はジェネリック関数を使うと保守性が上がります。
- ジェネリック関数を使う場合はwhereで型の制限をすると使える機能が保証されます。
最初はちょっと難しいと感じるところはあるかもしれないですが便利なので是非使ってみてください!
コメント