В чем разница между .ToList (), .AsEnumerable (), AsQueryable ()?

182

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

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

  1. Что конкретно означают эти методы?

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

  3. Почему бы, например, использовать .ToList().AsQueryable()вместо .AsQueryable()?

Амин Саки
источник

Ответы:

354

Об этом можно многое сказать. Позвольте мне сосредоточиться на 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>- часто используется для этой цели. Преимущество использования AsEnumerablevs. ToListзаключается в том AsEnumerable, что запрос не выполняется. AsEnumerableсохраняет отложенное выполнение и не создает часто бесполезный промежуточный список.

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

AsQueryableможет использоваться для того, чтобы перечисляемая коллекция принимала выражения в операторах 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, который filters ( Where) и projects ( 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) })

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

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

Восстановить IQueryable возможности

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

context.Observations.AsEnumerable()
                    .AsQueryable()

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

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

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

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

SelectПроизводит 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реализации.

казнить

Оба AsQueryableи AsEnumerableне выполняют (или перечисляют ) исходный объект. Они только меняют свой тип или представление. Оба задействованных интерфейса, IQueryableи IEnumerable, являются ничем иным, как «перечислением, ожидающим произойти». Они не выполняются до того, как их заставляют, например, как упоминалось выше, путем вызова ToList().

Это означает , что выполнение IEnumerableполучить, позвонив AsEnumerableна IQueryableобъект, будет выполнять лежащий в основе IQueryable. Последующее выполнение IEnumerableснова будет выполнять IQueryable. Который может быть очень дорогим.

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

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

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

Герт Арнольд
источник
Спасибо за ответ, не могли бы вы поделиться своим ответом на 3-й вопрос OP - 3. Почему бы использовать, например, .ToList (). AsQueryable () вместо .AsQueryable ()? ?
КГЗДЕВ
@ikram Я не могу думать ни о чем, где это было бы полезно. Как я объяснил, применение AsQueryable()часто основано на заблуждении. Но я позволю себе немного расслабиться и посмотрим, смогу ли я добавить больше освещения по этому вопросу.
Герт Арнольд
1
Отличный ответ. Не могли бы вы уточнить, что произойдет, если IEnumerable, полученный вызовом AsEnumerable () для IQueryable, будет перечислен несколько раз? Будет ли запрос выполняться несколько раз или данные, уже загруженные из базы данных в память, будут использованы повторно?
Антонин
@antoninod Хорошая идея. Готово.
Герт Арнольд
46

К списку()

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

AsEnumerable ()

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

AsQueryable ()

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

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

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

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

Ашутош Райна
источник
2
«AsQueryable () выполнит все удаленно», только если перечислимое уже является запрашиваемым. В противном случае это невозможно, и все по- прежнему выполняется локально. В вопросе есть «.... ToList (). AsQueryable ()», который может использовать некоторые пояснения в вашем ответе, IMO.
2

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

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();
Rm558
источник