Welcome!

.NET Authors: Liz McMillan, Yakov Werde, Matthew Pollicove , Kevin Benedict

Related Topics: .NET

.NET: Article

DNDJ Feature — Introducing C# Generics

Leverage code reuse without sacrificing type safety

Reusing Code with Generics
By using generics Evan can get the benefit of type safety at build time without having to duplicate code. Let's see how with a code fragment showing the major changes needed. The full code for generic List3<T> is found in Listing 2.

    public class List3<T>
    {
      internal static T NO_ITEM = default(T);
      internal class ListNode
      {
        public ListNode next = null;
        public T obj = NO_ITEM;
      }
      // ...

      public virtual void Add(T obj)
      // ...

      public virtual T Remove()
      // ...

With the syntax <T> immediately after the class name, we indicate a type parameter, T, which stands in place of some actual type that's specified at each location where a new List3 is used. In fact, we won't call this class List3 but rather List3<T>.

Within the class itself the type parameter T is used as if it were a real type (like our old String or Object types). The C# build time and runtime will conspire to use this generic class, List3<T>, as a template that can be used to generate as many "regular" classes as needed for each actual type that a program specifies to take the place of the type parameter T.

Here's how Evan can refactor the string manipulation code to use the generic class List3<T>.

    List3<String> aList = new List3<String>();
    aList.Add("a");
    aList.Add("b");
    for (Object item = aList.Remove();
      item != List3<String>.NO_ITEM;
      item = aList.Remove())
    {
      // Do something with item
    }

Note that where we want to use List3<T> we must specify what the type parameter really is - in this case String. The C# runtime will create a class for List3<String>, unless it's already done so. You don't need explicit casting because the Remove() method for List3<String> returns a String. At build time we have the advantage of full type checking that prevents entering the wrong type of object into the collection.

Where the same code could operate identically on many types, using generics elegantly solves the tension of how to write reusable code while maintaining type safety.

Back to the Bakery
Back at the bakery, Evan uses the generic List3<T> with Strings, Customer objects, and BakeItem objects:

    List3<BakeItem> waitingForOven = new List3<BakeItem>();
    List3<BakeItem> inOven = new List3<BakeItem>();
    List3<BakeItem> onDisplay = new List3<BakeItem>();

With three collections the software is modeling the process as bakery items are queued for the oven, cooked, and then put on display. To facilitate some cooking experiments, the baker asks Evan to track how many of a particular bakery item were in the oven at the same time. Each bakery item should maintain the maximum and minimum number of items in the oven during its cook time.

First, Evan creates an IWatermark interface and implements that interface in the BakeItem class. The IWatermark interface is as follows and the BakeItem class that implements this interface is shown in Listing 3.

    public interface IWatermark
    {
      int HighMark { get; set; }
      int LowMark { get; set; }
    }

The bakery items that are in the oven are in a List3<BakeItem> collection in a variable called inOven. If this collection knew that it held items that implement the IWatermark interface then it could invoke the required instrumentation during the additions and removals of items in the collection.

Having looked at the documentation for C# generics, Evan considers placing a constraint on a generic parameter type. Evan considers turning List3<T> into an intrusive container that makes use of the properties available via IWatermark. It would look like this:

    public class List3<T> where T : Iwatermark

However, this would require implementing IWatermark on all of the classes List3<T> contains, including Customer types and String types. This would also prevent List3<T> from using primitive types. A more viable approach could be to alter List3<T> to test internally if the type that it holds implements IWatermark and if so then record the necessary statistics. We could make use of C#'s is operator in the implementation of List3<T> to test type compatibility dynamically for any T with IWatermark as follows:

    public virtual void Add(T obj)
    {
      if (obj is IWatermark) {
        // Do stuff with IWatermark methods
    }

Unfortunately this approach is undesirable. It adds useless code in the general case where List3<T> is used with a type that doesn't implement IWatermark. Another pitfall of this approach is that any object that implements IWatermark will be instrumented in any List3<T> that it's put in, not just the collection that's being used to represent the oven. Finally, this strategy hides the intrusive nature of List3<T> and thus can hide type mismatch errors.

Leaving the List3<T> as general as possible, Evan implements List4<T> with a constraint that it requires anything put in it to implement IWatermark. See Listing 4 for the full watermarking collection. Here's the first line:

public class List4<T> : List3<T> where T : Iwatermark

A generic class can be used as a base class so Evan chose to specialize List4<T> from List3<T> but added a constraint that List4<T> requires all items put in it to implement IWatermark. The generic class List4<T> is explicit about its intrusive nature on the objects it holds and C#'s type safety provisions come into play at build time and runtime to prevent it from operating on inappropriate types. Here's the modified code snippet:

    List3<BakeItem> waitingForOven = new List3<BakeItem>();
    List4<BakeItem> inOven = new List4<BakeItem>();
    List3<BakeItem> onDisplay = new List3<BakeItem>();

Delving Deeper
Through illustration we introduced generic classes and how to put a constraint on a generic type parameter.

C# allows for generic classes, structs, interfaces, delegates, static methods, and instance methods. Any of these can be parameterized based on type and we're not limited to a single type parameter. For example, we could have a generic class Several<T, U, V> where each type parameter could be the same or unrelated.

Several<int, int, String> a = new Several<int, int, String>();

You can use generic methods to define algorithms that are clearly separated from the data type on which they operate. A generic method specifies type parameters following the method name:

public U DoSomething<U, V>(U u, V v)

Both parameters and return type can be made generic and the type parameters don't have to be the same as type parameters on the class. The C# compiler can do type inferencing on method parameters so that, for the above method, instead of coding DoSomething<String, int>("name", 2) you can simply type DoSomething("name", 2).

The compiler infers the generic type parameters from the data types of the method parameters. Properties and indexers don't allow for introducing new type parameters but they can use any type parameters of the class that contains them. Generic methods support constraints on their type parameters like classes.


More Stories By Robert R. Hauser

Robert R. Hauser has a masters degree in computer science specializing in Artificial Intelligence with more than 15 years of development experience in C, C++, C#, and Java, building software for networking, filesystems, middleware, and natural language processing. He works at Recursion Software, which is focused on providing a next generation application platform for .NET and Java.

Comments (0)

Share your thoughts on this story.

Add your comment
You must be signed in to add a comment. Sign-in | Register

In accordance with our Comment Policy, we encourage comments that are on topic, relevant and to-the-point. We will remove comments that include profanity, personal attacks, racial slurs, threats of violence, or other inappropriate material that violates our Terms and Conditions, and will block users who make repeated violations. We ask all readers to expect diversity of opinion and to treat one another with dignity and respect.