Ковариантность и Контравариантность в C#

Начну с того, что я являюсь Java-разработчиком, который учится программировать на C#. Поэтому я делаю сравнения того, что я знаю, с тем, что я изучаю.

Я играю с дженериками C# уже несколько часов, и мне удалось воспроизвести те же вещи, которые я знаю в Java на C#, за исключением нескольких примеров, использующих ковариацию и контравариантность. Книга, которую я читаю, не очень хороша в предмете. Я, конечно, буду искать больше информации в Интернете, но пока я делаю это, возможно, вы поможете мне найти реализацию C# для следующего кода Java.

Пример стоит тысячи слов, и я надеялся, что, посмотрев хороший пример кода, я смогу быстрее усвоить это.

ковариации

В Java я могу сделать что-то вроде этого:

public static double sum(List<? extends Number> numbers) {
    double summation = 0.0;
    for(Number number : numbers){
        summation += number.doubleValue();
    }
    return summation;
}

Я могу использовать этот код следующим образом:

List<Integer> myInts = asList(1,2,3,4,5);
List<Double> myDoubles = asList(3.14, 5.5, 78.9);
List<Long> myLongs = asList(1L, 2L, 3L);

double result = 0.0;
result = sum(myInts);
result = sum(myDoubles)
result = sum(myLongs);

Теперь я обнаружил, что C# поддерживает ковариацию / контравариантность только на интерфейсах и до тех пор, пока они явно объявлены для этого (out/in). Я думаю, что я не смог воспроизвести этот случай, потому что я не смог найти общего предка всех чисел, но я считаю, что я мог бы использовать IEnumerable для реализации такой вещи, если существует общий предок. Поскольку IEnumerable является ковариантным типом. Правильно?

Любые мысли о том, как реализовать список выше? Просто укажи мне правильное направление. Есть ли общий предок всех числовых типов?

контрвариация

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

public static void copy(List<? extends Number> source, List<? super Number> destiny){
    for(Number number : source) {
       destiny.add(number);
    }
}

Тогда я мог бы использовать его с контравариантными типами следующим образом:

List<Object> anything = new ArrayList<Object>();
List<Integer> myInts = asList(1,2,3,4,5);
copy(myInts, anything);

Моя основная проблема при попытке реализовать это в C# заключается в том, что я не смог найти интерфейс, который был бы одновременно ковариантным и контравариантным, как это было в случае со списком в моем примере выше. Может быть, это можно сделать с помощью двух разных интерфейсов в C#.

Есть мысли о том, как это реализовать?

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

4 ответа

Решение

Для 2-й части вашего вопроса вам не нужна контрвариантность, все, что вам нужно сделать, это указать, что первый тип может быть приведен ко второму. Опять же, используйте where TSource: TDest синтаксис, чтобы сделать это. Вот полный пример (который показывает, как это сделать с помощью метода расширения):

static class ListCopy
{
    public static void ListCopyToEnd<TSource, TDest>(this IList<TSource> sourceList, IList<TDest> destList)
        where TSource : TDest // This lets us cast from TSource to TDest in the method.
    {
        foreach (TSource item in sourceList)
        {
            destList.Add(item);
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        List<int> intList = new List<int> { 1, 2, 3 };
        List<object> objList = new List<object>(); ;

        ListCopy.ListCopyToEnd(intList, objList);
        // ListCopyToEnd is an extension method
        // This calls it for a second time on the same objList (copying values again).
        intList.ListCopyToEnd(objList);

        foreach (object obj in objList)
        {
            Console.WriteLine(obj);
        }
        Console.ReadLine();
    }

Вместо того, чтобы отвечать на ваши вопросы напрямую, я собираюсь ответить на несколько иные вопросы:

Есть ли в C# способ обобщать типы, которые поддерживают арифметические операторы?

Не легко, нет. Было бы неплохо иметь возможность сделать Sum<T> метод, который может добавить целые числа, двойные числа, матрицы, комплексные числа, кватернионы... и так далее. Хотя это довольно часто запрашиваемая функция, это также большая функция, и она никогда не была достаточно высокой в ​​списке приоритетов, чтобы оправдать ее включение в язык. Я лично хотел бы, но вы не должны ожидать этого в C# 5. Возможно, в гипотетической будущей версии языка.

В чем разница между ковариацией / контравариантностью "сайта вызова" Java и ковариацией / контравариантностью сайта объявления C#?

Принципиальным отличием на уровне реализации является, конечно, то, что на практике дженерики Java реализуются посредством стирания; хотя вы получаете преимущества приятного синтаксиса для универсальных типов и проверки типов во время компиляции, вы не обязательно получаете преимущества в производительности или интеграции системы типов во время выполнения, как в C#.

Но это действительно больше деталей реализации. Более интересное отличие с моей точки зрения состоит в том, что правила дисперсии Java применяются локально, а правила дисперсии C# применяются глобально.

То есть: определенные преобразования вариантов опасны, потому что они подразумевают, что определенные не типобезопасные операции не будут перехвачены компилятором. Классический пример:

  • Тигр - это млекопитающее.
  • Список X ковариантен в X. (Предположим.)
  • Поэтому список тигров - это список млекопитающих.
  • В список млекопитающих можно вставить жирафа.
  • Поэтому вы можете вставить жирафа в список тигров.

Что явно нарушает безопасность типа, а также безопасность жирафа.

C# и Java используют два разных метода для предотвращения нарушения безопасности этого типа. C# говорит, что когда I<T> Интерфейс объявляется, если он объявлен как ковариантный, то не должно быть метода интерфейса, который принимает T. Если нет способа вставить T в список, то вы никогда не вставите жирафа в список тигров, потому что нет способа вставить что-либо.

Java, напротив, говорит, что на этом локальном сайте мы получаем ковариантный подход к типу и обещаем не вызывать здесь никаких методов, которые могут нарушать безопасность типов.

У меня недостаточно опыта работы с функциями Java, чтобы сказать, что "лучше" и при каких обстоятельствах. Техника Java, безусловно, интересна.

Вы можете использовать IConvertible интерфейс:

public static decimal sum<T>(IEnumerable<T> numbers) where T : IConvertible
{
    decimal summation = 0.0m;

    foreach(var number in numbers){
        summation += number.ToDecimal(System.Globalization.CultureInfo.InvariantCulture);
    }
    return summation;
}

Обратите внимание на общее ограничение (where T : IConvertible), который похож на extends на Яве.

Там нет базы Number класс в.NET. Самое близкое, что вы можете получить, может выглядеть примерно так:

public static double sum(List<object> numbers) {
    double summation = 0.0;
    var parsedNumbers = numbers.Select(n => Convert.ToDouble(n));
    foreach (var parsedNumber in parsedNumbers) {
        summation += parsedNumber;
    }
    return summation;
}

Вы должны были бы поймать любые ошибки, которые происходят во время Convert.ToDouble если какой-либо объект в списке не является числовым и не реализует IConvertible,

Обновить

В этой ситуации, однако, я бы лично использовал IEnumerable и универсальный тип (и, благодаря Полю Тинг, вы можете заставить T реализовать IConvertible):

public static double sum<T>(IEnumerable<T> numbers) where T : IConvertible {
    double summation = 0.0;
    var parsedNumbers = numbers.Select(n => Convert.ToDouble(n));
    foreach (var parsedNumber in parsedNumbers) {
        summation += parsedNumber;
    }
    return summation;
}
Другие вопросы по тегам