Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# generic collections

Tags:

c#

generics

I'm coming to C# from a Java background and keep bumping in to the same sort of problem with generics that would be trivial to solve in Java.

Given the classes:

interface IUntypedField { }
class Field<TValue> : IUntypedField { }

interface IFieldMap
{
    void Put<TValue>(Field<TValue> field, TValue value);
    TValue Get<TValue>(Field<TValue> field);
}

I'd like to write something like:

class MapCopier
{
    void Copy(IEnumerable<IUntypedField> fields, IFieldMap from, IFieldMap to)
    {
        foreach (var field in fields)
            Copy(field, from, to); // <-- clearly doesn't compile as field is IUntypedField not Field 
    }

    void Copy<TValue>(Field<TValue> field, IFieldMap from, IFieldMap to)
    {
        to.Put(field, from.Get(field));
    }
}

In Java this would simple to solve as the fields collection would be an Iterable<Field<?>> and you could call Copy(Field, IFieldMap, IFieldMap) directly.

In C# I find myself doing switch/cast for all possible values of TValue (which smells horrifically, having to add a case for every type you add is clearly a bug waiting to happen, and only feasible if the set of types are finite):

foreach (var field in fields)
{
    switch (field.Type) // added an enum to track the type of Field's parameterised type
    {
    case Type.Int:   Copy((Field<int>)field, from, to); break;
    case Type.Long:  Copy((Field<long>)field, from, to); break; 
    ...
    }
}

The other option I sometimes do is to move the functionality in to the Field class, which again, stinks. It's not the responsibility of the field. At least this avoids the huge switch:

interface IUntypedField { void Copy(IFieldMap from, IFieldMap to); }
class Field<TValue> : IUntypedField 
{ 
    void Copy(IFieldMap from, IFieldMap to)
    {
        to.Put(this, from.Get(this));
    }
}

...

    foreach (var field in fields)
        field.Copy(from, to);

If you could write polymorphic extension methods (i.e. the Copy methods in IUntypedField and Field above) then you could at least keep the code in alongside the class whose responsibility it is.

Am I missing some feature of C# that would make this possible. Or is there some functional pattern that could be used? Any ideas?

(One last thing, I'm currently stuck on .Net 3.5, so can't make use of any covariance/contravariance, but would still be interested to know how they would help here, if at all).

like image 303
SimonC Avatar asked Jan 03 '11 03:01

SimonC


2 Answers

Here's a perfectly type-safe method that compiles lambdas to perform the copy:

static class MapCopier
{
    public static void Copy(IEnumerable<IUntypedField> fields, IFieldMap from, IFieldMap to)
    {
        foreach (var field in fields)
            Copy(field, from, to);
    }

    // cache generated Copy lambdas
    static Dictionary<Type, Action<IUntypedField, IFieldMap, IFieldMap>> copiers =
        new Dictionary<Type, Action<IUntypedField, IFieldMap, IFieldMap>>();

    // generate Copy lambda based on passed-in type
    static void Copy(IUntypedField field, IFieldMap from, IFieldMap to)
    {
        // figure out what type we need to look up;
        // we know we have a Field<TValue>, so find TValue
        Type type = field.GetType().GetGenericArguments()[0];
        Action<IUntypedField, IFieldMap, IFieldMap> copier;
        if (!copiers.TryGetValue(type, out copier))
        {
            // copier not found; create a lambda and compile it
            Type tFieldMap = typeof(IFieldMap);
            // create parameters to lambda
            ParameterExpression
                fieldParam = Expression.Parameter(typeof(IUntypedField)),
                fromParam = Expression.Parameter(tFieldMap),
                toParam = Expression.Parameter(tFieldMap);
            // create expression for "(Field<TValue>)field"
            var converter = Expression.Convert(fieldParam, field.GetType());
            // create expression for "to.Put(field, from.Get(field))"
            var copierExp =
                Expression.Call(
                    toParam,
                    tFieldMap.GetMethod("Put").MakeGenericMethod(type),
                    converter,
                    Expression.Call(
                        fromParam,
                        tFieldMap.GetMethod("Get").MakeGenericMethod(type),
                        converter));
            // create our lambda and compile it
            copier =
                Expression.Lambda<Action<IUntypedField, IFieldMap, IFieldMap>>(
                    copierExp,
                    fieldParam,
                    fromParam,
                    toParam)
                    .Compile();
            // add the compiled lambda to the cache
            copiers[type] = copier;
        }
        // invoke the actual copy lambda
        copier(field, from, to);
    }

    public static void Copy<TValue>(Field<TValue> field, IFieldMap from, IFieldMap to)
    {
        to.Put(field, from.Get(field));
    }
}

Note that this method creates the copying method on-the-fly rather than calling the Copy<TValue> method. This is essentially inlining, and saves about 50ns per call by not having the extra call. If you were to make the Copy method more complicated, it might be easier to call Copy rather than creating an expression tree to inline it.

like image 24
Gabe Avatar answered Oct 06 '22 16:10

Gabe


You can use reflection at least to avoid switch { }. Covariance/contravariance from fw 4.0 won't help in this case. Maybe it is possible to benefit from using dynamic keyword.

// untested, just demonstates concept
class MapCopier
{
    private static void GenericCopy<TValue>(Field<TValue> field, IFieldMap from, IFieldMap to)
    {
        to.Put(field, from.Get(field));
    }

    public void Copy(IEnumerable<IUntypedField> fields, IFieldMap from, IFieldMap to)
    {
        var genericMethod = typeof(MapCopier).GetMethod("GenericCopy");
        foreach(var field in fields)
        {
            var type = field.GetType().GetGenericArguments()[0];
            var method = genericMethod.MakeGenericMethod(type);
            method.Invoke(null, new object[] { field, from, to });
        }
    }
}
like image 151
max Avatar answered Oct 06 '22 18:10

max