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.

Here's a table of contents of what you'll learn in this lesson:
(click on a link to skip to its section)

Let's jump right in.

What are generics

C# Generics are, basically, 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.

Syntax:
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.

Example:
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 , which then allows us to use a string in the ConsoleLog() method.

Similarly, our type allows us to use an int in the ConsoleLog() method.

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
Syntax:
identifier = default(T);
Example:
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.

Syntax:
class/struct Identifier<T> where T : type
{
	generic body
}
Example:
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.

Example:
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: structT must be value type.
where T: classT must be reference type.
where T: new()T must have a default constructor.
where T: BaseClassNameT must be derived from the BaseClass.
where T: InterfaceNameT must implement the InterfaceName.

How to set multiple constraints

We can have multiple constraints within our generics.

Syntax:
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.

Example:
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.

Syntax:
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.

Example:
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.