Вывод универсального типа в C# по сравнению с ковариацией - ошибка или ограничение
Когда универсальный метод с зависимыми параметрами выводит тип, в некоторых случаях он дает неожиданные результаты. Если я указываю тип явно, все работает без каких-либо дальнейших изменений.
IEnumerable<List<string>> someStringGroups = null; // just for demonstration
IEqualityComparer<IEnumerable<string>> someSequenceComparer = null;
var grouped = someStringGroups
.GroupBy(x => x, someSequenceComparer);
Конечно, приведенный выше код не предназначен для выполнения, но он демонстрирует, что тип результата grouped
является IEnumerable<IEnumerable<string>,List<string>>
скорее, чем IEnumerable<List<string>,List<string>>
как и следовало ожидать из-за x => x
,
Если я указываю типы явно, все в порядке.
var grouped = someStringGroups
.GroupBy<List<string>,List<string>>(x => x, someSequenceComparer);
Если я не использую явный компаратор, все тоже работает как исключение.
Я думаю, что проблема заключается в том, что взять наименьший общий знаменатель из предоставленных типов аргументов (IEnumerable<string>
) имеет приоритет над ковариацией IEqualityComparer<>
интерфейс. Я бы ожидал обратного, то есть универсальный метод должен выводить наиболее конкретный тип, который удовлетворяется аргументами.
Вопрос: это ошибка или документированное поведение?
2 ответа
Я бы ожидал обратного, то есть универсальный метод должен выводить наиболее конкретный тип, который удовлетворяется аргументами.
На основании чего именно?
Поведение, которое вы видите, задокументировано и соответствует спецификации C#. Как вы можете себе представить, спецификация вывода типов довольно сложна. Я не буду здесь цитировать все это, но вы можете сами просмотреть его, если вам интересно. Соответствующий раздел - 7.5.2. Вывод типа.
Основываясь на комментариях, которые вы написали, я думаю, что, по крайней мере, часть путаницы связана с тем, что вы забыли, что в этом методе есть три параметра, а не два (что влияет на ход вывода). Кроме того, кажется, вы ожидаете второго параметра, keySelector
делегировать, чтобы влиять на вывод типа, когда это не так в этом случае (по крайней мере, не напрямую… это создает зависимость между параметрами типа, но не материально).
Но я думаю, что главное в том, что вы ожидаете, что вывод типа будет более агрессивным в отношении дисперсии типа, чем на самом деле требует спецификация.
При выводе типа первое, что происходит, описывается в спецификации в разделе 7.5.2.1. Первая фаза. Второй аргумент, для всех намерений и целей на этом этапе, игнорируется. Он не имеет явно объявленных типов для своих параметров (хотя это не имеет значения, если бы он был). На этом этапе вывод типа начинает разрабатывать границы для параметров типа, но не фиксирует сами параметры.
Вы называете эту перегрузку GroupBy()
:
public static IEnumerable<IGrouping<TKey, TSource>> GroupBy<TSource, TKey>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector,
IEqualityComparer<TKey> comparer)
Есть два типа параметров, которые должны быть выведены, TSource
а также TKey
, Во время вывода верно, что компилятор определяет верхнюю и нижнюю границы для параметров типа. Но они основаны на типах, переданных в вызов метода. Компилятор не ищет альтернативные базовые или производные типы, которые удовлетворяли бы требованиям типа.
Таким образом, для TSource
нижняя граница List<string>
определяется, а для TKey
верхняя граница IEnumerable<string>
идентифицировано (7.5.2.9 Нижние границы выводов). Эти типы - то, что вы предоставили для вызова, так что это то, что использует компилятор.
На втором этапе делается попытка исправить типы. TSource
не зависит ни от какого другого параметра, поэтому он фиксируется первым, так как List<string>
, Второй уход на второй этап исправляет TKey
, В то время как типовая дисперсия позволяет границы, установленные для TKey
разместить List<string>
, в этом нет необходимости, потому что в соответствии с его границами тип, который вы передали, может использоваться напрямую.
Таким образом, вы получите IEnumerable<string>
вместо.
Конечно, это было бы законно (если не соответствует спецификации) для использования компилятором List<string>
как TKey
вместо. Мы можем увидеть эту работу, если параметр явно приведен соответственно:
var grouped2 = someStringGroups
.GroupBy(x => x, (IEqualityComparer<List<string>>)someSequenceComparer);
Это изменяет тип выражения, используемого для вызова, таким образом, используемые границы и, наконец, фактический тип, выбранный во время вывода. Но в исходном вызове компилятору не нужно было во время вывода использовать тип, отличный от того, который вы указали, даже если бы он был разрешен, и поэтому он этого не сделал.
В спецификации C# есть несколько довольно волосатых частей. Вывод типа определенно является одним из них, и, честно говоря, я не эксперт в интерпретации этого раздела спецификации. Это заставляет мою голову болеть, и, безусловно, есть еще несколько сложных случаев, которые я, вероятно, не понимаю (т.е. я сомневаюсь, что смогу реализовать эту часть спецификации без дополнительного изучения). Но я считаю, что вышеизложенное является правильной интерпретацией частей, имеющих отношение к вашему вопросу, и я надеюсь, что я проделал разумную работу, объясняя это.
Я был бы уверен, что это ожидаемое поведение.
Интересующая нас сигнатура метода:
public static IEnumerable<IGrouping<TKey, TSource>> GroupBy<TSource, TKey>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector,
IEqualityComparer<TKey> comparer
)
source
имеет тип IEnumerable<List<string>>
поэтому естественный выбор для TSource
является List<string>
, comparer
имеет тип IEqualityComparer<IEnumerable<string>>
поэтому естественный выбор для TKey
является IEnumerable<string>
,
Если мы тогда посмотрим на последний параметр keySelector
является x=>x
, Удовлетворяет ли это ограничения типа, которые у нас есть? Да, это потому, что х List<string>
и это может быть неявно преобразовано в IEnumerable<string>
,
На этом этапе, почему компилятор должен искать что-то еще? Естественный и очевидный выбор без необходимости кастинга работает, поэтому он использует это. Если вам это не нравится, у вас всегда есть возможность сделать то, что вы сделали, и явно указать общие параметры.
Или, конечно, вы можете сделать свой тип сравнения IEqualityComparer<List<string>>
в этом случае ваш выходной объект будет того типа, который вы ожидаете (и я надеюсь, вы поймете, почему это так).