All types with <T>, such as List<int> and Dictionary<string, int>, are generic. Generics are a powerful mechanism that “works on any type, but is locked to one type when used.” You can achieve code reuse and type safety at the same time.
Learning Objectives
- Define and use generic classes and methods
- Know why and how to constrain type parameters with
where - Understand covariance (
out) and contravariance (in).
Key concepts
1) Generic classes
<T> is a placeholder meaning “a type that has not yet been determined.” It is determined at the time of use.
public class Box<T>
{
public T? Value { get; set; }
}
var intBox = new Box<int> { Value = 42 };
var strBox = new Box<string> { Value = "hello" };
If you use T instead of object, Boxing/Casting is not needed and the type is checked at compile time.
2) Generic methods
public static T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) >= 0 ? a : b;
}
int m = Max(3, 7); //T is inferred to be int
string s = Max("a", "z");
Most compilers will infer T. When specified, Max<int>(3, 7).
3) Constraints (where)
| pharmaceutical | Meaning |
|---|---|
where T : class |
Reference type only (string, class) |
where T : struct |
Value type only (int, bool, structure) |
where T : new() |
Requires parameterless constructor |
where T : IComparable<T> |
implement that interface |
where T : SomeBase |
SomeBase or its children |
Combining multiple: where T : class, new() (reference type + default constructor).
4) Covariance (out) and contravariance (in)
Simply put, it is an option that "allows casting between generic types".
IEnumerable<out T>:IEnumerable<Dog>can be used asIEnumerable<Animal>(covariant, only for output).Action<in T>:Action<Animal>can be used asAction<Dog>(converse, when only receiving input).
For now, just remember, “There is this.” Detailed usage is an advanced topic.
See as example
Example 1 — GenericClass : Definition/Use of Box<T>
var intBox = new Box<int> { Value = 42 };
var strBox = new Box<string> { Value = "hello" };
Console.WriteLine($"intBox: {intBox.Value}");
Console.WriteLine($"strBox: {strBox.Value}");
public class Box<T>
{
public T? Value { get; set; }
public void Show() => Console.WriteLine($"Value in Box: {Value}");
}
Execution result
intBox: 42
strBox: Hello
Memo: Code duplication is eliminated as all types can be contained in Box<T> defined once. ? in T? indicates that Value may initially be null.
Example 2 — GenericMethod : Type inference with Max<T>
Console.WriteLine(Max(3, 7));
Console.WriteLine(Max("apple", "banana"));
Console.WriteLine(Max(3.14, 2.71));
static T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) >= 0 ? a : b;
}
Execution result
7
banana
3.14
Note: int, string, and double all implement IComparable<T>, so they satisfy the constraint. If CompareTo is positive, a is larger.
Example 3 — Comparing Constraint : where constraints
//class + new() constraint → new instance can be created
static T Create<T>() where T : class, new() => new T();
//struct constraints → value types only
static T DoubleIt<T>(T x) where T : struct
{
Console.WriteLine($"Received value: {x}");
return x;
}
Person p = Create<Person>();
p.Name = "exponent";
Console.WriteLine($"Newly created by: {p.Name}");
DoubleIt(42);
DoubleIt(3.14);
//DoubleIt("String"); ← Compilation error: string is not a struct
public class Person
{
public string Name { get; set; } = "";
}
Execution result
Newly created by: Jisoo
Received value: 42
Received value: 3.14
Note: The new() constraint allows calling new T(). The struct constraint blocks invalid types at compile time.
Example 4 — Variance: A glance at covariability and contravariance
//IEnumerable<out T> — covariant (using Dog 's collection as Animal collection)
IEnumerable<Dog> dogs = [new Dog("Baduki"), new Dog("Poppy")];
IEnumerable<Animal> animals = dogs; //OK : out thanks
foreach (Animal a in animals)
{
Console.WriteLine($"Animal: {a.Name}");
}
//Action<in T> — Counterchange (Animal handling functions can be used in Dog)
Action<Animal> describe = a => Console.WriteLine($"The name is {a.Name}");
Action<Dog> describeDog = describe; //OK: thanks to
describeDog(new Dog("chocolate"));
public class Animal(string name)
{
public string Name { get; } = name;
}
public class Dog(string name) : Animal(name);
Execution result
Animal: Baduki
Animal: Poppy
The name is chocolate
Note: You can safely replace Output Only (out) with a more general type and Input Only (in) with a more specific type. The classes in this example use primary constructor syntax.
Common Mistakes
- I tried to call
new T(), but there was a compilation error because there were no restrictions onnew(). - Confusing
where T : IComparable(no generic) withwhere T : IComparable<T>. - Static fields in generic classes exist separately for each type —
Box<int>.XandBox<string>.Xare different variables. - Overusing
<T>everywhere — In places where only one type is used, the plain type is more readable. - Attempting to attach the
out/inkeyword to a class — transformation applies only to interfaces and delegates.
Cleanup
- Generics take types as parameters and create reusable type-safe code.
whereconstraints can limit and guarantee the capabilities ofT.outis covariant (output),inis covariant (input) — Interface/Delegate only.- Standard collections such as
List<T>andDictionary<K,V>are all the fruits of generics.
Try it yourself
cd src/GenericClass
dotnet run
cd ../GenericMethod
dotnet run
cd ../Constraint
dotnet run
cd ../Variance
dotnet run