Generics is the concept of generalising types of objects in their class and method. It can be thought of as a variable for type. The types of generics are generalised until a class or method is declared. A generic class can be used to create a code pattern that can support multiple object types.
Creating Generics
Generics can be created by adding <T>
after the class name. Here, T
is the generic type that gets set while calling it. A class can have multiple generic types that can be added by separating them with commas as <T,U,V>
. The general naming convention for creating generic types starts from T and moves alphabetically as T, U, V, etc.
// generic class
public class MyGenericClass < T > {
public Type GetMyType() {
return typeof (T);
}
}
// class to use create generic class and call it's method
public static class GenericClassTest {
public static void CreateObject() {
MyGenericClass < int > genericClass = new MyGenericClass < int > ();
Debug.Log(genericClass.GetMyType());
// Output: System.Int32
}
}
This class has the GetMyType
method that simply returns the type of generic class. A generic class is not very useful as the type T
can be anything. To use T
, we can limit its type.
Constraint on Generic Type
A constraint limits the generic type, which allows us to use more properties of generic types. A constraint can be given to a generic type by using the where
keyword.
Let's create a constraint for T
in MyGenericClass
. We will create its constraint as Monobehaviour
. This limits the value of T
as MonoBehaviour
or it's child class. This allows the use of MonoBehaviour
's properties.
public class MyGenericClass < T > where T: MonoBehaviour {
public T myMonoBehaviourObject; // creating object of type t i.e MonoBehaviour
public Vector3 GetMyPosition() {
// adding constraint allows us use of properties
return myMonoBehaviourObject.transform.position;
}
}
Using Generics with Inheritance
We will be creating an inventory system to handle consumable and non-consumable items for our demonstration.
Let us first create a base class Item
. It will contain properties itemName
, itemPrice
, and ownedQuantity
. This will contain two public methods, GetOwnedQuantity
and PurchaseItem
. A virtual method Use
will be used to consume the item. This method will be handled separately from the child classes.
public class Item {
public string itemName;
public float itemPrice;
protected int ownedQuantity;
public int GetOwnedQuantity() {
return ownedQuantity;
}
public void PurchaseItem() {
Debug.Log($"Item {itemName} is purchased with {itemPrice} price");
ownedQuantity++;
}
public virtual void Use() {}
}
Let's create two child classes deriving from the Item
base class. Both will have different constructors. Also, they both will override the Use
method separately. The ConsumableItem
class will decrease owned items by 1 when used.
To further differentiate the two classes, let's also add UnEquip
method to NonConsumableItem
class.
// consumeable item class. Owned quantity must be decreased when used
public class ConsumableItem: Item {
public ConsumableItem(string name, float price, int currentlyOwnedQuantity = 0) {
itemName = name;
itemPrice = price;
ownedQuantity = currentlyOwnedQuantity;
}
public override void Use() {
ownedQuantity--;
Debug.Log($"Item {itemName} is used. Now remaining {GetOwnedQuantity()}");
}
}
// Non consumable class. Item should be allowed to unequip
public class NonConsumableItem: Item {
public NonConsumableItem(string name, float price) {
itemName = name;
itemPrice = price;
}
public override void Use() {
Debug.Log($"Item {itemName} is used");
}
public void UnEquip() {
Debug.Log($"Item {itemName} is unequipped");
}
}
Now that we have the required base classes, we will be creating a controller class for them. We will use a generic system to make a controller.
Let's create a class ItemClass
that has the generic type T
. The type T
will be limited to Item
class so that we can use the properties of the Item
class. In this class, create a list of type T
so that items can be stored.
Note that List
is also a generic class, so we can pass T
as a type. Now, we create the methods AddItem
to add inventory items, GetItem
to get items from the name and UseItem
to use items with the name.
public class ItemController < T > where T: Item {// T is generic type constrained to item
private List < T > itemCollection = new List < T > (); // items are stored here.
// add item to list
public void AddItem(T item) {
itemCollection.Add(item);
}
// searches item in list with given name and returns it
public T GetItem(string itemName) {
foreach(T item in itemCollection) {
if (item.itemName == itemName) {
return item;
}
}
return null;
}
// searches item with given name in list and uses it
public void UseItem(string itemName) {
foreach(T item in itemCollection) {
if (item.itemName == itemName) {
item.Use();
}
}
}
}
Now to demonstrate its use, let's create a Demo
class with CreateInventory
method. This method will create a controller, add items, and use the item.
At first, create two controllers consumableItemController
and nonConsumableItemController
for handling ConsumableItem
and NonConsumable
item by passing them as a generic type. Now, add items to them using the AddItem
method. Note that only items of defined generic type while creating a controller can be added.
Added items can be used by calling the ItemController.Use(string)
method. Also, try GetItem
using the ItemCOntroller.GetItem(string)
method. This will always return the defined generic type for the controller.
public class Demo {
private void CreateInventory() {
//creating controllers
ItemController < ConsumableItem > consumableItemsController = new ItemController < ConsumableItem > ();
ItemController < NonConsumableItem > nonConsumableItemsController = new ItemController < NonConsumableItem > ();
//adding consumable items
consumableItemsController.AddItem(new ConsumableItem("Apple", 50, 6));
consumableItemsController.AddItem(new ConsumableItem("Bandages", 60, 3));
consumableItemsController.AddItem(new ConsumableItem("Repair dust", 80));
consumableItemsController.AddItem(new ConsumableItem("Health potion", 500, 4));
//adding non consumable items
nonConsumableItemsController.AddItem(new NonConsumableItem("Black sword", 1500));
nonConsumableItemsController.AddItem(new NonConsumableItem("Knight's helm", 2700));
nonConsumableItemsController.AddItem(new NonConsumableItem("Turtle armor", 3500));
nonConsumableItemsController.AddItem(new NonConsumableItem("Sprint boots", 1000));
nonConsumableItemsController.AddItem(new NonConsumableItem("Jester mask", 200));
//using consumable items
consumableItemsController.UseItem("Apple");
consumableItemsController.UseItem("Health potion");
//using non-consumable item
nonConsumableItemsController.UseItem("Black sword");
// when getting item we receive type of defined generic type so no need to cast
NonConsumableItem blackSword = nonConsumableItemsController.GetItem("Black sword");
blackSword.UnEquip(); // using property exclusive to child class type
}
}
// ** output**
// Item Apple is used. Now remaining 5
// Item Health potion is used. Now remaining 3
// Item Black sword is used
// Item Black sword is unequipped
Characteristics of a Generic System
- A generic class can be used as a template for code. It can make a block of code reusable. In the above
ItemController
class,AddItem
,GetItem
andUseItem
methods were reused for bothConsumable
andNonConsumable
classes. This generalisation makes modification easier as only the generic class needs to be modified. However, this should be used in moderation, as using it too much will make it harder to maintain. - Generic classes are type-safe. Once a generic class is defined with a type, its type cannot be changed. This can be seen in the example above when we created the
consumableItemController
andNonConsumableItemController
object.consumableItemController
is created usingConsumableItem
as the generic type, so it is expecting an object of the same type when callingAddItem
and returning an object of the same type when callingGetItem
. - Type check is done at compile time. This lets us find issues when compiling rather than finding issues during runtime. Fixing issues during compiling is easier than during the runtime.
- Strong type checking is used in a generic system. Once an object is created with a type, even its parent class cannot be used instead of the object.
Let us consider a snippet from the above example,
consumableItemsController.AddItem(new ConsumableItem("Apple",50,6));
Here,consumableItemsController
was created usingConsumeableItem
soAddItem
only accepts an object of typeConsumableItem
.
The following line gives the compile-time error,
consumableItemsController.AddItem(new Item());
This is because strong type checking is done.Item
is not accepted even if it is the base class forConsumableItem
. This eliminates casting as we will always receive items of provided generic type. - Generics are more efficient as it removes the need for boxing, unboxing and casting objects.
Using generic systems, re-usable code can be written that can be easily maintained. I hope this blog helps you implement a generic system in your system.
Thank you for reading!