Skip to content
Generic Classes and Methods
step 1/6

Reading — step 1 of 6

Learn

~3 min readGenerics and LINQ

Generic Classes and Methods

You already know List<int> and Dictionary<string, int>. Generics are the machinery behind those angle brackets — and you can build your own.

The problem generics solve

Before generics, reusable containers stored object:

ArrayList list = new ArrayList();
list.Add(42);
int n = (int)list[0];        // cast required — a runtime gamble
string s = (string)list[0];  // compiles fine, EXPLODES at runtime

Two costs: every read needs a cast the compiler cannot check, and value types get boxed (wrapped in a heap object) on the way in. Generics fix both — the type parameter makes the element type part of the compile-time contract.

class Box<T> {
    public T Value { get; set; }
    public Box(T v) { Value = v; }
}

var b = new Box<string>("hi");
string s = b.Value;        // no cast — the compiler KNOWS Value is a string

T is a type parameter: a placeholder filled in at the use site. Box<int> and Box<string> are two distinct types generated from one definition.

Generic methods

Methods can have their own type parameters, usually inferred from the arguments:

static T FirstOr<T>(List<T> list, T fallback) {
    return list.Count > 0 ? list[0] : fallback;
}

int x = FirstOr(new List<int> { 3, 4 }, -1);   // T inferred as int

Constraints — the trap and the fix

Inside a generic method the compiler only lets you do things that work for EVERY possible T. So this does not compile:

static T Largest<T>(List<T> list) {
    T m = list[0];
    foreach (var x in list)
        if (x.CompareTo(m) > 0) m = x;   // error: T has no CompareTo
    return m;
}

The fix is a where constraint — it narrows what T can be, and in exchange unlocks that type's members:

static T Largest<T>(List<T> list) where T : IComparable<T> {
    T m = list[0];
    foreach (var x in list)
        if (x.CompareTo(m) > 0) m = x;   // OK now
    return m;
}

Common constraints:

// where T : class          — reference type
// where T : struct         — value type
// where T : new()          — has a parameterless ctor (enables new T())
// where T : SomeBase       — inherits from SomeBase
// where T : IComparable<T> — implements the interface

Same story with operators: x == m on an unconstrained T is a compile error, and default(T) is how you spell "the zero value of T" (null for reference types, 0 for numbers).

Multiple type parameters

class Pair<A, B> {
    public A First;
    public B Second;
    public Pair(A a, B b) { First = a; Second = b; }
    public override string ToString() => $"({First}, {Second})";
}

One footnote worth knowing: C# generics are reified — List<int> really exists at runtime, unlike Java, where erasure turns every List<T> into a raw List.

Your exercise

Build exactly that Pair<A, B>: public fields First and Second, a two-argument constructor, and ToString() returning the pair in parentheses. In Main, construct new Pair<string, int>("Ada", 36) and pass it to Console.WriteLine. The grader expects exactly:

(Ada, 36)

The two mistakes the grader catches every time:

  1. Writing public string ToString() without override. It compiles (with a warning), but Console.WriteLine still calls the original object.ToString(), so instead of your text it prints the type name:
Pair`2[System.String,System.Int32]
  1. Format drift: it is (Ada, 36) — comma, then ONE space. $"({First}, {Second})" gets it right; (Ada,36) fails the test.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…

Generic Classes and Methods — C# Intermediate