In this post I will talk about another nifty feature that C# has offered for a while now, Generics. You probably won't run into Generics on a day to day basis. If you work alot with data collections however, you might find them useful at some point. I've probably personally used Generics twice on my own personal websites, and they come in extremely handy and can save alot of time and help avoid writing too much repetitive code. If you're familiar with C++, Generics are somewhat comparable to Templates in the old STL.
What are Generics?
With Generics you can postpone setting parameter types to a function or class until that function or class has been instantiated by the client. So for example, let's say that we have a function that performs a generic operation on a set of parameters being passed in.
public class SampleClass
{
public void DoWork(int data)
{
// do something with the data, anything
}
}
That's pretty straightforward. But now let's assume that we want to have the same operation run but instead of an integer, it takes in a decimal value.
public class SampleClass
{
public void DoWork(int data)
{
// do something with the data
}
public void DoWork(decimal data)
{
// do same thing with that data
}
}
Many times you'll see something like that above done, and it works, not a problem. But then maybe you'll need to do the same for strings and then for objects, etc, and it can quickly get out of hand. So this is where Generics come into play. By using Generics you can simply add a placeholder parameter to your parameter list, and when calling the method, you can specify the type that you wish to work with.
public class SampleClass
{
public void DoWork(T data)
{
// do something with the data
}
public void Run()
{
DoWork<int>(234);
DoWork<string>("hello world");
}
}
Assuming that the DoWork function performs a generic task and can accommodate different data types, we just simply need to specify the type using the notation and the corresponding values.
If you've used classes in the Collections namespace, like the List then you're already familiar with Generics. The syntax to declare a new List of type int is the following:
List<int> list = new List<int>();
Working with classes
It doesn't just apply to methods however, we can also overload classes and their properties. This would make more sense when working with a collection of different types, like queues and stacks. In this way we can create collections of strings or integers, or whatever other type we want.
public class SomeClass<T>
{
T data;
public void DoWork()
{
// do some work on the 'T' data
}
}
Same kind of logic as with methods. In order to initialize an instance of the class with different types, we'd do something like the following:
SomeClass obj = new SomeClass();
SomeClass obj2 = new SomeClass();
Add some constraints
Constraints apply restrictions to the kinds of types that client code can use for type arguments when it instantiates your class. If client code tries to instantiate your class by using a type that is not allowed by a constraint, the result is a compile-time error.
where T: struct - The type argument must be a value type. Any value type except Nullable can be specified. See Using Nullable Types (C# Programming Guide) for more information.
where T : class -
The type argument must be a reference type; this applies also to any class, interface, delegate, or array type.
where T : new() -
The type argument must have a public parameterless constructor. When used together with other constraints, the new() constraint must be specified last.
where T : <base class name> -
The type argument must be or derive from the specified base class.
where T : <interface name> -
The type argument must be or implement the specified interface. Multiple interface constraints can be specified. The constraining interface can also be generic.
where T : U -
The type argument supplied for T must be or derive from the argument supplied for U.
The syntax for using a where clause with Generics is as follows:
public class GenericList where T : class
{
}
The class above would only accept a type parameter that is a reference type. This is definitely an important feature in Generics, particularly when you know that some types must be limited, and also just a good programming habit to make your code more readable.
The default keyword
One small issue with using Generics is that it is impossible to assign a variable a default value if we do not yet know of its type. If it's a reference type we can set its value to null, and if its an integer then 0 is your value. Lucky for us, Generics bring with it the default keyword.
public T GetLast()
{
// The value of temp is returned as the value of the method.
// The following declaration initializes temp to the appropriate
// default value for type T. The default value is returned if the
// list is empty.
T temp = default(T);
Node current = head;
while (current != null)
{
temp = current.Data;
current = current.Next;
}
return temp;
}
A simple concept overall, but one that can save you alot of work if done correctly. It's also good to note that you shouldn't overdo it either. Sometimes having data types specifically stated makes reading the code must faster and less cringe worthy. Seeing random T's and U's all over the place might make some developers do a double take.