Introduction


In C#, Generics allow you to write code that doesn’t care about specific data types until the very last moment. Instead of writing separate code for int, string, or Vector2D, you write one blueprint using a placeholder: <T>.

The Core Purpose: “Write Once, Use Everywhere”

Why do we need Generics?

Imagine you are building a Storage Box for your game.

  • If you want to store Integers, you build an IntBox.
  • If you want to store Strings, you build a StringBox.
  • If you want to store Vector2Ds, you build a VectorBox.

Manual Way (Generics Class) Imagine you are building your game , and you need a way to store data safely. Without Generics, you have to build a new class for every single type of data.

// 1. Box for Integers
public class IntBox {
    public int Item;
    public void Display() { Console.WriteLine("Int: " + Item); }
}

// 2. Box for Strings
public class StringBox {
    public string Item;
    public void Display() { Console.WriteLine("String: " + Item); }
}

// 3. Box for Vector2D
public class VectorBox {
    public Vector2D Item;
    public void Display() { Console.WriteLine("Vector: " + Item.X + "," + Item.Y); }
}
// To store a Score (Integer)
IntBox scoreBox = new IntBox();
scoreBox.Item = 100;
scoreBox.Display();

// To store a Player Name (String)
StringBox nameBox = new StringBox();
nameBox.Item = "Arjun";
nameBox.Display();

// To store a Position (Vector2D)
VectorBox posBox = new VectorBox();
posBox.Item = new Vector2D(10, 20);
posBox.Display();

The Issue (The Mess):

  1. Code Duplication: You are writing the exact same Display logic three times.

  2. Maintenance Nightmare: If you want to add a feature (like a “Clear” method), you have to go into all three classes and add it manually.

  3. Human Error: You might forget to update one of them, leading to bugs.

Generic Way (Generics Class) Now, instead of building three separate boxes, we build One Generic Blueprint. We use <T> as a placeholder for “The Type.”

// THE MASTER BLUEPRINT
public class StorageBox<T> 
{
    public T Item;

    public void Display() 
    { 
        Console.WriteLine($"Stored {typeof(T).Name}: {Item}"); 
    }
}

Now, the developer only needs to know one class name: StorageBox.


public struct Vector2D 
{
	public float X, Y;
	public Vector2D(float x, float y) { X = x; Y = y; }

	// This tells the computer: 
	// "When someone asks for a string, show the X and Y, not just the name."
	public override string ToString() 
	{
		return $"({X}, {Y})";
	}
}


// The Compiler creates an Int version
StorageBox<int> intBox = new StorageBox<int>();
intBox.Item = 100;

// The Compiler creates a String version
StorageBox<string> nameBox = new StorageBox<string>();
nameBox.Item = "Flare Engine";

// The Compiler creates a Vector version
StorageBox<Vector2D> posBox = new StorageBox<Vector2D>();
posBox.Item = new Vector2D(5, 10);

intBox.Display();
nameBox.Display();
posBox.Display();

Without Generics, if you wanted a “Storage Box” for different items, you would have to build a new class for every item type. With Generics, you build one box and tell the computer: “This box holds ‘T’ (Type). I’ll tell you what ‘T’ is when I use it.”

T = Type

Why Generics don’t’ need inheritance

Just like Operator Overloading, Generics care about the Label, not the Family.

Generics are extremely flexible—you can apply that <T> blueprint to an entire Class or just to a single Method inside a normal class.

  • Generic Class: The whole building is designed to handle type T.
  • Generic Method: The building is normal, but it has one specialized room that can handle any type T.

//Example for Vector class

using System.Numerics; // Required for Generic Math

// We add <T> and a "Constraint" that T must be a number
public struct Vector2D<T>  // where T : INumber<T>
{
    public T X;
    public T Y;

    public Vector2D(T x, T y)
    {
        X = x;
        Y = y;
    }

    // Now '+' works for any numeric T!
    public static Vector2D<T> operator +(Vector2D<T> v1, Vector2D<T> v2)
    {
        return new Vector2D<T>(v1.X + v2.X, v1.Y + v2.Y);
    }

    public static Vector2D<T> operator *(Vector2D<T> v1, T scalar)
    {
        return new Vector2D<T>(v1.X * scalar, v1.Y * scalar);
    }
    
    public override string ToString() { // (virtual methods).
	    return  $"({X}, {Y})";
    }
}
// Case A: High Precision for Physics (double)
Vector2D<double> precisePos = new Vector2D<double>(10.555, 20.123);
Console.WriteLine(precisPos);
// Case B: Simple Grid Movement (int)
Vector2D<int> gridPos = new Vector2D<int>(5, 10);
Console.WriteLine(gridPos);
// Case C: Standard Game Math (float)
Vector2D<float> gamePos = new Vector2D<float>(2.5f, 3.0f);
Console.WriteLine(gamePos);

Example for Generic Methods

// 1. A NORMAL class (No <T> here)
public class PrinterBot
{
    public string BotName = "Flare-Bot";

    // 2. A GENERIC method (The <T> is only for this action)
    public void PrintAnything<T>(T data)
    {
        Console.WriteLine($"{BotName} says: {data}");
    }
}
PrinterBot bot = new PrinterBot(); // Created like any other class

// Case A: Printing an Integer
bot.PrintAnything<int>(500);

// Case B: Printing a String
bot.PrintAnything<string>("Welcome to C#");

// Case C: Printing your custom Vector2D
Vector2D playerPos = new Vector2D(10, 20);
bot.PrintAnything<Vector2D>(playerPos);
Where?Why?
Databases/ListsTo store any type of object without rewriting code.
API / WebTo send and receive different types of messages.
Game EnginesTo load different assets (Audio, Texture, Mesh) using one system.