![]() |
Languages »
C# »
Generics
Intermediate
License: The Code Project Open License (CPOL)
Interesting Use of Generics in C# 2By Gabriel RozsaGenerics in C# are cool. Not as cool as C++ templates, though. However, one can do pretty interesting things with them. |
C# (C# 2.0), .NET (.NET 2.0), Visual Studio (VS2005), Dev, Design
|
||||||||
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||
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.
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: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.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. EliminateSameDates as short as possible. 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.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! ;) ). 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.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.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.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.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.
21 March 2008 - First version of article.
| You must Sign In to use this message board. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 20 Mar 2008 Editor: |
Copyright 2008 by Gabriel Rozsa Everything else Copyright © CodeProject, 1999-2009 Web21 | Advertise on the Code Project |