Generics
You have already used generics many times: List<string>, Dictionary<string, int>, Queue<T>. The part in angle brackets is a type parameter: one List<T> class works for strings, ints, books or anything else, and the compiler still checks every type. Without generics you would need a StringList, an IntList and a BookList, or a single list of object that loses all type safety.
In this lesson you will write your own generic methods and classes, restrict type parameters with constraints, and use generic math to write one method that works for every number type.
Generic methods
A generic method declares its type parameters in angle brackets after its name. By convention a single type parameter is called T.
int a = 5, b = 9;
Swap(ref a, ref b);
Console.WriteLine($"{a} {b}");
string first = "Pune", second = "Goa";
Swap(ref first, ref second);
Console.WriteLine($"{first} {second}");
Console.WriteLine(Describe(42));
Console.WriteLine(Describe("Kochi"));
Console.WriteLine(Describe(3.5m));
void Swap<T>(ref T x, ref T y)
{
T temp = x;
x = y;
y = temp;
}
string Describe<T>(T value) => $"{value} is a {typeof(T).Name}";9 5 Goa Pune 42 is a Int32 Kochi is a String 3.5 is a Decimal
You did not write Swap<int>(...): the compiler infers T from the arguments. typeof(T) gives the actual type used for T in that call. (For swapping two variables you can also write (a, b) = (b, a); with tuples.)
Constraints
Inside a generic method, the compiler only lets you do things that work for every possible T. Writing item > best fails, because not every type supports >. A constraint says what T must be able to do, and in return you may use those abilities.
int[] runs = [45, 78, 12];
string[] cities = ["Pune", "Agra", "Kochi"];
double[] temps = [31.5, 38.25, 29.0];
Console.WriteLine(Largest(runs));
Console.WriteLine(Largest(cities));
Console.WriteLine(Largest(temps));
T Largest<T>(T[] items) where T : IComparable<T>
{
T best = items[0];
foreach (T item in items)
{
if (item.CompareTo(best) > 0)
{
best = item;
}
}
return best;
}78 Pune 38.25
where T : IComparable<T> means "T must be comparable with itself", which int, string and double all are. The compiler now allows item.CompareTo(best). Calling Largest with a type that is not comparable is a compile error, not a run-time crash.
| Constraint | Meaning |
|---|---|
where T : class | T must be a reference type |
where T : struct | T must be a non-nullable value type |
where T : notnull | T must not be a nullable type |
where T : new() | T must have a public parameterless constructor |
where T : SomeBaseClass | T must be that class or derive from it |
where T : ISomeInterface | T must implement that interface |
You can combine several constraints, for example where T : class, IComparable<T>, new().
Generic classes
A class can have type parameters too. Here is a shelf with a fixed capacity that can hold any kind of item:
var shelf = new Shelf<Book>(capacity: 2);
Console.WriteLine(shelf.TryAdd(new Book("Godan")));
Console.WriteLine(shelf.TryAdd(new Book("Gitanjali")));
Console.WriteLine(shelf.TryAdd(new Book("Malgudi Days")));
Console.WriteLine($"{shelf.Count} items, first: {shelf[0]}");
var lunchBoxes = new Shelf<string>(3);
lunchBoxes.TryAdd("Rajma chawal");
Console.WriteLine(lunchBoxes[0]);
class Shelf<T>(int capacity)
{
private readonly List<T> _items = [];
public int Count => _items.Count;
public T this[int index] => _items[index];
public bool TryAdd(T item)
{
if (_items.Count >= capacity)
{
return false;
}
_items.Add(item);
return true;
}
}
record Book(string Title);True
True
False
2 items, first: Book { Title = Godan }
Rajma chawalShelf<Book> and Shelf<string> are two different, fully type-checked types made from one class. shelf[0] is a Book, so there is no casting. The member public T this[int index] is an indexer: it lets callers use square brackets on your object, just like a list.
default(T)
Sometimes a generic method needs a "nothing" value for T. default(T) (or just default when the type is clear) gives 0 for numbers, false for bool and null for reference types.
List<int> empty = [];
List<string> names = ["Aarav", "Isha"];
Console.WriteLine(FirstItemOrDefault(empty));
Console.WriteLine(FirstItemOrDefault(names));
Console.WriteLine(FirstItemOrDefault(new List<string>()) is null);
T? FirstItemOrDefault<T>(List<T> items) => items.Count > 0 ? items[0] : default;0 Aarav True
LINQ already has a FirstOrDefault method that works this way; you will use it in LINQ.
Generic math
using System.Numerics;
int[] visitors = [120, 80, 45];
decimal[] bills = [99.5m, 0.5m, 250m];
Console.WriteLine(Total(visitors));
Console.WriteLine(Total(bills));
T Total<T>(T[] values) where T : INumber<T>
{
T sum = T.Zero;
foreach (T v in values)
{
sum += v;
}
return sum;
}245 350.0
Before generic math, you had to write one Total per number type, because + is not available on an unconstrained T. T.Zero works because INumber<T> declares a static abstract member that every number type provides.
Common mistakes
- Using operators on an unconstrained
T.a + bgivesCS0019: Operator '+' cannot be applied to operands of type 'T' and 'T'. Add a constraint such asINumber<T>. - Calling methods the constraint does not promise. If
Tis onlyIComparable<T>, you cannot call.Lengthon it. - Over-generalising. If a method only ever handles
decimal, just writedecimal. Use generics when you truly need several types. - Thinking
List<Manager>is aList<Employee>. It is not, because you could add anInternto it. Read-only interfaces such asIEnumerable<Employee>do accept aList<Manager>.
Key points
- Type parameters such as
<T>let one method or class work with many types safely. - The compiler usually infers
Tfrom the arguments of a generic method. - Constraints (
where T : ...) state whatTmust support and unlock those members. - Generic classes such as
Shelf<T>become separate, type-checked types for eachT. default(T)gives the zero or null value for any type.- Generic math (
INumber<T>,T.Zero) lets one method handle every number type.
Exercise
Write a generic method CountAbove<T>(IEnumerable<T> items, T limit) that returns how many items are greater than limit, using a constraint so you can compare. Test it with the marks [35, 72, 90, 64, 18] and limit 60, and with the cities ["Agra", "Pune", "Delhi", "Surat"] and limit "Kochi" (alphabetical comparison). Then write a generic class Pair<TFirst, TSecond> as a record and create a pair of a student name and her marks.
Show solution
int[] marks = [35, 72, 90, 64, 18];
string[] cities = ["Agra", "Pune", "Delhi", "Surat"];
Console.WriteLine(CountAbove(marks, 60));
Console.WriteLine(CountAbove(cities, "Kochi"));
var topper = new Pair<string, int>("Ananya", 90);
Console.WriteLine(topper);
int CountAbove<T>(IEnumerable<T> items, T limit) where T : IComparable<T>
{
int count = 0;
foreach (T item in items)
{
if (item.CompareTo(limit) > 0)
{
count++;
}
}
return count;
}
record Pair<TFirst, TSecond>(TFirst First, TSecond Second);3
2
Pair { First = Ananya, Second = 90 }72, 90 and 64 are above 60. Alphabetically, "Pune" and "Surat" come after "Kochi". Records can be generic too, so Pair<TFirst, TSecond> works for any two types.
Quick Quiz
1. Why is List<int> better than an old ArrayList for numbers?
Generic collections know their item type, so mistakes are caught early and values are not boxed.
2. What does where T : IComparable<T> allow inside a generic method?
The constraint guarantees that T implements IComparable<T>, so CompareTo is available.
3. What is default(T) when T is string?
string is a reference type, so its default value is null.
4. Which interface lets a generic method use + and T.Zero for any number type?
Generic math interfaces such as INumber<T> describe arithmetic operators and constants like Zero.
Learn C# with a mentor
Free lessons get you started. Our Programming & Software Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
