Foreach использует некоторую «магию», чтобы выяснить, какой тип зациклить Не требуется, чтобы коллекция поддерживала IEnumerable
чего-либо. Это, вероятно, объясняет разницу.
Проблема в том, что компилятор не может определить параметр общего типа для Where
. Если вы сделаете это, оно будет работать:
IEnumerable<DerivedRecord> ie = derivedCollection;
ie.Where(d => d.DerivedProperty == "");
Никакие броски не используются, но набор доступных вариантов был уменьшен до одного.
Или вы можете указать параметр типа явно:
derivedCollection.Where<DerivedRecord>(d => d.DerivedProperty == "");
Я думаю, что для решения вашей проблемы вам придется прекратить BaseCollection
наследовать другую версию IEnumerable<T>
:
abstract class BaseCollection
{
private readonly Collection<BaseRecord> _realCollection = new Collection<BaseRecord>();
public void Add(BaseRecord rec)
{
_realCollection.Add(rec);
}
public IEnumerable<BaseRecord> Items
{
get { return _realCollection; }
}
}
Там я все еще создаю экземпляр Collection<T>
, но как отдельный объект вместо базового класса. Затем перенаправьте его на него, чтобы имитировать его API по мере необходимости.
Производная универсальная версия должна полностью реализовывать IEnumerable<T>
.
class BaseCollection<TRecord> : BaseCollection, IEnumerable<TRecord>
where TRecord : BaseRecord,new()
{
public IEnumerator<TRecord> GetEnumerator()
{
return Items.Cast<TRecord>().GetEnumerator();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
Тогда остальная часть вашего примера кода прекрасно работает без обходных путей, которые я публиковал изначально, потому что у него есть только одна реализация IEnumerable<T>
для работы.
Обновление, более подробная информация
Вот действительно упрощенная версия той же ситуации, разрывая все ссылки с помощью IEnumerable
, объявляя все сами с нуля, чтобы мы могли видеть, что происходит.
public class R1 { }
public class R2 { }
public interface I<T> { }
public class C1<T> : I<T> { }
public class C2 : C1<R1>, I<R2> { }
class Program
{
public static I<T> M<T>(I<T> i) { return i; }
static void Main(string[] args)
{
var c2 = new C2();
var v = M(c2); // Compiler error - no definition for M
}
}
R1
и R2
похожи на два класса записей. В вашей версии R2
наследует R1
- но это не относится к этой проблеме, поэтому я ее опустил. I<T>
- это общий интерфейс, обозначающий IEnumerable<T>
. Но моя версия не имеет методов! Они также не имеют отношения к этой проблеме.
Затем я свернул вашу систему наследования классов коллекции до двух уровней. Базовый класс C1
реализует I<T>
, а затем производный класс C2
решает попросить C2
реализовать I<R1>
, а затем также реализует I<R2>
напрямую. Количество слоев также не имеет значения. Кроме того, ограничения типов не объявлены с where
, поэтому они также здесь опущены.
В результате у C2
есть две реализации I<T>
: I<R1>
и I<R2>
. Один он наследует от C1
, другой добавляет сам.
Наконец, у меня есть метод M
, который заменяет Where
в Linq. Это не обязательно должен быть метод расширения, чтобы показать проблему, поэтому для ясности я сделал его обычным статическим методом.
Поэтому, когда мы приходим к вызову нашего метода M
, компилятор должен выяснить, что такое T
. Это достигается путем просмотра того, что мы передали в качестве единственного параметра методу, который должен поддерживать I<T>
. К сожалению, мы передаем что-то, что поддерживает I<R1>
и I<R2>
, так как же процесс вывода типов может сделать выбор между ними? Не может.
Поскольку в моем интерфейсе нет методов, четкое указание new
перед методом не поможет мне, и поэтому оно не поможет вам. Проблема заключается не в том, чтобы решить, какой метод в интерфейсе вызывать, а в том, следует ли трактовать аргумент для M
как I<R1>
или I<R2>
.
Почему компилятор не сообщает об этом как о проблеме определения типа? В соответствии со спецификацией C # 3.0, сначала не выполняется вывод типа, чтобы создать набор доступных перегрузок, а затем выполняется разрешение перегрузки, чтобы выбрать лучший выбор. Если вывод типа не может сделать выбор между двумя возможными расширениями универсального метода, он устраняет оба, поэтому разрешение перегрузки даже не видит их, поэтому в сообщении об ошибке говорится, что нет никакого применимого метода с именем M
.
(Но если у вас есть Resharper, у него есть собственный компилятор, который он использует, чтобы выдавать более подробные ошибки в IDE, и в этом случае он специально говорит: «Аргументы типа для метода M не могут быть выведены из использования». )
Теперь, почему foreach
отличается? Потому что это даже не безопасно типа! Это относится ко времени, когда дженерики были добавлены. Он даже не смотрит на интерфейсы. Он просто ищет открытый метод с именем GetEnumerator
любого типа, через который он проходит. Например:
public class C
{
public IEnumerator GetEnumerator() { return null; }
}
Это очень хорошая коллекция для компилятора! (Конечно, он взорвется во время выполнения, потому что возвращает ноль IEnumerator
.) Но обратите внимание на отсутствие IEnumerable
или каких-либо обобщений. Это означает, что foreach
выполняет неявное приведение.
Таким образом, чтобы связать это с вашим кодом, у вас есть «down down» с BaseRecord
до DerivedRecord
, который вы реализуете с помощью оператора Cast
от Linq. Ну, в любом случае foreach
делает это для вас. В моем примере выше C
фактически представляет собой набор элементов типа object
. И все же я могу написать:
foreach (string item in new C())
{
Console.WriteLine(item.Length);
}
Компилятор успешно вставляет бесшумное приведение от object
до string
. Эти предметы могут быть чем угодно ... Тьфу!
Именно поэтому появление var
замечательно - всегда используйте var
для объявления вашей переменной цикла foreach
, и таким образом компилятор не будет вставлять приведение. Это сделает переменную наиболее специфического типа, который она может вывести из коллекции во время компиляции.