C# Generic Class Templates Tutorial
In this tutorial we learn about generic class templates and how they allow us to define placeholders for their member types.
We also cover how to constrain generics to certain types.
What are generics
Simply put, C# Generics are class templates. Generics allow us to define placeholders for the types of its members.
How to define a generic
We define a generic class by using open and close angular brackets after the class name. In between the brackets, we specify the type placeholder.
The placeholder will be substituted with an actual type when we implement the generic.
class/struct Identifier<T>
{
T varIdentifier;
T genericMethod(T genericParameter)
{
// method body
return genericParameter;
}
T genericProperty { get; set; }
}
In the example above, we use T as the type.
using System;
namespace Generics
{
class Program
{
static void Main(string[] args)
{
Logger<string> textLog = new Logger<string>();
textLog.ConsoleLog("Logger with a string type");
Logger<int> numLog = new Logger<int>();
numLog.ConsoleLog(01001000); // binary H
Console.ReadLine();
}
}
class Logger<T>
{
public void ConsoleLog(T toLog)
{
Console.WriteLine(toLog);
}
}
}
In the example above, we created a simple class with a method that prints something to the console. We don’t know what that something is when we create the class, the person using the generic class can specify what that something is with T.
The concept becomes clear in the Main() function. Our textLog object is of type
Similarly, our
How to set a parameter as default
We can use the default keyword to set a generic parameter to its default value. It’s useful because a generic type doesn’t know the placeholders up front, and cannot safely assume what the default value should be.
Defaults are:
- Numeric Value types are defaulted to 0
- Reference types are defaulted to null
identifier = default(T);
using System;
namespace Generics
{
class Program
{
static void Main(string[] args)
{
Logger<string> textLog = new Logger<string>();
textLog.ConsoleLog();
Logger<int> numLog = new Logger<int>();
numLog.ConsoleLog();
Console.ReadLine();
}
}
class Logger<T>
{
public void ConsoleLog(T toLog = default(T))
{
Console.WriteLine(toLog);
}
}
}
In the example above, we tell the compiler to give the generic method a default value according to its type.
When the method is used without any input, the default value is printed.
How to constrain a generic to a type
To further improve on type safety, we can constrain the generic to only certain types.
class/struct Identifier<T> where T : type
{
generic body
}
using System;
namespace Generics
{
class Program
{
static void Main(string[] args)
{
// reference type - compiler error
Logger<string> textLog = new Logger<string>();
textLog.ConsoleLog();
// value type
Logger<int> numLog = new Logger<int>();
numLog.ConsoleLog();
Console.ReadLine();
}
}
class Logger<T> where T : struct
{
public void ConsoleLog(T toLog = default(T))
{
Console.WriteLine(toLog);
}
}
}
In the example above, we told the compiler to check if the type of T is a value type (struct). Because int is a value type, it will work fine. However, string is not a value type so the compiler will raise an error.
If we changed the struct (value types) to class (reference types), the compiler will be happy with string, but complain about int.
using System;
namespace Generics
{
class Program
{
static void Main(string[] args)
{
// reference type
Logger<string> textLog = new Logger<string>();
textLog.ConsoleLog();
// value type - compiler error
Logger<int> numLog = new Logger<int>();
numLog.ConsoleLog();
Console.ReadLine();
}
}
class Logger<T> where T : class
{
public void ConsoleLog(T toLog = default(T))
{
Console.WriteLine(toLog);
}
}
}
The following table shows the types of generic constraints:
where T: struct | T must be value type. |
where T: class | T must be reference type. |
where T: new() | T must have a default constructor. |
where T: BaseClassName | T must be derived from the BaseClass. |
where T: InterfaceName | T must implement the InterfaceName. |
How to set multiple constraints
We can have multiple constraints within our generics.
Identifier<T, U> where T : type1 where U : type2
The two types between the angle brackets, are separated by a comma. The two where modifiers are not separated by any special characters.
using System;
namespace Generics
{
class Program
{
static void Main(string[] args)
{
Logger<int, string> textLog = new Logger<int, string>();
textLog.ConsoleLog(1, "Hello World");
Console.ReadLine();
}
}
class Logger<T, U> where T : struct where U : class
{
public void ConsoleLog(T index = default(T), U toLog = default(U))
{
Console.WriteLine(index + ". " + toLog);
}
}
}
In the example above, we use two types in our Logger class. When we create the object, we need to input both types.
Constraints on methods
Generic constraints aren’t limited to the class or struct only, we can constrain a method as well.
class/struct Class<T>
{
access type Method<U>(U parameter) where U : type
{
// method body
}
}
The method needs its own type, specified between the angle brackets.
using System;
namespace Generics
{
class Program
{
static void Main(string[] args)
{
Logger<string> textLog = new Logger<string>();
textLog.ConsoleLog(DateTime.Now);
Console.ReadLine();
}
}
class Logger<T>
{
public void ConsoleLog<U>(U toLog) where U : struct
{
Console.WriteLine(toLog);
}
}
}
In the example above, we have a Logger class with type T, and a ConsoleLog() method with type U where the U must be a struct (value) type.
Summary: Points to remember
- Generics are class templates that allow us to define member type placeholders.
- We can substitute the type placeholders for our own when instantiating a generic class.
- The default keyword is used to set a generic parameter to a default value.
- A generic can be constrained to one or more types to increase type safety.
- Both classes and functions can be constrained.