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

Interesting Use of Generics in C# 2

, 20 Mar 2008 CPOL
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)

Share

About the Author

No Biography provided

Comments and Discussions

 
GeneralSome thoughts about alternatives PinmemberUrs Enzler25-Mar-08 9:38 
GeneralRe: Some thoughts about alternatives PinmemberGabriel Rozsa25-Mar-08 11:40 
GeneralRe: Some thoughts about alternatives Pinmembermscholz25-Mar-08 22:03 
GeneralRe: Some thoughts about alternatives PinmemberGabriel Rozsa25-Mar-08 22:19 
GeneralAvoid ICloneable - Risky Implementation [modified] PinmemberDaveBlack25-Mar-08 8:15 
While this is a great concept for the use of Generics, I would strongly recommend to remove the implementation of ICloneable. According to the "Framework Design Guidelines (Krzysztof Cwalina and Brad Abrams)" and "Effective C# (Bill Wagner)" the implementation of ICloneable should almost always be avoided - details follow below along with references to each Book. It was actually an admitted "goof" by the .NET CLR team (specifically Krzysztof Cwalina - an Architect on the CLR Team) to ship with the ICloneable interface. The entire .NET Framework does not even consume ICloneable as a parameter in any method.

There are several problems with implementing ICloneable:
  1. Cloning has 2 potential implementations - shallow copy and deep copy. Thus, it can be implemented differently and is not consistent across different APIs that implement it. You might expect it to do deep copy when it only does a shallow copy or vice-versa. This is especially dangerous because we developers don't enjoy writing documentation to accurately describe what the implementation does Smile | :)

  2. Implementing ICloneable in a base class forces all derived classes to implement it *and* implement it in exactly the same way that the base class does (shallow or deep copy).

  3. Shallow Copy and Deep Copy do the same thing for Value Types - they create identical copies by using assignment. However, Shallow Copy and Deep Copy work very differently for Reference Types. In Shallow Copy, copies of Reference Types are *references* to the original object. Whereas, Deep Copy creates a new separate instance and recursively copies all member variables which are Reference Types.

  4. Because of issue #3 above, mixing Value Types and Reference Types in a class that implements ICloneable causes quite a few inconsistencies in the behavior of the class.

  5. Performance problems arise when using ICloneable on a Value Type that contains only built-in types - e.g. "structs", custom value types, etc.. Simple assignment copies the values more efficiently the Clone() since Clone() must box the value types and the caller must then unbox from the cloned boject to extract the value type.

  6. Things get even more complicated when you have a value type that contains Reference Type members. Built-in assignment for value types uses a shallow copy which causes the member variable that is a reference type to be *referenced* by both structs - definitely not what was intended. To fix this you would need to Clone() the reference type member variable *and* you would need to know that the implementation of ICloneable for that reference type created Deep Copies. Because of issue #1 above, you are rolling the dice here on which implementation you'll get on your reference type.

In summary, here are the recommended Best Practices for working with ICloneable:
  • Don't - no really, avoid it if at all possible
  • Prefer Copy Constructors over ICloneable
  • Never implement ICloneable on Value Types - use assignment instead
  • Especially avoid implementing ICloneable on non-sealed types
  • If you absolutely must implement ICloneable and need to make deep copies of the entire object hierarchy,
    1. Create an abstract Clone() method in the base class and force the derived classes to implement it. However, do not implement ICloneable in the base class.
    2. Then, create a protected copy constructor in the base class
    3. Have all derived classes use the base class copy constructor in the derived implementation of ICloneable.Clone()
    4. Add any additional deep copy implementation of the derived class members in ICloneable.Clone()
    5. Document, document, document the implementation of ICloneable so that any other Developer on your team, or a consumer of your API, knows exactly what they're going to get when using your ICloneable type.
Book References:
Framework Design Guidelines (Krzysztof Cwalina and Brad Abrams) - Chapter 8.4 p221
Effective C# (Bill Wagner) - Chapter 3, Item 27 p163

Dave Black - Principal Consultant, Black Box Solutions, Inc.

modified on Wednesday, December 29, 2010 11:47 AM

GeneralRe: Avoid ICloneable - Risky Implementation PinmemberGabriel Rozsa25-Mar-08 11:22 
GeneralMonads PinmemberAlexey Romanov24-Mar-08 9:20 
Questioncuriously recurring template pattern? PinmemberMyName IsNot George21-Mar-08 11:43 
AnswerRe: curiously recurring template pattern? PinmemberDaTired22-Mar-08 5:38 
GeneralRe: curiously recurring template pattern? PinmemberJASON_S_24-Mar-08 22:21 
GeneralVery Insightful PinmemberRay Gorski21-Mar-08 5: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 | Terms of Use | Mobile
Web04 | 2.8.150414.1 | Last Updated 21 Mar 2008
Article Copyright 2008 by Antoniu-Gabriel Rozsa
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid