Реализация IEnumerable <T>работает для foreach, но не для LINQ. - PullRequest
3 голосов
/ 25 января 2010

В настоящее время я использую следующую базовую коллекцию.

  abstract class BaseCollection : Collection<BaseRecord>
  {       
  }

Я хотел бы заменить его на общую коллекцию ниже. Это, однако, не является тривиальной задачей из-за количества производных классов, которые в настоящее время реализуют BaseCollection.

  abstract class BaseCollection<TRecord> : Collection<TRecord> where TRecord : BaseRecord,new()
  {

  }

Вместо масштабного ремонта я бы хотел медленно представить эту новую коллекцию.

   abstract class BaseCollection<TRecord> : BaseCollection,IEnumerable<TRecord> where TRecord : BaseRecord,new()
   {
      public new IEnumerator<TRecord> GetEnumerator()
      {
         return Items.Cast<TRecord>().GetEnumerator(); 
      }
   }

Хотя я могу перечислять коллекцию с помощью foreach, оператор LINQ, приведенный ниже, не компилируется. Это возможно? Я понимаю, что это немного взломать, но я не уверен, как еще это сделать.

   class Program
   {
      static void Main(string[] args)
      {
         DerivedCollection derivedCollection = new DerivedCollection
         {
            new DerivedRecord(),
            new DerivedRecord()
         };
         foreach (var record in derivedCollection)
         {
            record.DerivedProperty = "";
         }
         var records = derivedCollection.Where(d => d.DerivedProperty == "");
      }
   }

Вот две записи, использованные выше. Благодарю.

   class DerivedRecord : BaseRecord
   {
      public string DerivedProperty { get; set; }
   }

   abstract class BaseRecord
   {
      public string BaseProperty { get; set; }
   }

Вот производная коллекция.

   class DerivedCollection : BaseCollection<DerivedRecord>
   {

   }

Ответы [ 3 ]

4 голосов
/ 25 января 2010

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, и таким образом компилятор не будет вставлять приведение. Это сделает переменную наиболее специфического типа, который она может вывести из коллекции во время компиляции.

0 голосов
/ 10 декабря 2014

Оператор, вероятно, уже решил эту проблему, но если он кому-то поможет, ваш существующий Linq ломается, когда вы пытаетесь использовать BaseCollection, потому что он имеет два типа IEnumerable: IEnumerable<TRecord> и IEnumerable<BaseRecord> (унаследовано от BaseCollection -> Collection<BaseRecord>). Чтобы ваш существующий Linq компилировался с двумя типами IEnumerable, определите, какой IEnumerable вы хотите использовать в Linq, реализовав IQueryable<TypeYouWantToUse>.

abstract class BaseCollection<TRecord> : BaseCollection, IEnumerable<TRecord>, IQueryable<TRecord> where TRecord : BaseRecord, new()
{
    public new IEnumerator<TRecord> GetEnumerator()
    {
        return Items.Cast<TRecord>().GetEnumerator();
    }

    #region IQueryable<TRecord> Implementation
    public Type ElementType
    {
        get { return typeof(TRecord); }
    }

    public System.Linq.Expressions.Expression Expression
    {
        get { return this.ToList<TRecord>().AsQueryable().Expression; }
    }

    public IQueryProvider Provider
    {
        get { return this.ToList<TRecord>().AsQueryable().Provider; }
    }
    #endregion
}
0 голосов
/ 25 января 2010

Еще страшнее, но работает:

(IEnumerable<DerivedRecord>)derivedCollection).Where(d => d.DerivedProperty == "")
...