C#3中具有接口继承(co(ntra) - 方差?)的通用类型推断

时间:2009-12-20 19:48:43

标签: c# inheritance collections covariance contravariance

我有以下两种通用类型:

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

现在我想为这些接口的集合定义扩展方法,因为它们都是IRange<T>或者来自IRange<T>,我希望我能定义一个可以处理这两个接口的方法。请注意,该方法不需要处理两者之间的任何差异,只需处理来自IRange<T>的公共部分。

我的问题是:

我可以定义一个扩展方法来处理这两种类型之一的集合(IEnumerable<T>)吗?

我试过了:

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

然而,传递IEnumerable<IRange<Int32, String>>,如下所示:

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

给了我这个编译错误:

  

错误1'System.Collections.Generic.IEnumerable&gt;'不包含'Slice'的定义,也没有扩展方法'Slice'接受'System.Collections.Generic.IEnumerable&gt;'类型的第一个参数可以找到(您是否缺少using指令或程序集引用?)C:\ Dev \ VS.NET \ LVK \ LVK.UnitTests \ Core \ Collections \ RangeTests.cs 455 26 LVK.UnitTests

注意:我没想到它会编译。我知道co(ntra)-Variance(有一天我需要知道哪一个是哪种方式)才能知道这是行不通的。我的问题是,如果有任何我可以对Slice声明做些什么来使它工作。

好的,那么我尝试推断范围界面的类型,这样我就可以处理所有类型的IEnumerable<R>,只要问题RIRange<T>

所以我尝试了这个:

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

这给了我同样的问题。

那么,有没有办法调整这个?

如果没有,是我唯一的选择:

  1. 定义两个扩展方法,并在内部调用内部方法,也许通过将其中一个集合转换为包含基本接口的集合?
  2. 等待C#4.0?

  3. 以下是我设想定义这两种方法的方法(注意,我还处于早期设计阶段,所以这可能根本不起作用):

    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>
    

    以下是显示我的问题的示例程序代码。

    请注意,通过在Main方法中将调用从Slice1更改为Slice2会使两个用法产生编译器错误,因此我的第二次尝试甚至都不能处理我的初始情况。

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

2 个答案:

答案 0 :(得分:2)

我认为你正确地回答了你自己的问题 - 没有C#4.0对接口的共同/逆转支持,你不得不写一些重复的代码。

您可能还想使用IEnumerable<T> Enumerable.Cast<T>(this IEnumerable collection)方法 - 执行延迟,因此您可以在代码中使用它(显式地)在T和T的子类之间进行转换,而无需创建新的集合。

虽然,您可能想编写自己的强制转换,因为没有约束可以确保集合包含T的后代,因此您可以对运行时异常开放。我想一个具有以下语法的函数可以工作,但你将失去混合类型推理和扩展方法的能力:

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

不幸的是,你必须指定T,所以漂亮的干净扩展语法会超出窗口(如果有一些约定允许你对扩展方法进行类型推断,并且仍然允许显式类型参数声明,那将会很好,没有必须重复可以推断的类型。

答案 1 :(得分:1)

Lasse,我正在添加另一个答案,因为这与我现有的答案大不相同。 (也许我不应该这样做,在这种情况下,如果有人让我知道,也许我可以将其纳入现有的)。

无论如何,我想出了一个替代方案,我认为这个方案非常酷,而且很直接......

由于缺乏共同/逆变而不是强制复制每个扩展方法,而是提供了一个流畅的类型接口,它掩盖了所需的铸造行为。这样做的好处是,您只需提供一个函数来处理整套扩展方法的转换

以下是一个例子:

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

当然缺点是使用'扩展方法'(不再是扩展方法)需要链接到对RangeExtensions方法的调用,但我认为这是一个相当不错的权衡,因为无论多少扩展方法现在可以在RangeExtensions类上提供一次。您只需为IRange的每个后代添加一个RangeExtensions方法,并且行为是一致的。

还有(如下所示)你正在新建一个临时对象的缺点,所以有一个(可能是边际的)性能损失。

另一种方法是让每个RangeExtensions方法返回一个IEnumerable&gt;并将原始扩展方法作为实际扩展方法保留在静态类中,并采用'this IEnumerable&gt;范围的论点。

对我来说,问题在于基本接口(IRange)的intellisense行为与它的后代不同 - 在基本接口上,您可以看到扩展方法而无需链接对RangeExtensions的调用,而对于所有后代接口,您必须调用RangeExtensions来获取它。

我认为一致性比新近临时对象所获得的边际性能更重要。

让我知道你对Lasse的看法。

此致 菲尔