Object-Oriented C# · Lesson 9

Generics

Holder<T> stores one T and returns that same value.

A type parameter preserves useful compile-time information

A generic class defines behavior using a type parameter instead of committing to one concrete stored type. Holder of T exposes Value as T, and its constructor requires a T. Closing that definition with int produces Holder of int; closing it with string produces Holder of string. The compiler can therefore check the values each holder accepts and the operations a caller may use on its result without converting everything into an untyped object container.

The Holder class itself is a reference type in both cases. Choosing int as its type argument does not turn the surrounding Holder instance into a value type. Similarly, var at the call site still infers a specific compile-time type; it does not disable checking or allow Value to change between arbitrary unrelated types. Separate the generic container's own identity and mutability from the characteristics of the type used for its contents.

Close the same class definition with two types

The first construction supplies int as T and twelve as the constructor argument. Inside that closed type, Value is an int property, so the first WriteLine prints 12. The second construction supplies string as T and Ada as the argument. Its Value property is a string, and the next WriteLine prints Ada. The same source-level class definition supports both uses without duplicating the property or constructor body.

The constructor assigns each instance's get-only property. These two Holder objects are separate instances with separate stored values. Reading the int holder again would still return twelve; creating the string holder does not replace some global generic storage. The property has no public setter, but that only restricts replacement of the stored T through this API. If T is a mutable collection, callers may still mutate that collection through the reference returned by Value. A generic wrapper does not automatically make its content deeply immutable.

class Holder<T>
{
    public T Value { get; }
    public Holder(T value) { Value = value; }
}

Generic code can use only the capabilities its contract supplies

Inside Holder of T, the compiler cannot assume T has a Price property or supports every arithmetic operator just because one caller supplies an int. Constraints can state additional required capabilities when a generic algorithm genuinely needs them. This minimal holder needs none because it stores and returns the supplied value. Avoid replacing T with object as a quick workaround for a missing constraint; that can discard useful type information and move invalid assumptions from compilation into runtime casts.

Add a decimal holder, then store a mutable value

First create a Holder of decimal containing 9.50m and print Value. In the prepared invariant runtime, predict 9.50, while the original int and string outputs remain twelve and Ada. Hint: specify the intended type argument and keep the decimal suffix. In a controlled error exercise, try passing Ada to a Holder of int constructor and explain why the compiler rejects the argument before any run can produce output.

Next create a Holder of List of int containing a list with one element. Add a second element through holder.Value and print its Count, predicting two. Hint: the get-only property prevents assigning another list to Value, not calling methods on the returned list object. Then decide whether an independent snapshot or an immutable collection would better satisfy a holder whose contents must remain stable. Explain why that is an ownership requirement beyond the basic generic type parameter.

Name both the closed type and the stored value type

For each variable, state its inferred Holder type and the type returned by Value. Then distinguish compile-time rejection of an incompatible constructor argument from a runtime operation on a valid value. The unchanged sample's two lines are twelve and Ada. A generic abstraction is useful when it preserves the contract callers need while avoiding unnecessary duplication, not merely because putting angle brackets around a type makes a design more flexible.

  • Holder of int exposes an int Value.
  • Holder of string exposes a string Value.
  • The surrounding Holder remains a class in both uses.
  • A get-only property does not deeply freeze an arbitrary T.

A working example

var box = new Holder<int>(12);
Console.WriteLine(box.Value);
var text = new Holder<string>("Ada");
Console.WriteLine(text.Value);

class Holder<T>
{
    public T Value { get; }
    public Holder(T value) { Value = value; }
}

Try this
Say the type argument at the constructor.

Further reading