Generics
Summary
A generic type or method is written once against a placeholder type and
works correctly for any concrete type substituted in later — List<T>
works identically whether T is int, Enemy, or InventoryItem. Generics
let you write a single, type-safe implementation instead of duplicating the
same logic for every type it needs to support, or falling back to untyped
object and losing compile-time safety entirely.
Why This Matters
Without generics, a reusable container or algorithm has two bad options.
Either it's duplicated per type (IntStack, EnemyStack, ItemStack, each
with identical logic), which means every bug fix has to be applied N times,
or it's written against object, which compiles for any type but pushes
every mistake to runtime — pulling an Enemy out of a stack that actually
holds InventoryItems throws a cast exception mid-game instead of failing
at compile time. Generics give you the reuse of the object approach with
the safety of the duplicated approach.
Mental Model
A generic class is a form with a blank on it, not a filled-in document. The
form (List<T>) defines the shape — "a growable sequence of T" — without
committing to what T is. Filling in the blank (List<Enemy>,
List<int>) produces a fully concrete type that behaves exactly as if you'd
hand-written a list just for that type, except the compiler generated it
from the same form and checked every usage against the type you filled in.
Core Explanation
- Type parameters are placeholders, resolved at compile time.
Tinclass Stack<T>isn't a real type — it's a variable that stands in for whatever type is supplied when the generic is used (Stack<int>,Stack<Enemy>), and the compiler substitutes it everywhereTappears. - Type safety is enforced at every use site. Once you declare
Stack<Enemy> enemies, the compiler rejectsenemies.Push(someItem)at compile time — the same mistake against anobject-based stack would compile fine and fail only when the item is later cast back out. - Constraints narrow what
Tis allowed to be. A generic can requireTto implement an interface, have a parameterless constructor, or be a reference/value type (where T : IDamageable,where T : new()), letting the generic code call members onTthat a fully unconstrained placeholder wouldn't allow. - Generic methods vs. generic types. An entire class can be generic
(
Stack<T>), or just a single method on an otherwise ordinary class (T Find<T>(...)) — the same substitution rules apply at the smaller scope.
Syntax / Theory
C# (Unity)
public class ObjectPool<T> where T : Component
{
private readonly Stack<T> available = new Stack<T>();
private readonly T prefab;
public ObjectPool(T prefab) => this.prefab = prefab;
public T Rent() =>
available.Count > 0 ? available.Pop() : Object.Instantiate(prefab);
public void Return(T instance)
{
instance.gameObject.SetActive(false);
available.Push(instance);
}
}
// T is inferred from the argument — no need to write Find<Enemy> explicitly.
public T Find<T>(IEnumerable<T> items, System.Func<T, bool> predicate)
{
foreach (var item in items)
if (predicate(item)) return item;
return default;
}
C++ (Unreal — templates)
template<typename T>
class TObjectPool
{
public:
T* Rent()
{
if (Available.Num() > 0)
{
T* Instance = Available.Pop();
return Instance;
}
return NewObject<T>();
}
void Return(T* Instance) { Available.Push(Instance); }
private:
TArray<T*> Available;
};
Unreal's own container types (TArray<T>, TMap<K, V>) are themselves
C++ templates, following the identical placeholder-substitution model.
Practical Example
A save system needs to serialize and deserialize many unrelated data types without duplicating the read/write logic for each one.
public class SaveSlot<T>
{
public T Data;
public void Save(string path) =>
File.WriteAllText(path, JsonUtility.ToJson(Data));
public static SaveSlot<T> Load(string path) =>
JsonUtility.FromJson<SaveSlot<T>>(File.ReadAllText(path));
}
var settings = new SaveSlot<GameSettings> { Data = currentSettings };
settings.Save("settings.json");
var progress = new SaveSlot<PlayerProgress> { Data = currentProgress };
progress.Save("progress.json");
SaveSlot<T> is written once; the compiler generates a correctly-typed
version for GameSettings, PlayerProgress, or any future save-data type,
with no risk of accidentally loading progress data into a settings slot —
that mismatch is a compile error, not a runtime surprise.
Common Mistakes
- Reaching for
objectinstead of a type parameter for "I want this to work with anything," which discards compile-time checking entirely and pushes every type mismatch to a runtime cast exception. - Over-constraining or under-constraining
T. Too loose (where T : new()when the code also callsIDamageable.TakeDamage) fails to compile at the call site with a confusing error; too strict (constraining to a concrete class when an interface would do) needlessly limits what the generic can be used with. - Treating a generic type as if it shares state across different
Tfillings.ObjectPool<Enemy>andObjectPool<Bullet>are two entirely separate types with independent state — nothing is shared between them just because they come from the same generic definition. - Writing a generic before there's a second concrete use case. A generic form is justified by having at least two real callers with different types; generalizing prematurely for a hypothetical future type adds indirection with no payoff yet (see [[CON-0008]] on avoiding speculative structure).
Game Development Example
An ObjectPool<T> (Syntax / Theory above) is one of the most common
generic uses in gameplay code — bullets, particle effects, and enemies all
benefit from pooling, and without generics you'd need a near-identical
BulletPool, ParticlePool, and EnemyPool class, each fixing the same
bug independently if one is found.
public class WeaponSystem : MonoBehaviour
{
[SerializeField] private Bullet bulletPrefab;
private ObjectPool<Bullet> bulletPool;
void Awake() => bulletPool = new ObjectPool<Bullet>(bulletPrefab);
void Fire()
{
Bullet bullet = bulletPool.Rent();
bullet.transform.position = muzzle.position;
bullet.gameObject.SetActive(true);
}
}
Best Practices
- Only generalize a type or method once a second concrete use case exists — a generic with exactly one caller is usually premature.
- Constrain
Tas tightly as the generic's implementation actually requires, no tighter — a constraint that's too broad invites misuse, one that's too narrow blocks legitimate callers. - Prefer a generic method over a generic class when only one method actually needs the type parameter — it keeps the rest of the class simpler and non-generic.
- Give type parameters a descriptive name (
TItem,TKey) instead of a bareTonce a type has more than one parameter, soDictionary<TKey, TValue>reads clearly at the declaration.
Exercises
- Write a generic
Pair<T1, T2>class that stores two values of potentially different types and exposes them asFirst/Second. Instantiate it once with twoints and once with astringand abool. - Take the
ObjectPool<T>example and add awhere T : Componentisn't present — instead constrainTto a customIPoolableinterface with aResetState()method, and call it insideReturn. - You're deciding between a generic
Inventory<T>(one inventory type per item category) and a single non-genericInventorythat stores a commonItembase type. Argue which is the better choice for a game with weapons, potions, and quest items, and justify the trade-off. - Research C#'s covariance/contravariance (
out T,in T) on generic interfaces and explain, in your own words, whyIEnumerable<Enemy>can be used whereIEnumerable<object>is expected butList<Enemy>cannot be used whereList<object>is expected.
Related Concepts
- Collections — the built-in collection types (
List<T>,Dictionary<TKey, TValue>) are the generics a learner encounters first, before writing their own. - Interfaces — generic constraints (
where T : ISomeInterface) are how a generic gains the ability to call members on the placeholder type. - Data Types — understanding the difference between value types and
reference types explains why generic constraints like
where T : structorwhere T : classexist at all.
Further Reading
KNOWLEDGE_SCHEMA.md— GDKB's concept maturity ladder and cross-linking rules, which this document follows.- Microsoft C# documentation — Generics: https://learn.microsoft.com/dotnet/csharp/fundamentals/types/generics
- Unreal Engine C++ documentation — Templates and Generic Programming: https://dev.epicgames.com/documentation/en-us/unreal-engine/unreal-engine-cpp-templates