views:

296

answers:

2

I have the following two generic types:

interface IRange<T> where T : IComparable<T>
interface IRange<T, TData> : IRange<T> where T : IComparable<T>
                           ^---------^
                                |
                                +- note: inherits from IRange<T>

Now I want to define an extension methods for collections of these interfaces, and since they're both either IRange<T> or descends from IRange<T> I was hoping I could define one method that would handle both. Note that the method will not need to deal with any of the differences between the two, only the common part from IRange<T>.

My question is thus this:

Can I define one extension method that will handle collections (IEnumerable<T>) of either of these two types?

I tried this:

public static void Slice<T>(this IEnumerable<IRange<T>> ranges)
    where T : IComparable<T>

however, passing an IEnumerable<IRange<Int32, String>>, like this:

IEnumerable<IRange<Int32, String>> input = new IRange<Int32, String>[0];
input.Slice();

gives me this compiler error:

Error 1 'System.Collections.Generic.IEnumerable>' does not contain a definition for 'Slice' and no extension method 'Slice' accepting a first argument of type 'System.Collections.Generic.IEnumerable>' could be found (are you missing a using directive or an assembly reference?) C:\Dev\VS.NET\LVK\LVK.UnitTests\Core\Collections\RangeTests.cs 455 26 LVK.UnitTests

Note: I did not expect it to compile. I know enough about co(ntra)-variance (some day I need to learn which one is which way) to know that won't work. My question is if there's anything I can do to the Slice declaration to make it work.

Ok, so then I tried to infer the type of the range interface, so that I could handle all types of IEnumerable<R> as long as the R in question was a IRange<T>.

So I tried this:

public static Boolean Slice<R, T>(this IEnumerable<R> ranges)
    where R : IRange<T>
    where T : IComparable<T>

This gives me the same problem.

So, is there a way to tweak this?

If not, are my only options to:

  1. Define two extension methods, and call an internal method internally, perhaps by converting one of the collections to one that contains the base interface?
  2. Wait for C# 4.0?


Here's how I envision defining the two methods (note, I'm still in the early design phases of this, so this might not work at all):

public static void Slice<T>(this IEnumerable<IRange<T>> ranges)
    where T : IComparable<T>
{
    InternalSlice<T, IRange<T>>(ranges);
}

public static void Slice<T, TData>(this IEnumerable<IRange<T, TData>> ranges)
    where T : IComparable<T>
{
    InternalSlice<T, IRange<T, TData>>(ranges);
}

private static void Slice<T, R>(this IEnumerable<R> ranges)
    where R : IRange<T>
    where T : IComparable<T>


Here's a sample program code that shows my problem.

Note that by changing the calls from Slice1 to Slice2 in the Main method makes both usages produce compiler errors, so my second attempt didn't even handle my initial case.

using System;
using System.Collections.Generic;

namespace SO1936785
{
    interface IRange<T> where T : IComparable<T> { }
    interface IRange<T, TData> : IRange<T> where T : IComparable<T> { }

    static class Extensions
    {
        public static void Slice1<T>(this IEnumerable<IRange<T>> ranges)
            where T : IComparable<T>
        {
        }

        public static void Slice2<R, T>(this IEnumerable<R> ranges)
            where R : IRange<T>
            where T : IComparable<T>
        {
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            IEnumerable<IRange<Int32>> a = new IRange<Int32>[0];
            a.Slice1();

            IEnumerable<IRange<Int32, String>> b = new IRange<Int32, String>[0];
            b.Slice1(); // doesn't compile, and Slice2 doesn't handle either
        }
    }
}
+1  A: 

I think you answered your own question correctly - without C# 4.0's co/contravariance support for interfaces you're forced to write some duplicate code.

You may also want to use the IEnumerable<T> Enumerable.Cast<T>(this IEnumerable collection) method - it's delay executed, so you can use it (explicitly) within your code to convert between the subclass of T and T without creating a new collection.

Although, you may want to write your own cast since there is no constraint that ensures the collection contains a descendent of T and therefore you're open to runtime exceptions. I guess a function with the following syntax would work, but you'll lose the ability to mix type inference and extension methods:

public static IEnumerable<T> Cast<T,TSubset>(IEnumerable<TSubset> source)
   where TSubset : T
{
   foreach(T item in source) yield return item;
}

Unfortunately, you have to specify T, so the nice clean extension syntax goes out the window (it would be nice if there was some convention which allowed you to get type inference on extension methods, and still allow explicit statement of type arguments, without having to repeat the type which can be inferred.

Phil
The way I solved it was, if you look at that code at the bottom of my post, which has a "Slice1" method where inference works, for one of the two interfaces, and "Slice2", which can handle both interfaces, but inference doesn't work. The way I handle it was to write two Slice1-type methods, one for each interface, and make the Slice2-type method private, and contain the actual implementation. The two Slice1-type methods thus just forwards the call to the private one, with explicit type specifications. This also had the upshot of allowing me to do null-checks on call, instead of on enumeration.
Lasse V. Karlsen
Sound perfectly reasonable Lasse. It is one of those things which you can spend a lot of time trying to avoid, since it seems like surely there's a way around it. Don't think there is though - I think we're stuck doing that kind of thing till 4.0
Phil
Lasse V. Karlsen
One thing I was hoping to allow, since this is a class library after all, would be that I would provide the basic interfaces for "ranges", ie. from-to type, for any comparable type, and that new implementations could be built that the framework could handle. But the type inference problems seems to prohibit that model currently, so I'll have to come back to that portion of it when I remodel my C# 4.0 version next year.
Lasse V. Karlsen
+1  A: 

Lasse, I'm adding another answer since this is substantially different to my existing one. (maybe I shouldn't be doing this, in which case if someone lets me know maybe I can instead incorporate it into the existing one).

Anyway, I've come up with an alternative which I think is pretty cool, and straightforward...

Instead of being forced into duplicating each extension method because of lack of co/contravariance provide a fluent type interface which masks the required casting behavior. This has the advantage of you only having to provide one function to handle the casting for your entire set of extension methods

Here's an example:

class Program
{
    static void Main(string[] args)
    {
        IEnumerable<IRange<int>> enumRange1 = new IRange<int>[0];
        IEnumerable<IRange<int, float>> enumRange2 = new IRange<int, float>[0];

        IEnumerable<IRange<int, float, string>> enumRange3 = new TestRange<int, float, string>[]
        {
            new TestRange<int, float, string> { Begin = 10, End = 20, Data = 3.0F, MoreData = "Hello" },
            new TestRange<int, float, string> { Begin = 5, End = 30, Data = 3.0F, MoreData = "There!" }
        };

        enumRange1.RangeExtensions().Slice();
        enumRange2.RangeExtensions().Slice();
        enumRange3.RangeExtensions().Slice();
    }
}

public interface IRange<T> where T : IComparable<T>
{
    int Begin { get; set; }
    int End { get; set; }
}

public interface IRange<T, TData> : IRange<T> where T : IComparable<T>
{
    TData Data { get; set; }
}

public interface IRange<T, TData, TMoreData> : IRange<T, TData> where T : IComparable<T>
{
    TMoreData MoreData { get; set; }
}

public class TestRange<T, TData, TMoreData> : IRange<T, TData, TMoreData>
    where T : IComparable<T>
{
    int m_begin;
    int m_end;
    TData m_data;
    TMoreData m_moreData;

    #region IRange<T,TData,TMoreData> Members
    public TMoreData MoreData
    {
        get { return m_moreData; }
        set { m_moreData = value; }
    }
    #endregion

    #region IRange<T,TData> Members
    public TData Data
    {
        get { return m_data; }
        set { m_data = value; }
    }
    #endregion

    #region IRange<T> Members
    public int Begin
    {
        get { return m_begin; }
        set { m_begin = value; }
    }

    public int End
    {
        get { return m_end; }
        set { m_end = value; }
    }
    #endregion
}

public static class RangeExtensionCasts
{
    public static RangeExtensions<T1> RangeExtensions<T1>(this IEnumerable<IRange<T1>> source)
        where T1 : IComparable<T1>
    {
        return new RangeExtensions<T1>(source);
    }

    public static RangeExtensions<T1> RangeExtensions<T1, T2>(this IEnumerable<IRange<T1, T2>> source)
        where T1 : IComparable<T1>
    {
        return Cast<T1, IRange<T1, T2>>(source);
    }

    public static RangeExtensions<T1> RangeExtensions<T1, T2, T3>(this IEnumerable<IRange<T1, T2, T3>> source)
        where T1 : IComparable<T1>
    {
        return Cast<T1, IRange<T1, T2, T3>>(source);
    }

    private static RangeExtensions<T1> Cast<T1, T2>(IEnumerable<T2> source)
        where T1 : IComparable<T1>
        where T2 : IRange<T1>
    {
        return new RangeExtensions<T1>(
            Enumerable.Select(source, (rangeDescendentItem) => (IRange<T1>)rangeDescendentItem));
    }
}

public class RangeExtensions<T>
    where T : IComparable<T>
{
    IEnumerable<IRange<T>> m_source;

    public RangeExtensions(IEnumerable<IRange<T>> source)
    {
        m_source = source;
    }

    public void Slice()
    {
        // your slice logic

        // to ensure the deferred execution Cast method is working, here I enumerate the collection
        foreach (IRange<T> range in m_source)
        {
            Console.WriteLine("Begin: {0} End: {1}", range.Begin, range.End);
        }
    }
}

There is of course the disadvantage that the use of the 'extension methods' (which aren't really extension methods anymore) requires chaining onto a call to the RangeExtensions methods but I think it's a pretty decent tradeoff since no matter how many extension methods they can now be provided on the RangeExtensions class just once. You only have to add a single RangeExtensions method for each descendent of IRange and the behavior is consistent.

There's also (as implemented below) the disadvantage that you are newing up a temporary object, so there's a (probably marginal) performance penalty.

An alternative would be for each RangeExtensions method to instead return an IEnumerable> and leave the original extension methods as actual extension methods on a static class taking 'this IEnumerable> ranges' arguments.

For me, the problem with this is that the intellisense behavior would be different for the base interface (IRange) than it's descendents - on the base interface you would be able to see the extension methods without chaining a call to RangeExtensions, while for all the descendent interfaces you would have to call RangeExtensions to get it casted.

I think consistency is more important than the marginal performance hit you'll get from newing up a temporary object.

Let me know what you think Lasse.

Regards, Phil

Phil
This was really promising. I have a similar case somewhere else where I've already used this approach, but not to solve the double type inference problem, it just lent itself to that approach. Basically, in that other place, I have a syntax like: obj.Produce().Sums(), I could easily incorporate the same here. This might even let me work with IRange-interfaces not explicitly defined by me! I'm definitely going to look into this later tonight!
Lasse V. Karlsen
Cool ;)One thing you might also want to do, dependent on the nature of your methods (splice etc) is have them "return this".Obviously this is consistent with and common to fluent interfaces as it will allow the user to chain multiple operations together
Phil
Oh, I have good experience with fluent interfaces :) I even have a side-project going to try to auto-generate a fluent interface from a language file, with partial methods where code should be injected. You should see the fluent interface I built for my IoC container, there's *way* too many classes for it :P
Lasse V. Karlsen