Generic Types
Learn how to define and use generic classes and methods to create type-safe, reusable code that works with any data type.
We'll cover the following...
Consider the following Holder class:
public class Holder{public string[] Items { get; private set; }public Holder(int holderSize){Items = new string[holderSize];}public override string ToString(){// Use string.Join to efficiently concatenate itemsreturn "Items inside: " + string.Join(", ", Items);}}
Line 3: Defines a property
Itemsthat holds an array of strings.Lines 5–8: The constructor initializes the array with a specific size provided by the
holderSizeparameter.Line 13: Overrides the
ToStringmethod to return a readable list of the items contained in the array.
It has an Items property of type string. What if we need a similar class that holds integers? The functionality is the same, but the type is different.
We might define a class like this:
public class IntHolder{// Now, the Items property holds integerspublic int[] Items { get; private set; }public IntHolder(int holderSize){Items = new int[holderSize];}public override string ToString(){return "Items inside: " + string.Join(", ", Items);}}
Line 4: The
Itemsproperty is now anintarray.Line 8: The array is initialized as an integer array.
This works. However, is this feasible if we need this behavior for many different types? We would end up copying the same code repeatedly and changing only the data type of the internal array.
A better approach uses generics.
Generic types
Generics allow us to define classes with placeholders for types. We specify the actual type when we declare variables and create objects. Instead of defining separate IntHolder and StringHolder classes, we can define a single class and instantiate it like this:
Holder<int> intHolder = new Holder<int>(3); // Holder class holds integersHolder<string> stringHolder = new Holder<string>(3); // Holder class holds strings
With a generic Holder class, the type of the Items property is specified during variable declaration instead of being hard-coded inside the class.
Line 4: The
<T>syntax indicates thatHolderis a generic class.Tis a placeholder for a specific type.Line 7:
Tis used as the type for theItemsarray. If we create aHolder<int>, this becomesint[].Line 11: We initialize an array of type
T.
With the generic class defined, we can now use it in our main program to create instances for different data types.
Line 4: We instantiate
Holderwithint. The compiler replacesTwithintfor this instance.Lines 6–8: We assign integer values to the array elements. Since
Tisint, the compiler ensures we only assign integers.Line 11: We instantiate
Holderwithstring. The compiler replacesTwithstringfor this instance.Lines 13–14: We assign string values to the array elements. Since
Tisstring, the compiler ensures we only assign strings.Lines 16–17: We print the contents. The first call outputs integers, and the second outputs strings.
We declare Items to be an array of type T. This placeholder is replaced by a concrete type (like int or string) when we create a Holder<T> object. The type specified inside the angle brackets <> is the type argument, which replaces the type parameter T.
Lines 3–4: We create two instances of
Holderwith different type arguments.Lines 7–8: We use
GetType()to verify the underlying type of theItemsarray. The output will confirm one isSystem.Int32[]and the other isSystem.String[].
Default values
When writing generic code, we often do not know if T will be a value type or a reference type. We cannot simply assign null because T might be an int. We also cannot assign 0 because T might be a string.
To handle this, C# provides the default keyword, which returns the appropriate default value for any type:
Reference types (e.g.,
string,List<T>):nullNumeric types (e.g.,
int,double):0Boolean (
bool):falseStructs: All fields initialized to their default values
public class Job<T>{public T Name { get; set; } = null; // Incorrectpublic T JobPosition { get; set; } = default;}
Line 3: This line is incorrect because
Tmight be a value type (likeint), which cannot benull.Line 4: Depending on the actual type used,
defaultwill return the correct default value (e.g.,0forint,nullforstring).
Accept multiple type parameters
A generic class can accept multiple type parameters. We don’t have to limit ourselves to the letter T when choosing a placeholder:
public class SystemBootstrapper<A, B>{public A FirstParameter { get; set; }public B SecondParameter { get; set; }}
Line 1: The class accepts two generic parameters,
AandB.Line 3: The property
FirstParameteris of typeA.Line 4: The property
SecondParameteris of typeB.
Generic methods
Besides generic types, we can create generic methods that accept type parameters:
Line 2: We call
GetDefaultspecifyingstringas the type parameter. The result is stored in a nullable string (string?) becausedefaultfor string is null.Line 3: We call
GetDefaultspecifyingint. The result is0.Lines 5–8: We check if
defaultStringis null. Since the default value of a reference type likestringis null, this block executes and prints “Default string: null”.Line 10: We print the value of
defaultInt. Sinceintis a value type, its default is0.Line 14: The method signature defines a generic type parameter
<T>and returns typeT.Line 17: We return
default!. The!(null-forgiving operator) tells the compiler we intentionally accept thatTmight be null (e.g., for strings), suppressing the compiler warning.