Article Index
C# Generics for Beginners - Part 1
C# Generics for Beginners - Part 2
Introduction
In part 1, we covered type parameters. The example generic class we built did not do a lot. Basically all it did was store values. Now lets see how to build some logic into a generic class.
What the compiler knows.
To understand generics, you need to keep in mind what the compiler knows, and what it cannot possibly know.
Consider the following generic class:
public class GenericDemo<T>
{
public void Foo(T Value)
{
}
}
There is only one thing the compiler knows for certain about the Value parameter: it is of type Object. Whoever creates an instance of this generic class can pass through ANY type as the type parameter. The only common thing between all the possibilities is that they must be of type Object.
The implication of this is that inside the Foo method, we can only do things with the Value parameter we can do with an object.
Check out the documentation for object here:
https://msdn.microsoft.com/en-us/library/system.object(v=vs.110).aspx
As you can see what you can do with Value is limited.
So how can we provide the compiler with more information about the T type parameter in this example? We can tell the compiler more about the type parameter by adding constraints.
For reference. here is the .Net documentation for type parameter constraints: https://msdn.microsoft.com/en-us/library/d5x73970.aspx
So lets add a constraint.
public class GenericDemo<T> where T : IComparable<T>
{
public T GetBiggerValue(T value1, T value2)
{
if (value1.CompareTo(value2) >= 0) return value1;
return value2;
}
}
The "where T : iComparable<T>" bit on the first line, is telling the compiler that the type parameter T must implement the IComparable<T> interface. And since value1, and value2 are both of type T, the CompareTo method is now available to be called, where previously it was not.
We can call the GetBiggerValue method like so:
var biggestString = GenericDemo<string>.GetBiggerValue("b", "a");
var biggestInt = GenericDemo<int>.GetBiggerValue(123, 456);
We are allowed to pass int, and string as type parameters, because they both implement IComparable.
This does NOT compile:
var doesNotWork = GenericDemo<char[]>.GetBiggerValue("a".ToCharArray(), "b".ToCharArray());
The reason it does not compile, is because the type char[] does NOT implement IComparable, and the constraint we have added requires all type parameters passed through to implement IComparable.
Other Constraint Types
Lets take a quick look at the other constraint types.
Parameterless Constructor Constraint
public class HasParameterlessConstructor
{
public HasParameterlessConstructor() { }
}
public class DoesNotHaveParameterlessConstructor
{
public DoesNotHaveParameterlessConstructor(int parameter1, string paramter2) { }
}
public class GenericDemo<T> where T : new()
{
public static T CreateNewInstance()
{
var newInstance = new T();
return newInstance;
}
}
The constraint in the example above is "where T : new()". This tells the compiler that the type T, must have a parameterless constructor. In the example you will see two classes as well called HasParameterlessConstructor and DoesNotHaveParameterlessConstructor.
Here is a demonstration of this constraint in action:
var compiles = GenericDemo<HasParameterlessConstructor>.CreateNewInstance();
var alsoCompiles = GenericDemo<List<int>>.CreateNewInstance();
var doesNotCompile = GenericDemo<DoesNotHaveParameterlessConstructor>.CreateNewInstance();
The first two lines both compile since HasParameterlessConstructor and List<int> both have a parameterless constructor. This means that we can create a new instance of T by calling "new T()", like we do inside the CreateNewInstance method.
The third line, does NOT compile, because DoesNotHaveParameterlessConstructor has no constructors that take no parameters. "new T()" is impossible for DoesNotHaveParameterlessConstructor. You can only create an instance of DoesNotHaveParameterlessConstructor by calling new T(1,"a"), since its constructor takes an int, and a string, and it has no other constructors.
Reference and Value type Constraints
See this article for more information on reference and value types.
https://msdn.microsoft.com/en-us/library/t63sy5hs.aspx?f=255&MSPPError=-2147217396
The 'class' constraint in the following example tells the compiler that T must be a reference type.
public class GenericDemo<T> where T : class, IComparable<T>
{
public static T GetBiggerValue(T value1, T value2)
{
if (value1.CompareTo(value2) >= 0) return value1;
return value2;
}
}
The 'struct' constraint in the following example tells the compiler that T must be a value type.
public class GenericDemo<T> where T : struct, IComparable<T>
{
public static T GetBiggerValue(T value1, T value2)
{
if (value1.CompareTo(value2) >= 0) return value1;
return value2;
}
}
Basic Example
Lets build a rudimentary example of a generic class that uses constraints, that has some practical use.
What this example does is allows you to add jobs to a collection, and they will then be executed in order of priority.
public interface IJob
{
int GetPriority();
void Execute();
}
public class JobRunner<T> where T : IJob, IComparable<T>
{
private List<T> _jobList = new List<T>();
private class JobComparer : IComparer<T>
{
public int Compare(T x, T y)
{
return x.CompareTo(y);
}
}
public void AddJob(T job)
{
_jobList.Add(job);
}
public void ExecuteJobs()
{
var sortedByPriority = _jobList.OrderByDescending(x => x, new JobComparer());
foreach (var job in sortedByPriority)
{
job.Execute();
}
}
}
public class WriteToConsoleJob : IJob, IComparable<IJob>
{
private int _priority;
private string _toWrite;
public WriteToConsoleJob(int priority, string toWrite)
{
_toWrite = toWrite;
_priority = priority;
}
public int GetPriority()
{
return _priority;
}
public void Execute()
{
Console.WriteLine(_toWrite);
}
public int CompareTo(IJob other)
{
return this.GetPriority().CompareTo(other.GetPriority());
}
}
This generic class can be used like so:
var jobRunner = new JobRunner<WriteToConsoleJob>();
var highPriorityJob = new WriteToConsoleJob(1000, "high priority job");
var lowPriorityJob = new WriteToConsoleJob(50, "low priority job");
var mediumPriorityJob = new WriteToConsoleJob(500, "medium priority job");
jobRunner.AddJob(lowPriorityJob);
jobRunner.AddJob(highPriorityJob);
jobRunner.AddJob(mediumPriorityJob);
jobRunner.ExecuteJobs();
The output on the console will be:
high priority job
medium priority job
low priority job
This line:
public class JobRunner<T> where T : IJob, IComparable<T>
Tells the compiler that T must implement both IJob, as well as IComparable. IJob allows GetPriority() and Execute() to be called on instances of type T. IComparable allows CompareTo to be called. So you can create your own jobs that do anything you want, as long as it implements both IJob, and IComparable.
Conclusion
We now understand what type parameter constraints do. The knowledge you have now should allow you to build your own generic classes. It should also allow you to understand most C# that use generics you come across.