Jump to:
What is a Generic?
Generics in OOP allow us to define a specification of a class or method that can be used with any data type. When we design a generic, the data types of the method parameters or class isn't known - not until it is called or instantiated.
Defining a Generic
A generic class or method can be defined using angle brackets < > and an identifier. The standard identifier used for generics is a capital letter T i.e. <T>
but any letter can be used - even a word. If it makes sense to use an identifier other than T, feel free. It's not a rule, just a guideline that most programmers follow.
public static void Method<T>(T param1) { }
We can also use multiple generic types if needed, typically the second one is denoted as <U>
.
public static void Method<T, U>(T param1, U param2) { }
Generic Method
See the below example of an overloaded method that swaps two integers or strings values via reference.
using System;
public class Maths {
public static void Swap(ref int a, ref int b) {
int temp = a;
a = b;
b = temp;
}
public static void Swap(ref string a, ref string b) {
string temp = a;
a = b;
b = temp;
}
}
public class Program {
public static void Main() {
int twenty = 20;
int thirty = 30;
string forty = "forty";
string fifty = "fifty";
Maths.Swap(ref twenty, ref thirty);
Console.WriteLine("twenty = {0}", twenty);
Console.WriteLine("thirty = {0}", thirty);
Maths.Swap(ref forty, ref fifty);
Console.WriteLine("forty = {0}", forty);
Console.WriteLine("fifty = {0}", fifty);
}
}
Output:
twenty = 30
thirty = 20
forty = fifty
fifty = forty
Works like a charm, of course. But what if we added another overload for floats? And another for booleans. And another for class objects. This overloaded method is getting big. Now what if we needed to make a fundamental change to all of the overloads? Wouldn't it be nicer and easier to just have one method to rule them all?
using System;
public class Maths {
public static void Swap<T>(ref T a, ref T b) {
T temp = a;
a = b;
b = temp;
}
}
public class Program {
public static void Main() {
int twenty = 20;
int thirty = 30;
string forty = "forty";
string fifty = "fifty";
Maths.Swap<int>(ref twenty, ref thirty);
Console.WriteLine("twenty = {0}", twenty);
Console.WriteLine("thirty = {0}", thirty);
Maths.Swap<string>(ref forty, ref fifty);
Console.WriteLine("forty = {0}", forty);
Console.WriteLine("fifty = {0}", fifty);
}
}
Output:
twenty = 30
thirty = 20
forty = fifty
fifty = forty
Instead of two separate overloaded methods, we can trim it down to just one. The major difference is the <T>
right after the method name and replacing the data type for the parameters and temp variable with T
as well.
When we call the method from the Main program, we can specify the data type for T. In the first part, we're passing integers so the method is called as Swap<int>(...)
instead of just Swap(...)
. In the second part, we do the same thing but since we're passing strings, the method is called as Swap<string>(...)
.
Although, we can actually omit specifying the type when calling the generic method because the compiler will infer the data type from the arguments being passed.
// This works too!
Maths.Swap(ref twenty, ref thirty);
Maths.Swap(ref forty, ref fifty);
Generic Class
Classes can be generic along the same lines of thinking. A common use for a generic class is for defining collectios of items, where adding & removing are performed in the same way regardless of data type.
One common generic class is a collection called a stack, where items are pushed (added to the top of the stack) and popped (removed from the top of the stack). This is referred to as LIFO (Last In, First Out) since the item that is removed on a pop operation is teh last one that was recently added.
While the below example is not the Stack<> class exactly (as defined in the .Net Framework), it should show what we're talking about in regards to a use of a generic class.
using System;
public class Stack<T> {
readonly int max_size;
int index = 0;
public int size { get; set; }
T[] items = null;
public Stack() : this(100) { } // default max_size of 100
public Stack(int size) {
max_size = size;
items = new T[max_size];
}
// Push - Adds item to the top of the stack
public void Push(T item) {
if (size >= max_size)
throw new StackOverflowException();
items[index++] = item; // adds item in next spot
size++; // increments actual size
}
// Pop - returns item from top and then removes it from the stack
public T Pop() {
if (size <= 0)
throw new InvalidOperationException("Stack is empty, cannot Pop");
return items[--index]; // decrements by 1 and returns value at index
}
// Peek - returns the last item in the listt
public T Peek() {
return items[index - 1];
}
// Get - returns the value of the specified index
public T Get(int idx) {
return items[idx];
}
}
public class Program
{
public static void Main() {
Stack<string> StarWars = new Stack<string>();
StarWars.Push("The Phantom Menace");
StarWars.Push("Attack of the Clones");
StarWars.Push("Revenge of the Sith");
StarWars.Push("Rogue One");
StarWars.Push("Solo");
StarWars.Push("A New Hope");
StarWars.Push("The Empire Strikes Back");
StarWars.Push("Return of the Jedi");
StarWars.Push("The Force Awakens");
StarWars.Push("The Last Jedi");
StarWars.Push("The Rise of Skywalker");
Console.WriteLine("Size of Stack: {0}", StarWars.size);
Console.WriteLine(StarWars.Peek()); // The Rise of Skywalker
Console.WriteLine(StarWars.Pop()); // The Rise of Skywalker
Console.WriteLine(StarWars.Pop()); // The Last Jedi
Console.WriteLine(StarWars.Peek()); // The Force Awakens
Console.WriteLine(StarWars.Get(5)); // A New Hope
Console.WriteLine("Size of Stack: {0}", StarWars.size);
}
}
Output:
Size of Stack: 11
The Rise of Skywalker
The Rise of Skywalker
The Last Jedi
The Force Awakens
A New Hope
Size of Stack: 9
Remember the Coords struct?
struct Coords {
public int x;
public int y;
// constructor
public Coords(int p1, int p2) {
x = p1;
y = p2;
}
}
Use a Generic struct (yes, structs too!) instead and it's no longer limited to integers.
using System;
struct Coords<T> {
public T X;
public T Y;
}
public class Program
{
public static void Main()
{
Coords<int> intCoords;
intCoords.X = 1;
intCoords.Y = 2;
Console.WriteLine("intCoords = {0}, {1}", intCoords.X, intCoords.Y);
Coords<float> floatCoords;
floatCoords.X = 1.23f;
floatCoords.Y = 4.56f;
Console.WriteLine("floatCoords = {0}, {1}", floatCoords.X, floatCoords.Y);
Coords<string> stringCoords;
stringCoords.X = "Over there";
stringCoords.Y = "A bit closer";
Console.WriteLine("stringCoords = {0}, {1}", stringCoords.X, stringCoords.Y);
}
}
Output:
intCoords = 1, 2
floatCoords = 1.23, 4.56
stringCoords = Over there, A bit closer
Constraints
We can also place a restriction on a generic to only accept a certain type, such as a struct, class or specific class/interface.
A constraint is implemented on a generic by using the where keyword followed by the generic type variable (e.g. T), a colon, and the constraint type.
There are 6 types of constraints:
where T : struct | β | Type argument must be a value type |
where T : class | β | Type argument must be a reference type |
where T : new() | β | Type argument must have a public parameterless constructor |
where T : | β | Type argument must inherit from class |
where T : | β | Type argument must implement from interface |
where T : U | β | There are two type arguments T and U. T must be inheritted from U. |
Maybe the above example with the Coords struct doesn't make sense for its X & Y values to be strings. Let's add a constraint to it.
struct Coords<T> where T : struct {
public T X;
public T Y;
}
Neat! But why are we constraining with a struct? The table above shows that using a struct constraint will restrict the type argument to value types. Since strings are reference types, this should allow this struct to not accept a string. It will; however, accept a single character though. Unfortunately a generic cannot be constrained to individual data types but this is close enough to demonstrate a constraint.
using System;
struct Coords<T> where T : struct {
public T X;
public T Y;
}
public class Program
{
public static void Main()
{
Coords<int> intCoords;
intCoords.X = 1;
intCoords.Y = 2;
Console.WriteLine("intCoords = {0}, {1}", intCoords.X, intCoords.Y);
Coords<float> floatCoords;
floatCoords.X = 1.23f;
floatCoords.Y = 4.56f;
Console.WriteLine("floatCoords = {0}, {1}", floatCoords.X, floatCoords.Y);
Coords<string> stringCoords;
stringCoords.X = "Over there";
stringCoords.Y = "A bit closer";
Console.WriteLine("stringCoords = {0}, {1}", stringCoords.X, stringCoords.Y);
}
}
This would result in an error on the line instantiating stringCoords.
Compilation error (line 22, col 12): The type 'string' must be a non-nullable value type in order to use it as parameter 'T' in the generic type or method 'Coords<T>'
Collections
As mentioned above, there are collections of generic classes like Stack<>. Another common one is List<>.
List<> | Function | |
---|---|---|
.Add() | β | Adds an item at the end of the list |
.Clear() | β | Removes all items from the list |
.Contains() | β | Checks for an item; returns a boolean |
.Count | β | Returns the number of items in the list |
.Insert() | β | Adds an item at a specified index |
Below is a small example of using the List<> collection.
using System;
using System.Collections.Generic;
public class Program
{
public static void Main()
{
List<string> colors = new List<string>();
colors.Add("Red");
colors.Add("Green");
colors.Add("Blue");
colors.Remove("Green");
Console.WriteLine("# of Colors: {0}", colors.Count);
foreach (var color in colors) {
Console.WriteLine(color);
}
}
}
Output:
# of Colors: 2
Red
Blue
.Net has other several built-in collection classes in the System.Collections.Generic namespace, such as Queues, LinkedLists, Dictionaries, etc. The types of collections available in this namespace is fairly diverse so if one of those will provide the functionality of your needs, use them! However if it will make sense to create your own generic collection, go right ahead.
Benefits
- Reusability: A single generic type definition can be used for multiple purposes with the same code without any alterations. It's much easier to maintain one version of the method/class than multiple overloads.
- Type Safety: Generic data types provide better type safety, especially when used with collections since we need to specify the type of objects that are passed to them. Type checking is done at complile time instead of run-time, allowing bugs to be caught before release.
- Performance: Methods & classes utilizing generics provide better performance since they reduce the need for boxing/unboxing (conversion from value type to object and vice versa), and typecasting of the objects used. The actual code for data typed versions of a generic is done on demand instead of multiple typed versions that may not be used.
Awesome stuff!
Generics are a powerful feature of C# that allows us to create more versatile data structures and methods. Using generics can provide great abstraction and extensibility, allowing for code reuse, type-safety, and performance gains. Finding where to use them can take some practice and forethought as it may not be readily apparent. No problem though! Keep the concepts in mind for refactoring opportuntities at the least.
Top comments (0)