Flatten IEnumerable <IEnumerable <>>; понимание дженериков
Я написал этот метод расширения (который компилирует):
public static IEnumerable<J> Flatten<T, J>(this IEnumerable<T> @this)
where T : IEnumerable<J>
{
foreach (T t in @this)
foreach (J j in t)
yield return j;
}
Приведенный ниже код вызывает ошибку времени компиляции (не найдено подходящего метода), почему?:
IEnumerable<IEnumerable<int>> foo = new int[2][];
var bar = foo.Flatten();
Если я реализую расширение, как показано ниже, я не вижу ошибки времени компиляции:
public static IEnumerable<J> Flatten<J>(this IEnumerable<IEnumerable<J>> @this)
{
foreach (IEnumerable<J> js in @this)
foreach (J j in js)
yield return j;
}
Изменить (2). Этот вопрос, на который я отвечаю, ответил, но он поднял еще один вопрос относительно разрешения перегрузки и ограничений типа. Этот вопрос, который я поставил здесь: Почему не являются ограничения типа частью подписи метода?
Ответы
Ответ 1
Во-первых, вам не нужно Flatten()
; этот метод уже существует и называется SelectMany()
. Вы можете использовать его следующим образом:
IEnumerable<IEnumerable<int>> foo = new [] { new[] {1, 2}, new[] {3, 4} };
var bar = foo.SelectMany(x => x); // bar is {1, 2, 3, 4}
Во-вторых, ваша первая попытка не работает, потому что вывод типового типа работает только на основе аргументов метода, а не общих ограничений, связанных с этим методом. Поскольку нет аргументов, которые напрямую используют общий параметр J
, механизм вывода типа не может угадать, что должен быть J
, и, следовательно, не считает, что ваш метод является кандидатом.
Это наглядно показывает, как обходится SelectMany()
: для этого требуется дополнительный аргумент Func<TSource, TResult>
. Это позволяет механизму вывода типа определять оба типа общих типов, поскольку они оба доступны только на основе аргументов, предоставляемых методу.
Ответ 2
dlev ответ в порядке; Я просто подумал, что добавлю немного больше информации.
В частности, я отмечаю, что вы пытаетесь использовать generics для реализации своего рода ковариации на IEnumerable<T>
. В С# 4 и выше IEnumerable<T>
уже является ковариантным.
Ваш второй пример иллюстрирует это. Если у вас есть
List<List<int>> lists = whatever;
foreach(int x in lists.Flatten()) { ... }
тогда выведите вывод о том, что List<List<int>>
конвертируется в IE<List<int>>
, List<int>
конвертируется в IE<int>
, и поэтому из-за ковариации IE<List<int>>
можно конвертировать в IE<IE<int>>
. Это дает тип вывода что-то продолжать; он может заключить, что T является int, и все хорошо.
Это не работает на С# 3. Жизнь немного сложнее в мире без ковариации, но вы можете обойтись разумным использованием метода расширения Cast<T>
.