Generic programming is a powerful tool that C# offers to developers. In this article, we’ll take a look at how generics work in C# and some of the benefits they provide. We’ll also see how to use generics with classes, methods, and delegates. Finally, we’ll look at some of the drawbacks of using generics and ways to work around them.
While we go in-depth in this article with generics, we do have an introductory article on Generics if this is a new topic to you.
Let’s start!
What Are Generics in C#?
In C#, Generics is a concept that makes it possible for programmers to implement classes and methods that defer the specification of their types until the classes and methods are instantiated by the client code.
For example, a generic class might be created for handling customer data, with specific methods for adding, removing, and updating customer records. In this case, the compiler generates the code to handle the type it encounters as we instantiate the class.
Why Use Generics?
Generic classes and methods are essential as they make it possible for programmers to enjoy benefits such as reusability, type safety, and efficiency, unlike their non-generic counterparts.
This makes them ideal for use with collections and methods which utilize them to implement business logic.
In C#, the System.Collections.Generic
namespace contains generic-based collection classes. However, we can also create generic custom types to implement our generalized solutions and design patterns that enjoy the benefits of type safety and efficiency.
How Do Generics Work in C#?
To understand how generics work in C#, we are going to implement some generic classes and methods.
First, let’s start by creating a simple generic class to help us manipulate generic array objects:
public class GenericArray<T> { private T[] _arrayObj; public GenericArray(int size) { _arrayObj = new T[size + 1]; } public T RetrieveValue(int index) { return _arrayObj[index]; } public void InsertValue(int index, T value) { _arrayObj[index] = value; } }
Here, we define a generic class GenericArray
of type T
, which helps us define custom methods to help us manipulate array objects. We denote the generic type as T
since we use it as a placeholder for the type that the compiler encounters at runtime. However, we need to specify the type when instantiating the generic class.
In the class, we define a generic type variable _arrayObj
that holds the array values and a constructor to instantiate the class. Next, we define two public methods to insert and retrieve array elements by accepting the index of the array and the generic value as inputs.
We can then proceed to verify that we can generate arrays of different types:
var intArray = new GenericArray<int>(5); var guidArray = new GenericArray<Guid>(5); var rand = new Random(); for (int i = 0; i < 5; i++) { intArray.InsertValue(i, rand.Next()); } for (int i = 0; i < 5; i++) { guidArray.InsertValue(i, Guid.NewGuid()); } Assert.IsInstanceOfType(intArray.RetrieveValue(1), typeof(int)); Assert.IsInstanceOfType(guidArray.RetrieveValue(1), typeof(Guid));
Here, we can see that we can use the GenericArray
class to instantiate array objects of different types, which the compiler determines at runtime. Generics provide compile-time type safety as the solution would not build successfully when we attempt to insert a value of a different type such as a string
into the intArray
object.
Generic Methods
Next, let’s create generic methods with type parameters to understand how they work. To keep things simple, let’s write a method to swap elements given two generic type variables:
public void SwapElements(ref T left, ref T right) { T tempVar; tempVar = left; left = right; right = tempVar; }
This method accepts two generic type variables we pass by reference and swaps them by using a tempVar
variable to facilitate the swap process.
We can verify that the SwapElements()
method works with different data types at runtime using this unit test:
var firstInt = 1; var lastInt = 9; var firstChar = 'a'; var lastChar = 'z'; var integerObj = new GenericMethods<int>(); var charObj = new GenericMethods<char>(); integerObj.SwapElements(ref firstInt, ref lastInt); charObj.SwapElements(ref firstChar, ref lastChar); Assert.AreEqual(firstInt, 9); Assert.AreEqual(lastInt, 1); Assert.AreEqual(firstChar, 'z'); Assert.AreEqual(lastChar, 'a');
Generic Delegates
We can use generics when we want to utilize delegates in C#. We are aware that delegates help us hold references to methods and access them without using their actual names.
However, we also know that when we have many methods and need to encapsulate them using delegates, we need to define a delegate for each method. Creating many delegates may end up making our codebases difficult to maintain and reducing application performance.
Generic delegates come in handy as we can use the same delegate to hold references to multiple methods. To make this concept easy to understand, let’s define two methods to add and multiply two numbers and an additional method to print the results:
public int AdditionFunc(int num1, int num2) { return num1 + num2; } public int MultiplicationFunc(int num1, int num2 ) { return num2 * num1; } public void PrintString(string stringVal) { Console.WriteLine(stringVal); }
Both AdditionFunc
and MultiplicationFunc
take two integer values and return their sum and product respectively.
Next, we are going to define a generic delegate in the Main()
method to test these methods:
delegate T ArithmeticDelegates<T>(T num1, T num2); static void Main(string[] args) { var delegateObj = new GenericDelegates(); var addition = new ArithmeticDelegates<int>(delegateObj.AdditionFunc); var multiplication = new ArithmeticDelegates<int>(delegateObj.MultiplicationFunc); var num1 = 5; var num2 = 10; var printOutput = $"The sum of {num1} and {num2} is {addition(num1, num2)}"; delegateObj.PrintString(printOutput); printOutput = $"The multiplication of {num1} and {num2} is {multiplication(num1, num2)}"; delegateObj.PrintString(printOutput); }
Let’s understand how the code works.
First, we define a generic delegate ArithmeticDelegates
that accepts two generic type input parameters and returns one generic output type parameter.
In the next step, we create the delegateObj
object of GenericDelegates
type and pass the AdditionFunc()
and MultiplicationFunc()
methods to our delegate.
Finally, to verify that all the methods work as expected, we create two number variables and print the results of both the Addition and Multiplication methods.
Features of Generics
After implementing generic classes and methods in our example, we learn that generics enrich our programs in multiple ways.
First, it helps us maximize the principles of code reusability, type safety, and performance.
Besides that, the technique allows us to create generic collection classes, which are available in the System.Collections.Generic
namespace.
The technique also helps us define generic interfaces, classes, methods, events, and delegates.
Another way generics enrich our programs is their ability to help us enable access to methods that require specific data types.
Finally, the generics help us get information on the types used in a generic data type at compile-time, through the reflection technique.
Advantages of Generics
From our examples, we can see that we can reuse our code snippets without modifying them thanks to generics. This makes our code base simple to understand and modify when the need arises.
Besides reusability, we learn that we benefit from type safety as we need to specify the types of objects we intend to pass to our generic methods and classes. Passing objects of different types to the same generic object results in a compilation error.
Finally, generic types have been known to perform better than their standard type system counterparts since the former types don’t require boxing, unboxing, and type casting.
Disadvantages of Generics
First, we cannot use generics with lightweight dynamic methods as generics are determined at compile time while dynamics are resolved at runtime.
Besides that, we cannot use generic type parameters with enumerations. Generic enumerations can only occur in special circumstances such as nesting it in a generic type.
On top of that, .NET does not support context-bound generic types. It means we can derive generic types from ContextBoundObject
, but trying to create an instance of that type causes a TypeLoadException
.
Finally, we can’t instantiate nested types that are enclosed in generic types unless we assign them to the type parameters of all enclosing types.
Conclusion
In this article, we have learned how generics work in C#. They come in handy when we want to maximize code reuse, type safety, and performance. They make it possible for us to define our own generic interfaces, classes, methods, events, and delegates.