WindowsDevCenter.com
oreilly.comSafari Books Online.Conferences.

advertisement


AddThis Social Bookmark Button

Generics in .NET 2.0
Pages: 1, 2

Generics and Delegates

Delegates can be generics as well. This provides quite a bit of flexibility.

Assume we are interested in writing a framework. We need to provide a mechanism for an event source to talk to an object that is interested in the event. Our framework may not be able to control what the events are. You may be dealing with a stock price change (double price). I may be dealing with temperature change in a boiler (temperature value), where Temperature may be an object that has some information such as value, units, threshold, and so on. How can I define an interface for these events?

Let's take a look at how we can realize this by using pre-generic delegates:


    public delegate void NotifyDelegate(Object info);

    public interface ISource
    {
        event NotifyDelegate NotifyActivity;
    }

We have the NotifyDelegate accepting an Object. This is the best we could do in the past, as Object can be use to represent different types such as double, Temperature, and so on, though it involves boxing overhead for value types. ISource is an interface that different sources will support. The framework exposes the NotifyDelegate delegate and the ISource interface.

Let's look at two different sources:


    public class StockPriceSource : ISource
    {
        public event NotifyDelegate NotifyActivity;
        //…
    }

    public class BoilerSource : ISource
    {
        public event NotifyDelegate NotifyActivity;
        //…
    }

If we have an object of each of the above classes, we would register a handler for events, as shown below:


            StockPriceSource stockSource = new StockPriceSource();
            stockSource.NotifyActivity 
                        += new NotifyDelegate(
                                stockSource_NotifyActivity);

            // Not necessarily in the same program… we may have
            BoilerSource boilerSource = new BoilerSource();
            boilerSource.NotifyActivity 
                        += new NotifyDelegate(
                                boilerSource_NotifyActivity);

In the delegate handler methods, we would do something like the following:

For the handler for stock event, we would have:


void stockSource_NotifyActivity(object info)
{
        double price = (double)info; 
        // downcast required before use
}

The handler for the temperature event may look like this:


void boilerSource_NotifyActivity(object info)
{
        Temperature value = info as Temperature; 
        // downcast required before use
}

The above code is not intuitive, and is messy with the downcasts. With generics, the code is more readable and easier to work with. Let's take a look at the code with generics at work:

Here is the delegate and the interface:


    public delegate void NotifyDelegate<t>(T info);

    public interface ISource<t>
    {
        event NotifyDelegate<t> NotifyActivity;
    }

We have parameterized the delegate and the interface. The implementor of the interface can now say what the type should be.

The Stock source would look like this:


    public class StockPriceSource : ISource<double>
    {
        public event NotifyDelegate<double> NotifyActivity;
        //…
    }

and the Boiler source would look like this:


    public class BoilerSource : ISource<temperature>
    {
        public event NotifyDelegate<temperature> NotifyActivity;
        //…
    }

If we have an object of each of the above classes, we would register a handler for events, as shown below:


            StockPriceSource stockSource = new StockPriceSource();
            stockSource.NotifyActivity 
                += new NotifyDelegate<double>(
                        stockSource_NotifyActivity);

            // Not necessarily in the same program… we may have
            BoilerSource boilerSource = new BoilerSource();
            boilerSource.NotifyActivity 
                += new NotifyDelegate<temperature>(
                        boilerSource_NotifyActivity);

Now, the event handler for stock price would be:


        void stockSource_NotifyActivity(double info)
        {
                //…
        }

and the event handler for the temperature is:


        void boilerSource_NotifyActivity(Temperature info)
        {
                //…
        }

This code has no downcast and the types involved are very clear.

Generics and Reflection

Since generics are supported at the CLR level, you may use reflection API to get information about generics. One thing may be a bit confusing when you are new to generics: you have to keep in mind that there is the generics class you write and then there are types created from it at runtime. So, when using the reflection API, you have to make an extra effort to keep in mind which type you are dealing with. I illustrate this in the Example 7:

Example 7. Reflection on generics


    public class MyClass<t> { }

    class Program
    {
        static void Main(string[] args)
        {
            MyClass<int> obj1 = new MyClass<int>();
            MyClass<double> obj2 = new MyClass<double>();

            Type type1 = obj1.GetType();
            Type type2 = obj2.GetType();

            Console.WriteLine("obj1's Type");
            Console.WriteLine(type1.FullName);
            Console.WriteLine(
                type1.GetGenericTypeDefinition().FullName);

            Console.WriteLine("obj2's Type");
            Console.WriteLine(type2.FullName);
            Console.WriteLine(
                type2.GetGenericTypeDefinition().FullName);
        }
    }

I have an instance of MyClass<int>. I ask for the class name of this instance. Then I ask for the GenericTypeDefinition() of this type. GenericTypeDefinition() will return the type metadata for MyClass<T> in this example. You may call IsGenericTypeDefinition to ask if this is a generic type (like MyClass<T>) or if its type parameters have been specified (like MyClass<int>). Similarly, I query an instance of MyClass<double> for its metadata. The output from the above program is shown below:


obj1's Type
TestApp.MyClass`1
    [[System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral, 
    PublicKeyToken=b77a5c561934e089]]
TestApp.MyClass`1
obj2's Type
TestApp.MyClass`1
    [[System.Double, mscorlib, Version=2.0.0.0, Culture=neutral, 
    PublicKeyToken=b77a5c561934e089]]
TestApp.MyClass`1

We can see that MyClass<int> and MyClass<double> are classes that belong to the mscorlib assembly (dynamically created), while the class MyClass<t> belongs to my assembly.

Generics' Limitations

We have seen the power of generics so far in this article. Are there any limitations? There is one significant limitation, which I hope Microsoft addresses. In expressing constraints, we can specify that the parameter type must inherit from a class. How about specifying that the parameter must be a base class of some class? Why do we need that?

In Example 4, I showed you a Copy() method that copied contents of a source List to a destination list. I can use it as follows:


List<Apple> appleList1 = new List<Apple>();
List<Apple> appleList2 = new List<Apple>();
…
Copy(appleList1, appleList2);

However, what if I want to copy apples from one list into a list of Fruits (where Apple inherits from Fruit). Most certainly, a list of Fruits can hold Apples. So I want to write:


List<Apple> appleList1 = new List<Apple>();
List<Fruit> fruitsList2 = new List<Fruit>();
…
Copy(appleList1, fruitsList2);

This will not compile. You will get an error:


Error 1 The type arguments for method 
'TestApp.Program.Copy<t>(System.Collections.Generic.List<t>, 
System.Collections.Generic.List<t>)' cannot be inferred from the usage. 

The compiler, based on the call arguments, is not able to decide what T should be. What I really want to say is that the Copy should accept a List of some type as the first parameter, and a List of the same type or a List of its base type as the second parameter.

Even though there is no way to say that a type must be a base type of another, you can get around this limitation by still using the constraints. Here is how:


public static void Copy<T, E>(List<t> source, 
                                List<e> destination) where T : E

Here I have specified that the type T must be the same type as, or a sub-type of, E. We got lucky with this. Why? Both T and E are being defined here. We were able to specify the constraint (though the C# specification discourages using E to define the constraint of T when E is being defined as well).

Consider the following example, however:


    public class MyList<t>
    {
        public void CopyTo(MyList<t> destination)
        {
            //…
        }
    }

I should be able to call CopyTo:


MyList<apple> appleList = new MyList<apple>();
MyList<apple> appleList2 = new MyList<apple>();
//…
appleList.CopyTo(appleList2);

I must also be able to do this:


MyList<apple> appleList = new MyList<apple>();
MyList<fruit> fruitList2 = new MyList<fruit>();
//…
appleList.CopyTo(fruitList2);

This, of course, will not work. How can we fix this? We need to say that the argument to CopyTo() can be either MyList of some type or MyList of the base type of that type. However, the constraints do not allow us to specify the base type. How about the following?


public void CopyTo<e>(MyList<e> destination) where T : E

Sorry, this does not work. It gives a compilation error that:


Error 1 'TestApp.MyList<t>.CopyTo<e>()' does not define type 
parameter 'T'

Of course, you may write the code to accept MyList of any arbitrary type and then within your code, you may verify that the type is one of acceptable type. However, this pushes the checking to runtime, losing the benefit of compile-time type safety.

Conclusion

Generics in .NET 2.0 are very powerful. They allow you to write code without committing to a particular type, yet your code can enjoy type safety. Generics are implemented in such a way as to provide good performance and avoid code bloat. While there is the drawback of constraints' inability to specify that a type must be a base type of another type, the constraints mechanism gives you the flexibility to write code with a greater degree of freedom than sticking with the least-common-denominator capability of all types.

.NET Gotchas

Related Reading

.NET Gotchas
75 Ways to Improve Your C# and VB.NET Programs
By Venkat Subramaniam

Venkat Subramaniam founder of Agile Developer, Inc., has trained and mentored thousands of software developers in the US, Canada, Europe, and Asia. Venkat helps his clients effectively apply and succeed with agile practices on their software projects. He is a frequently invited speaker at international software conferences and user groups.


Return to OnDotNet.com