Ковариантность и Контравариантность в 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;
}