C# Programming/Generics

From Wikibooks, open books for an open world
Jump to: navigation, search

Generics are a new feature available since version 2.0 of the C# language and the common language runtime (CLR). Generics introduce to the .NET Framework the concept of type parameters, which make it possible to design classes and methods that defer the specification of one or more types until the class or method is declared and instantiated by client code. The most common use of generics is to create collection classes. Generic types were introduced to maximize code reuse, type safety, and performance.[1]

Generic classes[edit]

There are cases when you need to create a class to manage objects of some type, without modifying them. Without generics, the usual approach (highly simplified) to make such class would be like this:

public class SomeObjectContainer
{
    private object _obj;
 
    public SomeObjectContainer(object obj)
    {
        this._obj = obj;
    }
 
    public object GetObject()
    {
        return this._obj;
    }
}

And its usage would be:

class Program
{
    static void Main(string[] args)
    {
        SomeObjectContainer container = new SomeObjectContainer(25);
        SomeObjectContainer container2 = new SomeObjectContainer(5);
 
        Console.WriteLine((int) container.GetObject() + (int) container2.GetObject());
        Console.ReadKey(); // wait for user to press any key, so we could see results
    }
}

Note that we have to cast back to original data type we have chosen (in this case - int) every time we want to get an object from such a container. In such small programs like this, everything is clear. But in more complicated cases with more containers in different parts of the program, we would have to take care that the container is supposed to have int type in it and no other data type, as in such a case, a InvalidCastException is thrown.

Additionally, if the original data type we have chosen is a value type, such as int, we will incur a performance penalty every time we access the elements of the collection due to the autoboxing feature of C#.

However, we could surround every unsafe area with a try - catch block, or we could create a separate "container" for every data type we need just to avoid casting. While both ways could work (and worked for many years), it is unnecessary now, because generics offers a much more elegant solution.

To make our "container" class to support any object and avoid casting, we replace every previous object type with some new name, in this case T, and add <T> mark immediately after the class name to indicate that this T type is generic/any type.

Note: You can choose any name and use more than one generic type for class, i.e <genKey, genVal>.
public class GenericObjectContainer<T>
{
    private T _obj;
 
    public GenericObjectContainer(T obj)
    {
        this._obj = obj;
    }
 
    public T getObject()
    {
        return this._obj;
    }
}

Not a big difference, which results in simple and safe usage:

class Program
{
    static void Main(string[] args)
    {
        GenericObjectContainer<int> container = new GenericObjectContainer<int>(25);
        GenericObjectContainer<int> container2 = new GenericObjectContainer<int>(5);
        Console.WriteLine(container.getObject() + container2.getObject());
 
        Console.ReadKey(); // wait for user to press any key, so we could see results
    }
}

Generics ensures that you specify the type for a "container" once, avoiding previously mentioned problems and autoboxing for structs.

While this example is far from practical, it does illustrate some situations where generics are useful:

  • You need to keep objects of a single type in a class
  • You do not need to modify objects
  • You need to manipulate objects in some way
  • You wish to store a "value type" (such as int, short, string, or any custom struct) in a collection class without incurring the performance penalty of autoboxing every time you manipulate the stored elements.

Generic interfaces[edit]

A generic interface accepts one or more type parameters, similar to a generic class:

public interface IContainer<T>
{
    T GetObject();
    void SetObject(T value);
}
 
public class StringContainer : IContainer<string>
{
    private string _str;
 
    public string GetObject()
    {
        return _str;
    }
 
    public void SetObject(string value)
    {
        _str = value;
    }
}
 
public class FileWithString : IContainer<string>
{
    ...
}
 
class Program
{
    static void Main(string[] args)
    {
        IContainer<string> container = new StringContainer();
 
        container.SetObject("test");
 
        Console.WriteLine(container.GetObject());
        container = new FileWithString();
 
        container.SetObject("another test");
 
        Console.WriteLine(container.GetObject());
        Console.ReadKey();
    }
}

Generic interfaces are useful when multiple implementations of a particular class are possible. For example, both the List<T> class (discussed below) and the LinkedList<T> class, both from the System.Collections.Generic namespace, implement the IEnumerable<T> interface. List<T> has a constructor that creates a new list based on an existing object that implements IEnumerable<T>, and so we can write the following:

LinkedList<int> linkedList = new LinkedList<int>();
 
linkedList.AddLast(1);
linkedList.AddLast(2);
linkedList.AddLast(3);
// linkedList now contains 1, 2 and 3.
 
List<int> list = new List<int>(linkedList);
 
// now list contains 1, 2 and 3 as well!

Generic methods[edit]

Generic methods are very similar to generic classes and interfaces:

using System;
using System.Collections.Generic;
 
public static bool ArrayContains<T>(T[] array, T element)
{
    foreach (T e in array)
    {
        if (e.Equals(element))
        {
            return true;
        }
    }
 
    return false;
}

This method can be used to search any type of array:

using System;
using System.Collections.Generic;
 
class Program
{
    static void Main(string[] args)
    {
        string[] strArray = { "string one", "string two", "string three" };
        int[] intArray = { 123, 456, 789 };
 
        Console.WriteLine(ArrayContains<string>(strArray, "string one")); // True
        Console.WriteLine(ArrayContains<int>(intArray, 135)); // False
    }
}

Type constraints[edit]

One may specify one or more type constraints in any generic class, interface or method using the where keyword. The following example shows all of the possible type constraints:

public class MyClass<T, U, V, W>
    where T : class,        // T should be a reference type (array, class, delegate, interface)
        new()               // T should have a public constructor with no parameters
    where U : struct        // U should be a value type (byte, double, float, int, long, struct, uint, etc.)
    where V : MyOtherClass, // V should be derived from MyOtherClass
        IEnumerable<U>      // V should implement IEnumerable<U>
    where W : T,            // W should be derived from T
        IDisposable         // W should implement IDisposable
{
    ...
}

These type constraints are often necessary to

  1. create a new instance of a generic type (the new()) constraint
  2. use foreach on a variable of a generic type (the IEnumerable<T> constraint)
  3. use using on a variable of a generic type (the IDisposable constraint)

Notes[edit]