C# Generics: The Magic Shape-Shifter Box 🎁
Imagine you have a magical box. This box can hold anything you want—toys, cookies, or even dinosaurs! But here’s the cool part: once you decide what goes inside, it only accepts that thing. No mix-ups. No surprises.
That’s what Generics are in C#. They let you create code that works with any type while keeping everything safe and organized.
🌟 Generic Fundamentals
What Problem Do Generics Solve?
Without Generics (The Messy Way):
Think of a toy box that accepts “anything.” You throw in a car, a doll, and suddenly… a banana? Now when you reach in, you don’t know what you’ll get!
ArrayList box = new ArrayList();
box.Add(5); // number
box.Add("hello"); // text
// Confusing! What's inside?
With Generics (The Smart Way):
Now imagine a box with a label. It says “TOYS ONLY.” Everyone knows what goes in, and you always know what comes out!
List<int> numberBox = new List<int>();
numberBox.Add(5);
numberBox.Add(10);
// Only numbers allowed!
The Magic <T> Symbol
The letter T is like a placeholder—a blank space waiting for you to fill in.
T = "Whatever type YOU choose"
When you write <T>, you’re saying: “I’ll tell you the type later!”
Real Example:
// T becomes int
List<int> ages = new List<int>();
// T becomes string
List<string> names = new List<string>();
Why Use Generics?
| Without Generics | With Generics |
|---|---|
| Type errors at runtime 💥 | Errors caught early ✅ |
| Need type casting | No casting needed |
| Slower (boxing/unboxing) | Faster performance |
| Less readable code | Crystal clear code |
🏠 Generic Classes
Building Your Own Magic Box
A generic class is like building your own customizable container. You design it once, then use it with any type!
The Blueprint:
public class Box<T>
{
private T item;
public void Put(T thing)
{
item = thing;
}
public T Get()
{
return item;
}
}
Using Your Box:
// A box for toys (strings)
Box<string> toyBox = new Box<string>();
toyBox.Put("Teddy Bear");
string myToy = toyBox.Get();
// A box for numbers
Box<int> scoreBox = new Box<int>();
scoreBox.Put(100);
int myScore = scoreBox.Get();
Multiple Type Parameters
What if your magic box needs TWO labels? No problem! Use multiple letters.
public class Pair<TFirst, TSecond>
{
public TFirst First { get; set; }
public TSecond Second { get; set; }
}
Usage:
// A pair of name (string) and age (int)
Pair<string, int> person =
new Pair<string, int>();
person.First = "Luna";
person.Second = 8;
graph TD A["Pair&lt;TFirst, TSecond&gt;"] B["TFirst = string"] C["TSecond = int"] D["Result: Pair&lt;string, int&gt;"] A --> B A --> C B --> D C --> D
🔧 Generic Methods
A Method That Transforms
Instead of making a whole class generic, you can make just one method generic. It’s like having a single magic wand that works on anything!
The Swap Trick:
public static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
Watch It Work:
int x = 5, y = 10;
Swap(ref x, ref y);
// Now x=10, y=5!
string first = "Hello";
string second = "World";
Swap(ref first, ref second);
// Now first="World", second="Hello"!
Generic Methods in Non-Generic Classes
You can add a generic method to a regular class. No need to make the whole class generic!
public class Helper
{
public T GetFirst<T>(T[] items)
{
return items[0];
}
}
Usage:
Helper helper = new Helper();
int[] numbers = {1, 2, 3};
int first = helper.GetFirst(numbers);
// first = 1
string[] words = {"Hi", "Bye"};
string firstWord = helper.GetFirst(words);
// firstWord = "Hi"
🔒 Generic Constraints
Setting the Rules
Sometimes your magic box shouldn’t accept everything. What if you want only toys with batteries? You need constraints!
Constraints say: “T must be a certain kind of thing.”
The Constraint Keywords
| Constraint | Meaning |
|---|---|
where T : class |
T must be a reference type |
where T : struct |
T must be a value type |
where T : new() |
T must have a constructor |
where T : SomeClass |
T must inherit from SomeClass |
where T : ISomething |
T must implement interface |
Examples in Action
Only Classes Allowed:
public class Holder<T> where T : class
{
public T Item { get; set; }
}
// Works!
Holder<string> h1 = new Holder<string>();
// Error! int is not a class
// Holder<int> h2 = new Holder<int>();
Must Have Empty Constructor:
public T CreateNew<T>() where T : new()
{
return new T();
}
Must Implement Interface:
public void Print<T>(T item)
where T : IPrintable
{
item.Print();
}
Combining Multiple Constraints
You can stack rules together!
public class Manager<T>
where T : class, IComparable, new()
{
// T must be:
// 1. A reference type (class)
// 2. Implement IComparable
// 3. Have an empty constructor
}
graph TD A["T enters the gate"] B{Is it a class?} C{Has IComparable?} D{Has new constructor?} E["✅ Allowed In!"] F["❌ Rejected!"] A --> B B -->|Yes| C B -->|No| F C -->|Yes| D C -->|No| F D -->|Yes| E D -->|No| F
🔄 Covariance and Contravariance
The Direction of Types
This sounds fancy, but it’s really about which direction types can flow. Think of it like water flowing uphill or downhill.
Covariance (OUT = Can Go Up)
Covariance lets you use a more specific type where a general type is expected.
Imagine: A basket of Red Apples 🍎 can be treated as a basket of Fruits 🍇🍊🍎.
The Keyword: out
// IEnumerable is covariant
IEnumerable<string> strings =
new List<string> { "Hi" };
// string inherits from object
// So this works!
IEnumerable<object> objects = strings;
Creating Covariant Interface:
public interface IProducer<out T>
{
T Produce(); // T only goes OUT
}
Contravariance (IN = Can Go Down)
Contravariance is the opposite. You can use a more general type where a specific type is expected.
Imagine: Someone who can eat ANY Fruit 🍇🍊🍎 can definitely eat Red Apples 🍎.
The Keyword: in
// Action is contravariant
Action<object> printObject =
(o) => Console.WriteLine(o);
// object is more general than string
// So this works!
Action<string> printString = printObject;
Creating Contravariant Interface:
public interface IConsumer<in T>
{
void Consume(T item); // T only goes IN
}
The Simple Rule
| Direction | Keyword | Memory Trick |
|---|---|---|
| Covariance | out |
Data goes OUT (return values) |
| Contravariance | in |
Data goes IN (parameters) |
graph TD subgraph Covariance A["Child Type"] -->|Can become| B["Parent Type"] C["List&lt;Cat&gt;"] -->|Can become| D["IEnumerable&lt;Animal&gt;"] end subgraph Contravariance E["Parent Type"] -->|Can become| F["Child Type"] G["Action&lt;Animal&gt;"] -->|Can become| H["Action&lt;Cat&gt;"] end
Quick Summary Table
| Concept | Keyword | Direction | Use When |
|---|---|---|---|
| Covariance | out |
Child → Parent | Returning values |
| Contravariance | in |
Parent → Child | Taking parameters |
| Invariant | none | No conversion | Both in and out |
🎯 Putting It All Together
Here’s a complete example combining everything:
// Generic class with constraint
public class Zoo<T> where T : Animal
{
private List<T> animals =
new List<T>();
// Generic method
public void Add<TAnimal>(TAnimal pet)
where TAnimal : T
{
animals.Add(pet);
}
// Covariant return
public IEnumerable<T> GetAll()
{
return animals;
}
}
// Usage
Zoo<Mammal> mammalZoo = new Zoo<Mammal>();
mammalZoo.Add(new Cat());
mammalZoo.Add(new Dog());
// Covariance in action!
IEnumerable<Animal> allAnimals =
mammalZoo.GetAll();
💡 Key Takeaways
-
Generics = Type-Safe Templates Write once, use with any type safely
-
<T>is Your Placeholder Fill it in when you use the class/method -
Constraints = Safety Rules Control what types are allowed
-
Covariance (
out) = Child → Parent For when you’re giving data OUT -
Contravariance (
in) = Parent → Child For when you’re taking data IN
🎉 You Did It!
Generics might seem tricky at first, but now you know they’re just smart, flexible boxes. You control what goes in, and C# makes sure nothing unexpected sneaks out!
