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
  }
}
Simple Generic Class

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;
  }
}
Adding Constraints to Generic

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() {}
}
Base Item

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");
  }
}
Classes Derived from Item

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();
      }
    }
  }
}
Generic Class to Control Item Types

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
Use of Generic Class

Characteristics of a Generic System

  1. 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 and UseItem methods were reused for both Consumable and NonConsumable 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.
  2. 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 and NonConsumableItemController object. consumableItemController  is created using ConsumableItem as the generic type, so it is expecting an object of the same type when calling AddItem  and returning an object of the same type when calling GetItem .
  3. 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.
  4. 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 using ConsumeableItem so AddItem only accepts an object of type ConsumableItem .
    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 for ConsumableItem . This eliminates casting as we will always receive items of provided generic type.
  5. 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!