Ковариация и контравариантность в С#
Начну с того, что я являюсь разработчиком Java для программирования на С#. Поэтому я сравниваю то, что знаю с тем, чему я учусь.
Я играю с генераторами С# в течение нескольких часов, и я смог воспроизвести те же самые вещи, которые я знаю в Java на С#, за исключением нескольких примеров, использующих ковариацию и контравариантность. Книга, которую я читаю, не очень хороша в этом вопросе. Я обязательно буду искать дополнительную информацию в Интернете, но пока я это делаю, возможно, вы можете помочь мне найти реализацию С# для следующего кода 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);
Теперь я обнаружил, что С# поддерживает ковариацию/контравариантность только на интерфейсах и до тех пор, пока они явно объявлены для этого (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);
Моя основная проблема, пытаясь реализовать это на С#, заключается в том, что я не мог найти интерфейс, который одновременно был ковариантным и контравариантным, так как это случай List в моем примере выше. Возможно, это может быть сделано с двумя различными интерфейсами в С#.
Любые мысли о том, как реализовать это?
Благодарим всех за любые ответы, которые вы можете внести. Я уверен, что я буду многому научиться из любого примера, который вы можете предоставить.
Ответы
Ответ 1
Для второй части вашего вопроса вам не нужна контравариантность, все, что вам нужно сделать, - это указать, что первый тип можно отбросить ко второму. Снова используйте синтаксис 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();
}
Ответ 2
Вместо того, чтобы отвечать на ваши вопросы напрямую, я собираюсь ответить на несколько несколько разные вопросы:
Есть ли у С# способ обобщения типов, поддерживающих арифметические операторы?
Не легко, нет. Было бы неплохо иметь способ создания метода Sum<T>
, который мог бы добавлять целые числа, парные, матричные, комплексные числа, кватернионы... и так далее. Хотя это довольно часто запрашиваемая функция, она также является большой функцией, и она никогда не была достаточно высокой в списке приоритетов, чтобы оправдать ее включение в язык. Мне это очень понравилось, но вы не должны ожидать этого в С# 5. Возможно, в гипотетической будущей версии языка.
В чем разница между ковариацией/контравариантностью и сайтом С# "коллаборацией/контравариантностью" сайта "call site" Java?
Фундаментальное различие на уровне реализации - это, конечно, что, как практический вопрос, Java-дженерики реализуются посредством стирания; хотя вы получаете преимущества приятного синтаксиса для типичных типов и проверки типа времени компиляции, вы не обязательно получаете преимущества производительности или преимущества интеграции системы времени выполнения, которые вы бы на С#.
Но это действительно больше деталей реализации. Более интересное отличие от моей перспективы заключается в том, что правила Java-дисперсии применяются локально, а правила отклонения С# применяются глобально.
То есть: некоторые варианты конверсий опасны, потому что они подразумевают, что некоторые небезопасные операции не будут улавливаться компилятором. Классический пример:
- Тигр - это млекопитающее.
- Список X ковариантен в X. (Предположим.)
- Список тигров - это список млекопитающих.
- Список млекопитающих может содержать жирафа.
- Поэтому вы можете вставить жирафа в список тигров.
Что явно нарушает безопасность типа, а также безопасность жирафа.
С# и Java используют два разных метода для предотвращения нарушения безопасности этого типа. С# говорит, что когда объявлен интерфейс I<T>
, если он объявлен как ковариант, тогда не должно быть никакого метода интерфейса, который принимает в T. Если нет способа вставки T в список, тогда вы никогда не будете вставьте жирафа в список тигров, потому что нет способа вставить что-либо.
Java, напротив, говорит, что на этом локальном сайте мы относимся к типу ковариантно, и мы обещаем не называть какие-либо методы прямо здесь, что может нарушить безопасность типов.
У меня недостаточно опыта работы с Java, чтобы сказать, что "лучше" при каких обстоятельствах. Техника Java, безусловно, интересна.
Ответ 3
Вы можете использовать интерфейс 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
в Java.
Ответ 4
В .NET нет базового класса Number
. Самое близкое, что вы можете получить, может выглядеть примерно так:
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
и общий тип (и, благодаря Paul Tyng, вы можете заставить 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;
}