简体   繁体   中英

Generic type inference with interface inheritance (co(ntra)-variance?) in C# 3

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
        }
    }
}

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.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM