Вывод общего типа с наследованием интерфейса (co (ntra) -вариантность?) В C # 3 - PullRequest
4 голосов
/ 20 декабря 2009

У меня есть два следующих универсальных типа:

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>» не содержит определения для «Slice», и не найден метод расширения «Slice», принимающий первый аргумент типа «System.Collections.Generic.IEnumerable>». (вам не хватает директивы using или ссылки на сборку?) C: \ Dev \ VS.NET \ LVK \ LVK.UnitTests \ Core \ Collections \ RangeTests.cs 455 26 LVK.UnitTests

Примечание : я не ожидал, что он скомпилируется. Я достаточно знаю о ко (ntra) -вариантности (когда-нибудь мне нужно узнать, какой именно, какой путь), чтобы понять, что это не сработает. У меня вопрос, могу ли я что-нибудь сделать с объявлением Slice, чтобы оно заработало.

Хорошо, тогда я попытался определить тип интерфейса диапазона, чтобы я мог обрабатывать все типы IEnumerable<R>, пока рассматриваемый R был IRange<T>.

Итак, я попробовал это:

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

Это дает мне ту же проблему.

Итак, есть ли способ настроить это?

Если нет, то мои единственные варианты:

  1. Определите два метода расширения и вызовите внутренний метод внутренне, возможно, путем преобразования одной из коллекций в ту, которая содержит базовый интерфейс?
  2. Ждать C # 4.0?

Вот как я представляю определение двух методов (обратите внимание, я все еще на ранних стадиях разработки этого, так что это может вообще не работать):

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>

Вот пример программного кода, который показывает мою проблему.

Обратите внимание, что при изменении вызовов с Slice1 на Slice2 в методе Main оба использования приводят к ошибкам компилятора, поэтому моя вторая попытка даже не обработала мой первоначальный случай.

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 ]

2 голосов
/ 21 декабря 2009

Я думаю, что вы правильно ответили на свой вопрос - без поддержки со / контравариантности 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 голос
/ 21 декабря 2009

Лассе, я добавляю другой ответ, поскольку он существенно отличается от моего существующего. (возможно, я не должен был делать это, и в этом случае, если кто-то даст мне знать, возможно, я смогу вместо этого включить его в существующий).

В любом случае, я придумала альтернативу, которая, на мой взгляд, довольно крутая и прямолинейная ...

Вместо того, чтобы заставлять дублировать каждый метод расширения из-за отсутствия со / контравариантности, предоставьте интерфейс свободно используемого типа, который маскирует требуемое поведение приведения. Преимущество этого заключается в том, что вам нужно предоставить только одну функцию для обработки приведения для всего набора методов расширения

Вот пример:

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 только один раз. Вам нужно только добавить один метод RangeExtensions для каждого потомка IRange, и поведение будет согласованным.

Есть также (как реализовано ниже) недостаток, заключающийся в том, что вы обновляете временный объект, поэтому существует (возможно, незначительное) снижение производительности.

В качестве альтернативы для каждого метода RangeExtensions можно было бы вместо этого возвращать IEnumerable> и оставлять исходные методы расширения в качестве реальных методов расширения для статического класса, принимающего аргументы «this IEnumerable> range».

Для меня проблема в том, что поведение intellisense для базового интерфейса (IRange) будет отличаться от его потомков - на базовом интерфейсе вы сможете увидеть методы расширения, не связывая вызов RangeExtensions, в то время как для всех дочерних интерфейсов вам придется вызывать RangeExtensions для его приведения.

Я думаю, что согласованность важнее, чем предельное снижение производительности, которое вы получите от обновления временного объекта.

Дайте мне знать, что вы думаете, Лассе.

С уважением, Фил

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...