Click here to Skip to main content
Click here to Skip to main content

Interesting Use of Generics in C# 2

, 20 Mar 2008
Rate this:
Please Sign up or sign in to vote.
Generics in C# are cool. Not as cool as C++ templates, though. However, one can do pretty interesting things with them.

Introduction

While playing with .NET 2.0's Generics, an interesting pattern came to my mind as a solution for a code reuse problem. I quickly implemented the solution, fairly skeptical about the C# compiler being able to compile my code successfully. To my surprise, the compiler didn't complain a bit. Still thinking that even though it compiled successfully, some weird exception might be thrown at runtime, I tried executing my piece of code. It worked flawlessly.

Background

The problem I had to solve:

The project I was working on contained several "data holder"-type classes that were similar in their purpose. These classes were all used to store different data. These data were associated with a date and had to be transmitted to another layer of the application as a list. This layer that was to receive the lists of the objects had two requirements: the data had to be sorted chronologically (by the date) and there could be no two pieces of data in a list having the same date.

So I have my classes, all having different properties to store the useful data, plus the Date property. Then I have my List<>'s of instances of these classes. These must be sorted of course and the entries having the same dates must be eliminated. The eliminated entries don't just disappear, since we don't want to lose the data they're holding. Therefore we must combine the data of the removed entry with the data of the entry that stays in the list.

OK, so now I had a static function that would take as input parameter a strongly-typed generic list of a particular class and return a list usable for my other layer. Since I wanted everything to be strongly-typed, I had to copy-paste-edit this function for every class. Not nice, so enter Generics.

The classic solution:

So how would a regular developer solve this? Create an interface, make the data holder classes implement the interface. Create a generic class having a parameter that inherits from the interface, have the sort / eliminate dates code work with the generic parameter. Something like:

public interface IDateObj
{
    DateTime Date
    {
        get;
        set;
    }
    
    void Combine(IDateObj obj);
}

And the generic class for that:

public static class ObjListManager<T> where T : ICloneable, IDateObj
{
    public static List<T> EliminateSameDates(List<T> listOrig)
    {
        if (listOrig.Count < 2)
            return listOrig;

        List<T> list = new List<T>(listOrig.Count);
        //copy listOrig to list
        foreach (T obj in listOrig)
        {
            T objCopy = (T)obj.Clone();
            list.Add(objCopy);
        }

        //sort list by date
        list.Sort(delegate(T obj1, T obj2) { return obj1.Date.CompareTo(obj2.Date); });

        for (int i = list.Count - 1; i >= 1; i--)
        {
            T curr = list[i];
            T prev = list[i - 1];
            //if it has the same date as the one before
            if (prev.Date.Date == curr.Date.Date)                    
            {
                prev.Combine(curr);                    
                prev.Date = curr.Date;
                list.RemoveAt(i);
            }
        }            

        return list;
    }    
}
Not bad. This way we could do the job like this:
public class VersementObject : ICloneable, IDateObj
{
    // implementation ...
    
    public void Combine(IDateObj obj)
    {
        // get my object
        VersementObject myObj = (VersementObject)obj;
        // do whatever
    }
}

void Main()
{
    List<VersementObject> list;    
    // init list
    
    // prepare
    list = ObjListManager<VersementObject>.EliminateSameDates(list);    
    // do whatever with it
}
No big deal so far. Nothing interesting, really. Thing is that I wanted to do better than that because:
  1. In VersementObject.Combine I have a parameter of type IDateObj. But what I really want is a VersementObject, hence I need a conversion. I want to get rid of that.
  2. The VersementObject.Combine function must be public because it is an implementation of the IDateObj interface. But this function is only used as a trait for the VersementObject class in the EliminateSameDates function, I don't need it to be visible for all users of the VersementObject class.
  3. I don't like long code lines, they're harder to follow when they don't fit in the editing window. With Generics it is inevitable to have long code lines (unless you name your classes using three-lettered acronyms). But still, I'd like to make the line containing the invocation of EliminateSameDates as short as possible.
  4. What if I need to do additional processing after the objects having the same date are eliminated? All I can do is write a new function that calls EliminateSameDates and then does the processing on the list. Flexible, but not so nice. I don't want to write wrappers in the Main code. I could add a method to the interface and have EliminateSameDates call it, as a last step before returning. This would force all implementers of IDateObj to provide an empty stub for this method. And a public one, too! A lot of useless code polluting my nice classes.

About the code

So if I want to solve the 4 problems I still had, the interface has to go.
Now, the interface refuses to go away quietly because I need the Date property in the EliminateSameDates function. I also need Combine. So what's the next best thing after interfaces? Abstract classes. Actually, they're better than interfaces since you can put some code in them as well, not only stubs. Of course, the big drawback is that you can only inherit from one such class at a time because C# doesn't allow multiple inheritance (note: C++ rules! Wink | ;) ). But this time, an abstract class should do perfectly well.

So, for this to work I need a generic abstract class that would only accept as a generic parameter a type derived from itself. Nice. In theory. I was thinking that the only problem with this will be that the compiler won't see the beauty of my solution. Why? Well, let's see. The fact that I use the class itself to specify constraints about the generic parameter of the very class I was using in the definition. It looked like recursion, the nasty kind that never stops.

I was wrong. The compiler was smarter than I expected. So here's the solution:

public abstract class ObjListManager<T> : ICloneable where T : ObjListManager<T>
{
    public abstract DateTime Date
    {
        get;
        set;
    }

    public abstract object Clone();

    protected abstract void Combine(T obj);

    delegate bool EliminateCriterion(T prev, T curr);

    static List<T> Eliminate(List<T> listOrig, EliminateCriterion crit)
    {
        if (listOrig.Count < 2)
            return listOrig;

        List<T> list = new List<T>(listOrig.Count);
        //copy listOrig to list
        foreach (T obj in listOrig)
        {
            T objCopy = (T)obj.Clone();
            list.Add(objCopy);
        }

        //sort list by date
        list.Sort(delegate(T obj1, T obj2) { return obj1.Date.CompareTo(obj2.Date); });

        for (int i = list.Count - 1; i >= 1; i--)
        {
            T curr = list[i];
            T prev = list[i - 1];
            //if it has the same date as the one before
            if (crit(prev, curr))                    
            {
                prev.Combine(curr);                    
                prev.Date = curr.Date;
                list.RemoveAt(i);
            }
        }            

        return list;
    }

    public static List<T> EliminateSameDates(List<T> listOrig)
    {
        return Eliminate(listOrig, delegate(T prev, T curr) 
        { return prev.Date.Date == curr.Date.Date; });
    }

    public static List<T> EliminateSameMonths(List<T> listOrig)
    {
        return Eliminate(listOrig, delegate(T prev, T curr) 
        { return prev.Date.Year == curr.Date.Year && 
             prev.Date.Month == curr.Date.Month; });
    }
}
And the implementation of the class becomes:
public class VersementObject : ObjListManager<VersementObject>
{
    // other properties, useful data

    public int ID
    {
        get { return _id; }
        set { _id = value; }
    }

    public override DateTime Date
    {
        get { return _date; }
        set { _date = value; }
    }    

    public override object Clone()
    {
        VersementObject obj = new VersementObject();
        // copy all fields
        return obj;
    }

    protected override void Combine(VersementObject obj)
    {
        // do whatever combine logic
    }

    public static new List<VersementObject> 
                EliminateSameDates(List<VersementObject> listOrig)
    {            
        List<VersementObject> list = 
        ObjListManager<VersementObject>.EliminateSameDates(listOrig);

        // init ID's
        int vId = 0;
        foreach (VersementObject vo in list)
        {
            vo.ID = vId++;
        }

        return list;
    }
}
And finally the way I invoke the method:
list = VersementObject.EliminateSameDates(list);
Neat. So that solved my problems. Including the last one. If I need to do some special processing of the list, after it got sorted and prepared, I can do it in the EliminateSameDates function by overriding it with a new implementation. In the code I have a property ID that needs to be set to the value of the position of the object in the list.

As a bonus I also implemented an EliminateSameMonths method. This one eliminates all entries from the list that have their date in the same months. All classes inheriting from my ObjListManager<> have the two methods: EliminateSameDates and EliminateSameMonths, so if the logic of the application changes in time (for example all entries in the same months and not the same date are now to be removed), you can simply switch the calls.

The two Eliminate- methods are very similar so I had them both call the same private method, Eliminate, with different elimination criteria. The criterion for elimination is supplied using a delegate. I also used anonymous methods for the implementation of the delegates to keep the code short and compact.

After running my code successfully and the feeling of surprise and relief slowly fading away, another shadow of doubt entered my mind. What if it all works because I have an abstract class? Maybe the abstract-ness helps avoid the recursion I was worried about. So I tried it without having an abstract class. Well, it worked flawlessly again. Nice.

Points of Interest

The Generics in C# 2 are pretty nice, even though they are no match for the templates in C++. Anyway, they both have pros and cons. Today, the C# compiler surprised me. I have underestimated it. Anyway, the conclusion of my little experiment is that C#'s Generics is a pretty powerful tool to optimize and embellish your code and that I should never stop at the "classic" solution. There's always an even better solution at hand.

History

21 March 2008 - First version of article.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

No Biography provided

Comments and Discussions

 
GeneralSome thoughts about alternatives PinmemberUrs Enzler25-Mar-08 8:38 
GeneralRe: Some thoughts about alternatives PinmemberGabriel Rozsa25-Mar-08 10:40 
GeneralRe: Some thoughts about alternatives Pinmembermscholz25-Mar-08 21:03 
GeneralRe: Some thoughts about alternatives PinmemberGabriel Rozsa25-Mar-08 21:19 
GeneralAvoid ICloneable - Risky Implementation [modified] PinmemberDaveBlack25-Mar-08 7:15 
GeneralRe: Avoid ICloneable - Risky Implementation PinmemberGabriel Rozsa25-Mar-08 10:22 
GeneralMonads PinmemberAlexey Romanov24-Mar-08 8:20 
Questioncuriously recurring template pattern? PinmemberMyName IsNot George21-Mar-08 10:43 
AnswerRe: curiously recurring template pattern? PinmemberDaTired22-Mar-08 4:38 
GeneralRe: curiously recurring template pattern? PinmemberJASON_S_24-Mar-08 21:21 
GeneralVery Insightful PinmemberRay Gorski21-Mar-08 4:13 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140721.1 | Last Updated 21 Mar 2008
Article Copyright 2008 by Antoniu-Gabriel Rozsa
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid