Custom Software Apps & SharePoint Consulting

C# Generics Summary: Methods, Types and Specifying Generic Type Parameter Constraints

Generics in C# are classes, structures, interfaces, and methods that have type parameters for one or more of the types that they use. For example, instead of using a collection class that allows any type, we can use a generic collection class and define the specific type(s) for the collection. The primary use case of generics is indeed to create type-safe generic collections and enumerators. Generics can also be used in lambda expressions, delegates and utility code.

There are a number of benefits of using generics. First, it promotes code reusability by writing code that is independent of the type(s) used for generic type parameter(s). Performance over object collections is improved because no runtime casts or boxing conversions need to be performed. Lastly, generics promote type safe programming and allow type inference to determine type parameters without requiring explicit specification.

Lambda Expressions and Func<>

A Lambda Expression is an anonymous function that contains an expression or statements is essentially a shorter way of writing a delegate. The Func<T, TResult> delegates use type parameters to define how many and, if necessary, the type(s) of input parameters and the return type of the delegate. Following are a few examples below on how lambda expressions are used in .NET:

  1. The following code: Clicked += delegate(object sender, EventArgs args) { DoSomething(); } Can be replaced with: Clicked += (sender, args) => { DoSomething(); }
  2. It is used frequently in LINQ: ordersTable.Where(order => order.IsPending); ordersTable.Select(order =>     new { order.OrderID, order.Name, order.IsPending } );

Note that even though arguments are not specified, they remain type-safe. The types are simply inferred based on program information available to the compiler.

Constraints

When defining a generic class, you can apply restrictions to what types can be used as the arguments. These restrictions are called Constraints and applied by using where keyword. It is used to guarantee that a call to this method conforms to these constraints, and thus access the functionality defined by the constraints. These are the different types of constraints:

  1. Reference/Value type constraint: The type argument must be a value type (struct) or a reference type (class). This constraint must be specified first.
    • Table<TEntity> where TEntity : class
    • Nullable<T> where T : struct
  2. Base class and interface constraints: For the base class constraint, the type argument must be equivalent to the base class or derive from it. For interface constraints, the type argument must implement the specified interface. The base class constraint must be specified first.
    • Foo<T> where T : BaseFoo, IDisposable
  3. Constructor constraint: The type argument must have a public parameterless constructor. This constraint must be specified last.
    • Foo<T> where T : new()
  4. Naked constraint: In the example below, the type argument D must be equivalent to T or derive from it.
    • AddRange<T, D> (this List<T> list, IEnumerable<D> collection )     where D : T
  5. Recursive constraint: The type parameter may be used as an argument in its own constraints.
    • Foo<T> where T : IComparable<T>
  6. Specifying constraints for multiple parameters: A separate where clause is used for each type parameter.
    • Foo<S, T> where S : class, T, new() where T : IComparable<T>

Share this post with your friends

Skip to content