C# generics
last modified October 15, 2026
In this article we show how to define and use generics in C#. In generic programming, we use types as parameters to define other types and methods.
Generics were added in C# 2.0.
Generics are classes, structures, interfaces, and methods that have placeholders (type parameters) for one or more of the types that they store or use. A generic collection class might use a type parameter as a placeholder for the type of objects that it stores.
Type parameters are provided after the class, structure, interface, or method name in angle brackets. This syntax tells the compiler that such a name is used in the definition.
By convention, the names of type parameters start with a capital letter
T, such as T, TKey, or
TValue.
In effect, we delay the specification of the data type until it is actually used in the program. Generics enable us to write code that works with a variety of types without repeating the code for each type.
void Swap<T>(ref T lhs, ref T rhs)
We have a definition of a generic method. The T parameter is later
replaced with a concrete type such as int or string at
the calling side.
Generics are abstract blueprints; they cannot be used as-is. When we use a generic class or method, we have to instantiate it with a specific data type that is substituted for the matching placeholder.
.NET provides a large set of interfaces and classes in the
System.Collections.Generic namespace for implementing generic
collections.
Generics provide the following advantages:
- Reusability
- Type safety
- Improved performance
One generic method/class definition can be used for various types, thus reducing the amount of code needed. Generics provide type safety; the compiler checks at compile time that correct types are being provided. Generic types provide better performance because they reduce the need for boxing, unboxing, type checking, and typecasting of variables or objects.
C# generic method
In the following example, we define a generic Swap method.
int x = 10;
int y = 20;
char a = 'x';
char b = 'y';
Swap<int>(ref x, ref y);
Swap<char>(ref a, ref b);
Console.WriteLine($"x = {x}, y = {y}");
Console.WriteLine($"a = {a}, b = {b}");
void Swap<T>(ref T lhs, ref T rhs)
{
T temp = lhs;
lhs = rhs;
rhs = temp;
}
The Swap method exchanges the values of two variables. There is
one definition of a generic Swap method, but it is used for two
different types.
Swap<int>(ref x, ref y); Swap<char>(ref a, ref b);
We call the Swap method for integer values and char values. The
type specification for the method is optional since the compiler can infer the
types from the arguments.
void Swap<T>(ref T lhs, ref T rhs)
{
T temp = lhs;
lhs = rhs;
rhs = temp;
}
Capital T is used as a placeholder, which is replaced with a
specific type when the method is used.
$ dotnet run x = 20, y = 10 a = y, b = x
Next we have a generic Debug method. It prints the type and the
value of the passed generic argument.
string s = "falcon";
bool b = true;
int i = 42;
float f = 4.4f;
double d = 2.0;
Debug<string>(s);
Debug<bool>(b);
Debug<int>(i);
Debug<float>(f);
Debug<double>(d);
void Debug<T>(T arg)
{
Console.WriteLine($"[{typeof(T).Name}]: {arg}");
}
To get the type of the type parameter, we use the typeof operator.
Unlike in Java, the type arguments are available at runtime in C#. The
Name property returns the name of the .NET type.
$ dotnet run [String]: falcon [Boolean]: True [Int32]: 42 [Single]: 4.4 [Double]: 2
In the next example, we create a method that randomly shuffles a list.
var rng = new Random();
var vals = new List<int> { 1, 2, 3, 4, 5, 6 };
var words = new List<string> { "sky", "blue", "war", "toy", "tick" };
Shuffle<int>(vals);
Shuffle<string>(words);
foreach (var e in vals)
{
Console.Write($"{e} ");
}
Console.WriteLine("\n-----------------------");
foreach (var e in words)
{
Console.Write($"{e} ");
}
Console.WriteLine();
void Shuffle<T>(IList<T> vals)
{
int n = vals.Count;
while (n > 1)
{
n--;
int k = rng.Next(n + 1);
T value = vals[k];
vals[k] = vals[n];
vals[n] = value;
}
}
We shuffle a list of integers and a list of words. The method implements the Fisher-Yates shuffle algorithm.
$ dotnet run 6 2 3 1 4 5 ----------------------- toy war sky blue tick $ dotnet run 2 5 1 4 3 6 ----------------------- war sky blue toy tick
Since .NET 8, arrays and spans can be shuffled with the built-in
Random.Shuffle method.
Next we create methods for list access.
var vals = new List<int> { 1, 2, 3, 4, 5, 6 };
var words = new List<string> { "sky", "blue", "war", "toy", "tick" };
Console.WriteLine(First(vals));
Console.WriteLine(Second(vals));
Console.WriteLine(Last(vals));
Console.WriteLine(SecondLast(vals));
Console.WriteLine("--------------");
Console.WriteLine(First(words));
Console.WriteLine(Second(words));
Console.WriteLine(Last(words));
Console.WriteLine(SecondLast(words));
T First<T>(IList<T> items) => items[0];
T Second<T>(IList<T> items) => items[1];
T Last<T>(IList<T> items) => items[^1];
T SecondLast<T>(IList<T> items) => items[^2];
In the example, we have four generic methods to get the first, second, last, and last but one elements of the list.
Console.WriteLine(First(vals)); Console.WriteLine(Second(vals)); ...
The compiler infers the type of the parameter based on the value we pass to the
method; therefore, specifying type (e.g. First<int>) is
optional.
$ dotnet run 1 2 6 5 -------------- sky blue tick toy
C# generic delegate
In the following example, we define a generic delegate. The generic methods use generic math, which was introduced in C# 11 and .NET 7.
using System.Numerics; var mv1 = new ModifyVal<int>(Inc); var mv2 = new ModifyVal<int>(Dec); var mv3 = new ModifyVal<float>(Inc); var mv4 = new ModifyVal<decimal>(Dec); Console.WriteLine(mv1(7)); Console.WriteLine(mv2(7)); Console.WriteLine(mv3(8.5f)); Console.WriteLine(mv4(8.5m)); T Inc<T>(T val) where T : INumber<T> => val + T.One; T Dec<T>(T val) where T : INumber<T> => val - T.One; delegate T ModifyVal<T>(T val);
We define a generic delegate ModifyVal; it is used to refer to
two generic methods: Inc and Dec.
T Inc<T>(T val) where T : INumber<T> => val + T.One;
The Inc is a generic method; it takes a generic parameter and
returns a generic value. The INumber<T> constraint allows us
to use arithmetic operators on the generic type. The static
T.One property returns the value 1 of the given numeric type. All
the checking is done at compile time.
delegate T ModifyVal<T>(T val);
This is the definition of the generic delegate.
$ dotnet run 8 6 9.5 7.5
C# generic class
Next, we define a generic class.
var ds1 = new DataStore<string>();
ds1.Data = "an old falcon";
Console.WriteLine(ds1);
var ds2 = new DataStore<int>();
ds2.Data = 23;
Console.WriteLine(ds2);
var ds3 = new DataStore<bool>();
ds3.Data = false;
Console.WriteLine(ds3);
class DataStore<T>
{
public T? Data { get; set; }
public override string ToString()
{
return Data?.ToString() ?? string.Empty;
}
}
The example stores a value in a generic property. The property is declared as nullable, because it has no value until it is set.
$ dotnet run an old falcon 23 False
C# generic FindAll
In the next example, we define a FindAll list extension method.
public static class ExtensionMethods
{
public static List<T> FindAll<T>(this List<T> vals, List<Predicate<T>> preds)
{
List<T> data = new List<T>();
foreach (T e in vals)
{
bool pass = true;
foreach (Predicate<T> p in preds)
{
if (!(p(e)))
{
pass = false;
break;
}
}
if (pass) data.Add(e);
}
return data;
}
}
The FindAll method returns list elements that satisfy all the
specified predicates.
public static List<T> FindAll<T>(this List<T> vals, List<Predicate<T>> preds)
The FindAll method takes a list of generic predicate functions as
a parameter. It returns a filtered generic list.
var preds = new List<Predicate<int>>();
preds.Add(e => e > 0);
preds.Add(e => e % 2 == 0);
var vals = new List<int> {-3, -2, -1, 0, 1, 2, 3, 4};
var filtered = vals.FindAll(preds);
foreach (var e in filtered)
{
Console.WriteLine(e);
}
Console.WriteLine("---------------------");
var words = new List<string> {"sky", "wrath", "wet", "sun", "pick", "who",
"cloud", "war", "water", "jump", "ocean"};
var preds2 = new List<Predicate<string>>();
preds2.Add(e => e.StartsWith("w"));
preds2.Add(e => e.Length == 3);
var filtered2 = words.FindAll(preds2);
foreach (var e in filtered2)
{
Console.WriteLine(e);
}
We define two lists: an integer list and a string list. From the integer list, we filter out all positive even values. From the string list, we get all words that start with 'w' and have three letters.
$ dotnet run 2 4 --------------------- wet who war
C# generic constraints
The where clause in a generic definition specifies constraints on
the types that are used as arguments for type parameters in a generic type or
method.
| Constraint | Description |
|---|---|
| where T : struct | T must be a non-nullable value type |
| where T : class | T must be a reference type |
| where T : notnull | T must be a non-nullable type |
| where T : unmanaged | T must be a non-nullable unmanaged type |
| where T : new() | T must have a public parameterless constructor |
| where T : BaseClass | T must be or derive from the specified base class |
| where T : IInterface | T must implement the specified interface |
| where T : U | T must be or derive from the type parameter U |
The table lists the most common constraints.
var shelter = new Shelter<Cat>();
shelter.Add(new Cat("Lucky"));
shelter.Add(new Cat("Missy"));
shelter.Show();
var kennel = new Shelter<Dog>();
kennel.Add(new Dog("Rex"));
kennel.Show();
// var garden = new Shelter<Flower>(); // compile-time error
interface IAnimal
{
string Name { get; }
string Sound();
}
record Cat(string Name) : IAnimal
{
public string Sound() => "meow";
}
record Dog(string Name) : IAnimal
{
public string Sound() => "woof";
}
record Flower(string Name);
class Shelter<T> where T : IAnimal
{
private readonly List<T> animals = [];
public void Add(T animal) => animals.Add(animal);
public void Show()
{
foreach (var animal in animals)
{
Console.WriteLine($"{animal.Name} says {animal.Sound()}");
}
}
}
In the example, we define a Shelter class that restricts the
T type to types implementing the IAnimal interface.
Thanks to the constraint, we can access the Name property and call
the Sound method inside the generic class.
// var garden = new Shelter<Flower>(); // compile-time error
The Flower type does not implement IAnimal; if we
uncomment the line, the compiler reports the CS0311 error.
$ dotnet run Lucky says meow Missy says meow Rex says woof
In the next example, we use an interface constraint to write a generic
Max method.
Console.WriteLine(Max(3, 7));
Console.WriteLine(Max("falcon", "eagle"));
Console.WriteLine(Max(new DateOnly(2026, 1, 1), new DateOnly(2025, 12, 31)));
T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) > 0 ? a : b;
}
The method works with all types that implement the
IComparable<T> interface. We compare integers, strings, and
dates.
$ dotnet run 7 falcon 1/1/2026
C# generic list iterator
The following example creates a generic iterator.
var words = new List<string> { "sky", "cloud", "rock", "war", "web" };
var it = CreateIterator(words);
string? e;
while ((e = it()) != null)
{
Console.WriteLine(e);
}
Iterator<T> CreateIterator<T>(IList<T> data) where T : class
{
var i = 0;
return delegate { return (i < data.Count) ? data[i++] : null; };
}
public delegate T? Iterator<T>() where T : class;
An iterator uses an anonymous method. Each call returns the next element of the
list; when there are no more elements, it returns null. The
class constraint is needed, because only reference types can be
null. The anonymous method captures the i variable,
which keeps the current position.
$ dotnet run sky cloud rock war web
C# generic list forEach
The following example creates a generic forEach method for a list container.
Action<int> show = Console.WriteLine;
var vals = new List<int> { 1, 2, 3, 4, 5, 6, 7 };
forEach(vals, show);
Console.WriteLine("--------------------------");
var words = new List<string> { "sky", "cloud", "rock", "water" };
forEach(words, Console.WriteLine);
Console.WriteLine("--------------------------");
var users = new List<User>
{
new ("John Doe", "gardener"),
new ("Roger Roe", "driver"),
};
forEach(users, Console.WriteLine);
void forEach<T>(IList<T> vals, Action<T> fn)
{
foreach (T val in vals)
{
fn(val);
}
}
record User(string Name, string Occupation);
The generic forEach method goes over integer, string, and
User elements of three list containers.
void forEach<T>(IList<T> vals, Action<T> fn)
The forEach method takes a generic list and a generic
Action delegate as parameters.
$ dotnet run
1
2
3
4
5
6
7
--------------------------
sky
cloud
rock
water
--------------------------
User { Name = John Doe, Occupation = gardener }
User { Name = Roger Roe, Occupation = driver }
C# generic dictionary ForEach
In the next example, we create a generic ForEach method for
a dictionary.
var domains = new Dictionary<string, string>
{
{"sk", "Slovakia"},
{"ru", "Russia"},
{"de", "Germany"},
{"no", "Norway"}
};
domains.ForEach((k, v) => Console.WriteLine($"{k} - {v}"));
var vals = new Dictionary<int, string>
{
{1, "coin"},
{2, "pen"},
{3, "pencil"},
{4, "book"}
};
vals.ForEach((k, v) => Console.WriteLine($"{k} - {v}"));
static class DictionaryExtension
{
public static void ForEach<T1, T2>(this Dictionary<T1, T2> dict,
Action<T1, T2> fn) where T1 : notnull
{
foreach (KeyValuePair<T1, T2> pair in dict)
{
fn(pair.Key, pair.Value);
}
}
}
The ForEach method is defined as an extension method. We use the
foreach loop to go over the pairs of the dictionary and call the
Action delegate for each of them. The notnull
constraint is required, because dictionary keys cannot be null.
$ dotnet run sk - Slovakia ru - Russia de - Germany no - Norway 1 - coin 2 - pen 3 - pencil 4 - book
Source
In this article we have covered generic programming in C#.
Author
List all C# tutorials.