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 unequippedCharacteristics 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
ItemControllerclass,AddItem,GetItemandUseItemmethods were reused for bothConsumableandNonConsumableclasses. 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
consumableItemControllerandNonConsumableItemControllerobject.consumableItemControlleris created usingConsumableItemas the generic type, so it is expecting an object of the same type when callingAddItemand 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,consumableItemsControllerwas created usingConsumeableItemsoAddItemonly 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.Itemis 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!