<style> .reveal-up, .reveal-left, .reveal-right, .reveal-stagger > * { opacity: 1 !important; transform: none !important; } </style>
  1. Learn
  2. C#
  3. Working with data
All lessons in this course

Lesson 33 of 60 14 minIntermediate

Generics

Write classes and methods that work with any type while staying type-safe: generic methods, generic classes, constraints and why List<T> beats ArrayList.

You have been using generics since the collections lessons: List<string>, Dictionary<string, double>. The <T> is a type parameter: a placeholder that is filled in with a real type when the code is used. Generics let you write code once that works with many types, without losing type safety.

The problem generics solve

WithoutGenerics.cs
// Before generics: one method per type…static int LargerInt(int a, int b) => a > b ? a : b;static double LargerDouble(double a, double b) => a > b ? a : b; // …or 'object', which loses type safetyvar oldList = new System.Collections.ArrayList();oldList.Add(5);oldList.Add("five");              // compiles: anything goesint n = (int)oldList[1]!;         // InvalidCastException at RUN time

Generic methods

Interactive

Write once, use with any type

Choose what T is. The compiler makes a version of Larger<T> for that type. Then turn the constraint on and try Beam.

T =
Generic definition
Larger.cs
static T Larger<T>(T a, T b)    where T : IComparable<T>{    return a.CompareTo(b) >= 0 ? a : b;}
What it means for T = int
Program.cs
int x = 7;int y = 12;int big = Larger<int>(x, y);   // or just Larger(x, y)Console.WriteLine(big);
Output
12
Compiles. T is int everywhere, so the result is a int: no casts, no boxing, and a wrong type is caught at compile time.
Generic.cs
static T Larger<T>(T a, T b) where T : IComparable<T>    => a.CompareTo(b) >= 0 ? a : b; static void Swap<T>(ref T a, ref T b) => (a, b) = (b, a); Console.WriteLine(Larger(7, 12));            // T inferred as intConsole.WriteLine(Larger("Beam", "Column"));  // T = string int x = 1, y = 2;Swap(ref x, ref y);Console.WriteLine($"{x} {y}");
Output
12
Column
2 1

Usually you don’t write <int> in the call: the compiler infers T from the arguments.

Generic classes

Result.cs
public class Result<T>{    public bool Ok { get; }    public T? Value { get; }    public string Error { get; } = "";     private Result(bool ok, T? value, string error) { Ok = ok; Value = value; Error = error; }     public static Result<T> Success(T value) => new(true, value, "");    public static Result<T> Fail(string error) => new(false, default, error);} Result<double> r = Result<double>.Success(42.2);Result<Beam> b = Result<Beam>.Fail("Profile not found");Console.WriteLine(r.Ok ? $"Value {r.Value}" : r.Error);Console.WriteLine(b.Ok ? "ok" : b.Error);
Output
Value 42.2
Profile not found

Constraints

ConstraintT must…
where T : classbe a reference type
where T : structbe a non-nullable value type
where T : new()have a parameterless constructor (so you can new T())
where T : StructuralMemberbe that class or derive from it
where T : IComparable<T>implement that interface
where T : notnullnot be a nullable type

Constraints are a two-way deal: callers can only use allowed types, and inside the method you can use everything the constraint guarantees (like CompareTo).

default(T)

default gives the default value of whatever T is: 0 for numbers, false for bool, null for reference types. Handy for “not found” results in generic code.

Quick check

What does the compiler do when you call Larger(3, 9) with static T Larger<T>(T a, T b)?

Quick check

Which constraint lets you write new T() inside a generic method?

Practice

Write a generic class Pair<TFirst, TSecond> with two properties and a method Pair<TSecond, TFirst> Swapped(). Create a Pair<string, int>("B1", 6), swap it and print both.

Show the solution
Solution.cs
public class Pair<TFirst, TSecond>{    public TFirst First { get; }    public TSecond Second { get; }    public Pair(TFirst first, TSecond second) { First = first; Second = second; }     public Pair<TSecond, TFirst> Swapped() => new(Second, First);    public override string ToString() => $"({First}, {Second})";} var p = new Pair<string, int>("B1", 6);Console.WriteLine(p);Console.WriteLine(p.Swapped());
Output
(B1, 6)
(6, B1)

Remember

  • Generics use type parameters (<T>) to write reusable, type-safe code.
  • The compiler usually infers T from the arguments.
  • Constraints (where T : …) limit T and unlock members you can use.
  • Generic collections avoid casts and boxing, so they are faster and safer.
0 of 60 lessons completed