Now that we've seen LINQ in action and discussed generic collections, it's time to explain what all that strange<T>
syntax was about. Let's learn about generics!
A generic in C# is a type that uses objects of a different type. Said different type is not specified until an instance of the generic object is created. We can identify a generic type by looking for the syntax <T>
, where T is a placeholder that represents the type being used by the generic.
public class MyGeneric<T> { /*...*/ }
//Elsewhere
var myGeneric = new MyGeneric<int>();
var myOtherGeneric = new MyGeneric<OtherClass>();
We have already seen generics in use with LINQ and collections. Here, we will dive deeper into what these types can do for us developers, how to create both generic classes and generic methods, and how to create constraints.
The Sample Solution
Creating a Generic
The most common use for generic types is to create collection classes. For example, let's say we wanted to implement a custom collection class called StackQueue
which implements methods for both the Stack
class and the Queue
class provided by C#.
public class StackQueue<T> { /*...*/ }
T is the common placeholder for a type that is not defined yet.
A Queue
has methods for Enqueue()
and Dequeue()
, and a Stack
has methods for Push()
and Pop()
. Since Dequeue()
and Pop()
are the same operation (remove the element at the front of the list) our StackQueue
class will implement Enqueue()
, Push()
, and Pop()
.
public class QueueStack<T>
{
private List<T> elements = new List<T>();
//Insert at "back of line" or bottom of list
public void Enqueue(T item)
{
Console.WriteLine("Queueing " + item.ToString());
elements.Insert(elements.Count, item);
}
//Insert at "front of line" or top of list
public void Push(T item)
{
Console.WriteLine("Pushing " + item.ToString());
elements.Insert(0, item);
}
//Remove the element at the top of the list
public T Pop()
{
var element = elements[0];
Console.WriteLine("Popping " + element.ToString());
elements.RemoveAt(0);
return element;
}
}
For the Enqueue()
and Push()
methods, the placeholder T is used as the type of a parameter. For the Pop()
method, T is used as the return type.
Because StackQueue
is a generic class, we can put off specifying the type that it will operate on until an instance of it is created:
var myStackQueue = new StackQueue<int>(); //T is now int
myStackQueue.Enqueue(1);
myStackQueue.Push(2);
myStackQueue.Push(3);
myStackQueue.Enqueue(4); //At this point, the collection is { 3, 2, 1, 4 }
var firstValue = myStackQueue.Pop(); //3
var secondValue = myStackQueue.Pop(); //2
We will get a build error if we attempt to Push()
any object which is not of the type specified by our StackQueue
instance:
Generic Methods
Instead of creating an entire generic class, we can implement a generic method for situations where a generic class might be more than what we need.
public static class SwapMethods
{
public static void Swap<T>(ref T first, ref T second)
{
T temp;
temp = first;
first = second;
second = temp;
}
}
public static void Test()
{
int a = 5;
int b = 3;
Swap<int>(ref a, ref b);
Console.WriteLine(a + " " + b); //Output: 3 5
}
Constraints
It is possible to tell the C# compiler to only allow certain types in a generic type; this is called a constraint. For example, we could restrict our StackQueue
class to only be usable on reference types:
public class StackQueue<T> where T : class { /*...*/ }
If we then tried to create an instance of StackQueue
with a value type we get an error.
There are many kinds of constraints we can use, and they work for all generic types including generic classes and generic methods. Let's see a few of them.
Specific Class or Interface
We can constrain a generic class to use a specific class:
public class StackQueue<T> where T : OtherClass { /*...*/ }
Due to inheritance, this instance of StackQueue
will also accept instances of any classes which, in turn, inherit from OtherClass
.
We can also constrain generics to use types which implement a specific interface:
public class StackQueue<T> where T : ISomeOtherInterface { /*...*/ }
Public Parameterless Constructor
We can constrain a generic class to only use types which have a public parameterless constructor.
public class StackQueue<T> where T : new() { /*...*/ }
You may remember from the class basics post that any class which does not implement a custom constructor will automatically have a public parameterless constructor.
Nullables
We can constrain generic types into using either a specific type or a null
instance using the nullable syntax:
public class StackQueue<T> where T : class? { /*...*/ } //Will accept
//a class or null
Why Use Constraints?
Constraints are about expectations. By creating a constraint, we are creating an expectation on any instance of the generic type that said instance must follow the rules of the constraint.
The primary reason we use constraints is to force compilation errors when we accidentally use a generic type for something other than what it is meant for. That way, we get these errors immediately, as opposed to waiting for some unknown condition at runtime to cause them.
Glossary
- Generic - a C# type that uses other types. The type being used will be defined when an instance of the generic type is created. Uses the syntax
<T>
. - Constraint - Used to restrict the kinds of types that can be used in a generic type.
Summary
Generics are types in C# which can use other types in their implementation. The type being used is not specified until an instance of the generic type is created. Generic types are often used for collections. We most often create generic classes or generic methods.
We can place constraints on generic types to restrict the kinds of types they can use. Restrictions might include using a specific class or any classes which inherit from it, using types with a public parameterless constructor, or using nullable types. Constraints work on all generic types.
In the next post in this series, we will discuss two advanced types that C# allows us to use: tuples and anonymous types. Check that out here:
Happy Coding!