Какая разница между .ToList(),.AsEnumerable(), AsQueryable()?

Я знаю некоторые отличия LINQ к объектам и LINQ к объектам, которые первый реализует IQueryable, а второй реализует IEnumerable, а область моего вопроса находится в пределах EF 5.

Мой вопрос заключается в том, какие технические отличия этих трех методов? Я вижу, что во многих ситуациях все они работают. Я также вижу использование их комбинаций как .ToList().AsQueryable().

  • Что именно означают эти методы?

  • Есть ли проблема с производительностью или что-то, что может привести к использованию одного над другим?

  • Зачем использовать, например, .ToList().AsQueryable() вместо .AsQueryable()?

Ответ 1

Об этом можно многое сказать. Позвольте мне сосредоточиться на AsEnumerable и AsQueryable и упомянуть ToList() на этом пути.

Что делают эти методы?

AsEnumerable и AsQueryable лить или преобразовать в IEnumerable или IQueryable, соответственно. Я говорю о том, чтобы отличить или конвертировать по одной причине:

  • Когда исходный объект уже реализует целевой интерфейс, сам исходный объект возвращается, но переходит к целевому интерфейсу. Другими словами: тип не изменяется, но тип времени компиляции.

  • Когда исходный объект не реализует целевой интерфейс, исходный объект преобразуется в объект, реализующий целевой интерфейс. Таким образом изменяются тип и тип времени компиляции.

Позвольте мне показать это с некоторыми примерами. У меня есть этот небольшой метод, который сообщает тип времени компиляции и фактический тип объекта (любезность Jon Skeet):

void ReportTypeProperties<T>(T obj)
{
    Console.WriteLine("Compile-time type: {0}", typeof(T).Name);
    Console.WriteLine("Actual type: {0}", obj.GetType().Name);
}

Попробуем произвольный linq-to-sql Table<T>, который реализует IQueryable:

ReportTypeProperties(context.Observations);
ReportTypeProperties(context.Observations.AsEnumerable());
ReportTypeProperties(context.Observations.AsQueryable());

Результат:

Compile-time type: Table`1
Actual type: Table`1

Compile-time type: IEnumerable`1
Actual type: Table`1

Compile-time type: IQueryable`1
Actual type: Table`1

Вы видите, что сам класс таблицы всегда возвращается, но его представление изменяется.

Теперь объект, реализующий IEnumerable, а не IQueryable:

var ints = new[] { 1, 2 };
ReportTypeProperties(ints);
ReportTypeProperties(ints.AsEnumerable());
ReportTypeProperties(ints.AsQueryable());

Результаты:

Compile-time type: Int32[]
Actual type: Int32[]

Compile-time type: IEnumerable`1
Actual type: Int32[]

Compile-time type: IQueryable`1
Actual type: EnumerableQuery`1

Вот оно. AsQueryable() преобразовал массив в EnumerableQuery, который "представляет коллекцию IEnumerable<T> в качестве источника данных IQueryable<T>". (MSDN).

Какое использование?

AsEnumerable часто используется для перехода от любой реализации IQueryable к LINQ к объектам (L2O), главным образом потому, что первая не поддерживает функции, которые имеет L2O. Подробнее см. Что такое эффект AsEnumerable() в объекте LINQ?.

Например, в запросе Entity Framework мы можем использовать только ограниченное число методов. Поэтому, если, например, нам нужно использовать один из наших собственных методов в запросе, мы обычно пишем что-то вроде

var query = context.Observations.Select(o => o.Id)
                   .AsEnumerable().Select(x => MySuperSmartMethod(x))

ToList - который преобразует IEnumerable<T> в List<T> - часто используется для этой цели. Преимущество использования AsEnumerable vs. ToList заключается в том, что AsEnumerable не выполняет запрос. AsEnumerable сохраняет отложенное выполнение и не создает часто бесполезный промежуточный список.

С другой стороны, когда принудительное выполнение запроса LINQ желательно, ToList может быть способом сделать это.

AsQueryable можно использовать, чтобы сделать перечисляемые коллекции accept выражениями в операторах LINQ. Подробнее см. Здесь Мне действительно нужно использовать AsQueryable() для коллекции?.

Примечание о злоупотреблении психоактивными веществами!

AsEnumerable работает как наркотик. Это быстрое исправление, но по цене и не устраняет основной проблемы.

Во многих ответах я вижу людей, которые применяют AsEnumerable, чтобы исправить практически любую проблему с неподдерживаемыми методами в выражениях LINQ. Но цена не всегда ясна. Например, если вы это сделаете:

context.MyLongWideTable // A table with many records and columns
       .Where(x => x.Type == "type")
       .Select(x => new { x.Name, x.CreateDate })

... все аккуратно переведено в оператор SQL, который фильтрует (Where) и проекты (Select). То есть, как длина, так и ширина, соответственно, набора результатов SQL уменьшаются.

Теперь предположим, что пользователи хотят видеть только часть даты CreateDate. В Entity Framework вы быстро обнаружите, что...

.Select(x => new { x.Name, x.CreateDate.Date })

... не поддерживается (на момент написания). Ах, к счастью, исправление AsEnumerable:

context.MyLongWideTable.AsEnumerable()
       .Where(x => x.Type == "type")
       .Select(x => new { x.Name, x.CreateDate.Date })

Конечно, он работает, наверное. Но он извлекает всю таблицу в память, а затем применяет фильтр и прогнозы. Ну, большинство людей достаточно умны, чтобы сначала выполнить Where:

context.MyLongWideTable
       .Where(x => x.Type == "type").AsEnumerable()
       .Select(x => new { x.Name, x.CreateDate.Date })

Но все же сначала берутся все столбцы, а проецирование выполняется в памяти.

Реальное решение:

context.MyLongWideTable
       .Where(x => x.Type == "type")
       .Select(x => new { x.Name, DbFunctions.TruncateTime(x.CreateDate) })

(Но для этого требуется немного больше знаний...)

Что не делают эти методы?

Теперь важное предостережение. Когда вы делаете

context.Observations.AsEnumerable()
                    .AsQueryable()

вы получите исходный объект, представленный как IQueryable. (Потому что оба метода только сбрасывают и не конвертируют).

Но когда вы делаете

context.Observations.AsEnumerable().Select(x => x)
                    .AsQueryable()

каков будет результат?

Select создает a WhereSelectEnumerableIterator. Это внутренний класс .Net, который реализует IEnumerable, а не IQueryable. Таким образом, произошло преобразование в другой тип, и последующий AsQueryable больше не сможет вернуть исходный источник.

Следствием этого является то, что использование AsQueryable не способ магического ввода поставщика запросов с его конкретными функциями в перечислимый. Предположим, вы делаете

var query = context.Observations.Select(o => o.Id)
                   .AsEnumerable().Select(x => x.ToString())
                   .AsQueryable()
                   .Where(...)

Условие where никогда не будет переведено в SQL. AsEnumerable(), за которым следуют инструкции LINQ, окончательно сокращает соединение с провайдером запросов сущностей.

Я преднамеренно показываю этот пример, потому что я видел вопросы здесь, где люди, например, пытаются "внедрить" Include возможности в коллекцию, вызвав AsQueryable. Он компилируется и запускается, но ничего не делает, потому что базовый объект больше не имеет реализации Include.

Конкретные реализации

До сих пор это было только о Queryable.AsQueryable и Enumerable.AsEnumerable. Но, конечно, любой может писать методы экземпляра или методы расширения с одинаковыми именами (и функциями).

Фактически, общим примером конкретного метода расширения AsEnumerable является DataTableExtensions.AsEnumerable. DataTable не реализует IQueryable или IEnumerable, поэтому регулярные методы расширения не применяются.

Ответ 2

ToList()

  • Выполнить запрос немедленно

AsEnumerable()

  • lazy (выполнить запрос позже)
  • Параметр: Func<TSource, bool>
  • Загрузите КАЖДОЕ запись в память приложения, а затем обработайте/отфильтруйте их. (например, Where/Take/Skip, он выберет * из таблицы1, в память, затем выберите первые X-элементы) (В этом случае, что он сделал: Linq-to-SQL + Linq-to-Object)

AsQueryable()

  • lazy (выполнить запрос позже)
  • Параметр: Expression<Func<TSource, bool>>
  • Преобразовать выражение в T-SQL (с конкретным провайдером), запросить удаленно и загрузить результат в вашу память приложения.
  • Вот почему DbSet (в Entity Framework) также наследует IQueryable для получения эффективного запроса.
  • Не загружайте каждую запись, например. если Take (5), он будет генерировать select top 5 * SQL в фоновом режиме. Это означает, что этот тип более дружелюбен к базе данных SQL, поэтому этот тип обычно имеет более высокую производительность и рекомендуется при работе с базой данных.
  • Так что AsQueryable() обычно работает намного быстрее, чем AsEnumerable(), поскольку он сначала генерирует T-SQL, который включает все ваши условия, в которых есть условия в Linq.

Ответ 3

ToList() будет все в памяти, а затем вы будете работать над ним. поэтому, ToList(). где (применять некоторый фильтр) выполняется локально. AsQueryable() будет выполнять все дистанционно, то есть фильтр на нем отправляется в базу данных для применения. Queryable ничего не делает, пока вы его не выполняете. ToList, однако выполняется немедленно.

Кроме того, посмотрите на этот ответ Зачем использовать AsQueryable() вместо List()?.

ИЗМЕНИТЬ: Кроме того, в вашем случае, когда вы делаете ToList(), каждая последующая операция является локальной, включая AsQueryable(). Вы не можете переключиться на удаленный, как только вы начнете выполнять локально. Надеюсь, что это немного улучшится.

Ответ 4

Обнаружена плохая производительность в приведенном ниже коде.

void DoSomething<T>(IEnumerable<T> objects){
    var single = objects.First(); //load everything into memory before .First()
    ...
}

Исправлено с помощью

void DoSomething<T>(IEnumerable<T> objects){
    T single;
    if (objects is IQueryable<T>)
        single = objects.AsQueryable().First(); // SELECT TOP (1) ... is used
    else
        single = objects.First();

}

Для IQueryable оставайтесь в IQueryable, когда это возможно, старайтесь не использовать его, как IEnumerable.

Обновление. Это можно еще больше упростить в одном выражении, спасибо Герт Арнольд.

T single =  objects is IQueryable<T> q? 
                    q.First(): 
                    objects.First();