Codecademy Logo

C# Generics

Generic Methods

In C#, generic methods can specify the type of data they take as parameters, or return. They take their type parameters whenever they are called. The type parameter can be used as the return type, or for the parameter, or both.

This is distinctly different from a method in a generic class that uses the class’s own type parameters. In contrast, a generic method requires its type parameter to be specified whenever the method is called. This means that, in any class, a generic method can use a different data type whenever it is called.

// Declaring a generic method.
T GenericMethod<T> (T value) { return value; }
// Calling a generic method.
int x = GenericMethod<int> (10);

Constraints

In C#, constraints are used to restrict the types that can be used as arguments for a generic type parameter. They are declared with the where keyword after specifying the type parameter: class myClass<T> where T : constraints.

// define a generic class with a constraint.
class GenericClass<T> where T : class { }
// define a generic interface with a constraint.
interface IGenericInterface<T> where T : struct { }
// define a generic method with a constraint.
T GenericMethod<T> where T : new() (T value) { }

Covariance and Contravariance

In C#, covariance and contravariance allow for implicit reference conversion for generic type parameters.

Covariance means we can use a more derived type than originally specified. If the Derived class is derived from Base, then an instance of Interface<Derived> can be assigned to a variable of type Interface<Base>. Covariant type parameters are used only in an interface’s return types.

Contravariance means we can use a less derived type than originally specified. With the same classes above, this would mean we can assign Interface<Base> to a variable of type Interface<Derived>. Contravariant type parameters are only used in an interface’s parameter types.

// Interface with contravariant type parameter.
interface ISerializer<in T> { string GetJSON (T input); }
// Interface with a covariant type parameter.
interface IDeserializer<out T> { T ReadJSON (string input); }
// If DerivedClass is derived from BaseClass, the following are legal:
ISerializer<DerivedClass> v1 = new ISerializer<BaseClass>()
IDeserializer<BaseClass> v2 = new IDeserializer<DerivedClass>()

Multiple Constraints

In C#, you can apply multiple constraints to a single generic type parameter by combining various types of constraints. Constraints are separated by commas after the where T :.

// Declaring a class with multiple constraints.
class GenericClass<T> where T : class, IComparable<T>, new() { }

class Constraint

In C#, the class constraint specifies that the type argument must be a reference type, such as a class, interface, delegate or array type. Trying to insatiate the generic class, interface, or method with some other type will throw an exception.

// Defining a class with the class constraint.
class GenericClass<T> where T : class { }

struct Constraint

In C#, the struct constraint specifies that the type argument must be a value type, such as bool, int, or a struct. Trying to instantiate the generic class, interface, or method with some other type will cause an exception.

// Declaring a class with a struct constraint.
class GenericClass<T> where T : struct { }

new() Constraint

In C#, the new() constraint specifies that the type argument must have a parameterless constructor. If you attempt to instantiate the generic class, interface, or method with a type whose constructor requires parameters, it will throw an exception.

// Declaring a class with a new() constraint.
class GenericClass<T> where T : new() { }

Base Class Constraint

In C#, a base class constraint ensures that the type argument inherits from a specific base class. If you attempt to instantiate the generic class, interface, or method with a type not derived from the base class, it will throw an exception.

// Declaring a class with a base class constraint.
class GenericClass<T> where T : List { }

Interface Constraint

In C#, an interface constraint ensures that the type argument implements a specific interface. If you try to instantiate the generic class, interface, or method with a type that is not derived from the interface, it will throw an exception.

// Defining a class with an interface constraint.
class GenericClass<T> where T : IList { }

Multiple Constraint Limitations

In C# when using multiple type constraints there are some limitations:

  • struct and class constraints must be first.
  • new() must be last.
  • Only one base class constraint is allowed.
  • The struct and new() constraints can’t be combined.
// Legal combinations of constraints.
class GenericClass1<T> where T : class, new() { }
class GenericClass2<T> where T : IComparable<T>, IEquatable <T>, new() { }
class GenericClass3<T> where T : struct, IEquatable <T> { }
class GenericClass4<T> where T : List, new() { }
// Illegal combinations of constraints
class BadGenericClass1<T> where T : List, List<T> { }
class BadGenericClass2<T> where T : new(), IComparable<T>, IEquatable <T> { }
class BadGenericClass3<T> where T : struct, new() { }

Generics

In C#, generics enable the creation of classes, methods, and interfaces that can operate with any data type. They are defined with a type parameter and are instantiated with a specific data type.

// Defining a genric class
class GenericClass<T> { }
// Instantiating a generic class
GenericClass<int> x = new GenericClass<int>;

Generic collections

In C#, generics are often used for collection classes and interfaces. The System.Collections.Generic namespace offers many built in classes and interfaces for dealing with generic collections. Examples include:

  • List<T>
  • Dictionary<TKey,TValue>
  • Queue<T>
  • Stack<T>
  • SortedList<TKey,TValue>
List<int> myList = new Lint<int>();
int x = 10;
myList.Add(x);

Type parameters

In C#, generic type parameters are specified using angle brackets (<...>). The type parameter goes after the name of the generic class, interface, or method.

// Defining a genric class
class GenericClass<T> { }
// Instantiating a generic class
GenericClass<int> x = new GenericClass<int>;

C# Generic Classes

Generic classes in C# ensure type safety by allowing developers to define the specific data types they operate on. This approach minimizes runtime errors and enhances code reusability. Generics serve as powerful tools for creating flexible and type-safe code structures, accommodating various data types while reducing redundancy.

Generic Methods in Non-Generic Classes.

In C#, generic methods can be declared within non-generic classes. Because the method declares its own type parameters, it can be defined within any class, not just a generic class.

// A non-generic class with generic methods.
class SerializeMe {
public static string GetJSON<T> (T input) { /* serialization code goes here */ };
public static T ReadJSON<T> (string input) { /* deserialization code goes here */ };
}

Default Keyword

The default keyword in C# is used to return a default value of a parameterized type. This is useful when it’s unknown if the type parameter could be a value type or a reference type.

// Example, returning a default value when an error is thown.
Public T ReadJSON(string input) {
try {
/* deserialization code here */
}
catch (Exception e) {
return default(T);
}
}

Generic Interfaces

In C#, generic interfaces define methods and properties that can operate on different data types. They are defined just like generic classes and use the type parameter for method return types and parameters. They can be implemented in a generic class or implemented in a non-generic class when the type parameter is specified.

// Define a generic interface.
interface IGenericInterface<T> {
T ReturnValue(T value);
}
// Implemented in a generic class.
class myGenericClass<T> : IGenericInterface<T> {
T ReturnValue(T value) { return value; }
}
// Implemented in a non-generic class
class myNonGenericClass : IGenericInterface<int> {
int ReturnValue(int value) { return value; }
}

Learn more on Codecademy