Quick Links

In C#, the "T" parameter is often used to define functions that take any kind of type. They're used to write generic classes and methods that can work with any kind of data, while still maintaining strict type safety. We'll discuss how they work, and how to use them.

How Does <T> Work?

The "T" variable you've probably seen in some method definitions is called a Generic type parameter, or simply just a "Generic." Generic methods that use T can be used with any type, making it easy to define classes and methods that don't care about the data they're handling but want to preserve it.

For example, collections make use of generics, so that they can handle whatever the user throws at it. There isn't a different definition for 

        List<string>
    

 and

        List<int>
    

; instead, there's one definition for

        List<T>
    

.

In practice, that looks like the following. Even though this is a class and not a method, you can still pass it type parameters using the

        <T>
    

 bracket syntax. Then, anywhere you need to reference something of this type, you simply substitute the parameter instead.

Pass type parameters using &lt;T&gt; bracket syntax.

Putting anything in the

        <>
    

 brackets allows you to use that name in place of a valid type, anywhere in the definition of the class or method that is using that parameter. If you imagine a

        GenericList<int>
    

, anywhere you'd write

        int
    

 as a type you would instead write

        T
    

, and leave it up to the user to tell this class which type to use.

It's really that simple. For a

        GenericList<string>
    

, the code above is functionally equivalent to writing the following, though it should be noted that this definition doesn't include

        GenericList<string>
    

, because everything in the brackets is type parameters, and primitives can't be used as type names. For a class that doesn't use any parameters, like this one, it's defined as usual with no brackets.

A class that doesn't use any parameters is defined with no brackets.

You can actually name this T variable anything, although it's common practice to at least start it with "T." If you have a function that needs multiple-type arguments, you can name them differently, like "TOutput" or "TInput." This is used a lot in delegate definitions, and in Dictionaries where you have TKey and TValue.

Bad naming practice.

Of course, you can also use generics in methods, as well as interfaces and delegates. They work the same way, and you can even pass the type parameter as a type parameter to another function.

Use generics in methods.

Notably though, you can use the type parameter in the actual parameters of the function. You'll still need to put it in

        <>
    

 brackets, otherwise it will just be an invalid type, but you can use the parameter anywhere in the definition of this method.

Use type parameter in the actual parameters of the function

They're particularly useful in delegates, because you can use them to accept functions with variable parameters.

Type Constraints

Generics are great, but it can cause some issues when the function is allowed to take any possible type you throw at it. Sometimes, it's best to put a few restrictions on usage.

This is done with the

        where T :
    

 syntax. The simplest form of this is

        where T : ClassName
    

, which ensures that the T parameter must be or derive from the given type

        ClassName
    

. This enables type-safe polymorphism, such as this function that takes any kind of Fruit, and returns a

        List<Orange>
    

, rather than a

        List<Fruit>
    

, which would be technically correct but lose valuable type information.

 Type-safe polymorphism

You can see that if we try to use this function with anything that isn't a Fruit, the compiler will yell at you.

 If you use this function with anything that isn't a Fruit, the compiler will comment

Beyond simple inheritance, there are a few more useful constraints:

  •         where T : InterfaceName
        
     - like
            T : ClassName
        
     but makes sure the type argument implements the given interface.
  •         where T : class
        
     - ensures the type argument is a reference type.
  •         where T : struct
        
     - ensures the type argument is a non-nullable value type.
  •         where T : notnull
        
     - the type argument must a non-nullable type.
  •         where T : new()
        
     - the type argument must be able to be constructed without parameters.
  •         where T : TOther
        
     - the type argument
            T
        
     must be, or derive from, the type argument
            TOther
        
    .

You can specify multiple constraints in a comma-separated list.