Generics and constraints
Writing one method that works for any type, without losing the type back out.
After this lesson you can
- Write a generic method with a type parameter
- Constrain a type parameter so its members become usable
- Explain what a generic method keeps that object-typed code loses
Without generics, a reusable method either repeats itself per type or
falls back to object and loses the type on the way out.
public static object First(object[] items) => items[0]; // loses the type
A type parameter keeps the connection between what went in and what comes out:
public static T First<T>(List<T> items) => items[0];
int n = First(new List<int> { 1, 2, 3 }); // T inferred as int
string s = First(new List<string> { "a", "b" }); // T inferred as string
T is a placeholder the caller fills, almost always without writing it
out — the compiler infers it from the argument.
Try it
public static class Solution{ public static (T, T) Swap<T>(T a, T b) => (b, a); public static string Describe() { var (a, b) = Swap(1, 2); var (x, y) = Swap("left", "right"); return $"{a},{b} / {x},{y}"; }}Constraints
Inside the method, you may only do what every possible T allows —
by default, almost nothing. where narrows what a caller may pass, and
in exchange lets the method use it:
public static T Max<T>(T a, T b) where T : IComparable<T>
=> a.CompareTo(b) >= 0 ? a : b;
where T : IComparable<T> says: only types that know how to compare
themselves to another instance of themselves may be used here. Without
the constraint, a.CompareTo(b) would not compile — the compiler has
no way to know an arbitrary T supports it.
Common constraints: where T : class (reference types only),
where T : struct (value types only), where T : new() (must have a
public parameterless constructor, so the method can create one), and
where T : SomeBaseType (must derive from it, or implement it if it is
an interface).
Generic classes work the same way
public class Box<T>
{
public T Value { get; set; }
}
var intBox = new Box<int> { Value = 5 };
var stringBox = new Box<string> { Value = "hi" };
List<T> and Dictionary<TKey, TValue> are themselves ordinary
generic classes, written with exactly this feature — nothing about
them is special-cased into the language.
Common mistakes
public static class Solution{ public static bool AreEqual<T>(T a, T b) => EqualityComparer<T>.Default.Equals(a, b); public static string Describe() { int culture = "apple".CompareTo("Banana"); int ordinal = string.CompareOrdinal("apple", "Banana"); return $"CompareTo={Math.Sign(culture)} CompareOrdinal={Math.Sign(ordinal)} " + $"AreEqual(3,3)={AreEqual(3, 3)} AreEqual(\"a\",\"b\")={AreEqual("a", "b")}"; }}- Assuming
string.CompareTois ordinal. It is culture-aware, using the current culture: it sorts"apple"before"Banana", the way a dictionary would, whilestring.CompareOrdinalcompares character codes and puts the capitalB(66) beforea(97). A genericMax<T>that callsCompareToinherits whatever the type's comparison is. - Writing
a == bon an unconstrainedT. It does not compile, because the compiler cannot knowThas an==. UseEqualityComparer<T>.Default.Equals(a, b), which usesIEquatable<T>when the type has it. - Forgetting what
default(T)is. It isnullfor a reference type and zero for a number, so returningdefaultfrom a generic "find" method cannot tell "not found" from a real0.
What interviewers ask
Exercise · Pro
The larger of two, for any type that can compare itself
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade