203

IQueryable我知道第一个实现和第二个实现的 LINQ to Entities 和 LINQ to Objects 的一些区别,IEnumerable我的问题范围在 EF 5 内。

我的问题是这三种方法的技术差异是什么?我看到在许多情况下它们都有效。我还看到使用它们的组合,例如.ToList().AsQueryable().

  1. 这些方法到底是什么意思?

  2. 是否存在任何性能问题或会导致使用其中一个的问题?

  3. 例如,为什么要使用.ToList().AsQueryable()而不是.AsQueryable()

4

4 回答 4

412

关于这一点有很多话要说。让我重点关注AsEnumerable并一路AsQueryable提及。ToList()

这些方法有什么作用?

AsEnumerable并分别AsQueryable转换或转换为IEnumerableor 。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 实体有什么影响?.

例如,在实体框架查询中,我们只能使用有限数量的方法。因此,例如,如果我们需要在查询中使用我们自己的方法之一,我们通常会编写类似

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

ToList - 将 a 转换IEnumerable<T>为 a List<T>- 也经常用于此目的。使用AsEnumerablevs.的优点ToListAsEnumerable不执行查询。AsEnumerable保留延迟执行,并且不会构建通常无用的中间列表。

另一方面,当需要强制执行 LINQ 查询时,ToList可能是一种方法。

AsQueryable可用于使可枚举集合接受 LINQ 语句中的表达式。有关更多详细信息,请参见此处:我真的需要在收集时使用 AsQueryable() 吗?.

注意滥用药物!

AsEnumerable像药物一样工作。这是一个快速修复,但需要付出代价,并且不能解决根本问题。

在许多 Stack Overflow 答案中,我看到人们申请AsEnumerable解决几乎所有与 LINQ 表达式中不受支持的方法有关的问题。但价格并不总是很清楚。例如,如果您这样做:

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

...所有内容都被巧妙地转换为过滤( Where) 和项目( Select) 的 SQL 语句。也就是说,SQL 结果集的长度和宽度都被分别减小了。

现在假设用户只想查看CreateDate. 在实体框架中,您会很快发现...

.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将永远无法返回原始源。

这意味着 using并不是一种将具有特定功能的查询提供程序神奇地注入AsQueryable可枚举的方法。假设你这样做

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

where 条件永远不会被翻译成 SQL。AsEnumerable()紧随其后的 LINQ 语句最终切断了与实体框架查询提供程序的连接。

我特意展示了这个例子,因为我在这里看到了一些问题,例如人们试图Include通过调用AsQueryable. 它编译并运行,但它什么也不做,因为底层对象不再有Include实现。

执行

两者都不执行(或AsQueryable枚举源对象。他们只改变他们的类型或表示。所涉及的接口和都不过是“等待发生的枚举”。它们在被迫执行之前不会被执行,例如,如上所述,通过调用.AsEnumerableIQueryableIEnumerableToList()

这意味着执行IEnumerable通过调用对象获得AsEnumerable的.IQueryable将执行底层的IQueryable. 随后的执行IEnumerable将再次执行IQueryable. 这可能非常昂贵。

具体实现

到目前为止,这只是关于Queryable.AsQueryableEnumerable.AsEnumerable扩展方法。但当然,任何人都可以编写具有相同名称(和函数)的实例方法或扩展方法。

实际上,特定AsEnumerable扩展方法的一个常见示例是DataTableExtensions.AsEnumerable. DataTable没有实现IQueryableor IEnumerable,所以常规的扩展方法不适用。

于 2013-08-01T14:03:19.783 回答
62

列表()

  • 立即执行查询

AsEnumerable()

  • 懒惰(稍后执行查询)
  • 范围:Func<TSource, bool>
  • 将每条记录加载到应用程序内存中,然后处理/过滤它们。(例如Where/Take/Skip,它将从table1中选择*,进入内存,然后选择前X个元素)(在这种情况下,它做了什么:Linq-to-SQL + Linq-to-Object)

AsQueryable()

  • 懒惰(稍后执行查询)
  • 范围:Expression<Func<TSource, bool>>
  • 将 Expression 转换为 T-SQL(使用特定的提供程序),远程查询并将结果加载到您的应用程序内存中。
  • 这就是为什么 DbSet(在实体框架中)也继承 IQueryable 来获得高效查询的原因。
  • 不要加载每条记录,例如如果Take(5),它会在后台生成select top 5 * SQL。这意味着这种类型对 SQL 数据库更友好,这就是为什么这种类型通常具有更高的性能,并且在处理数据库时推荐使用。
  • 所以AsQueryable()通常比AsEnumerable()它最初生成 T-SQL 的速度要快得多,它包括你在 Linq 中的所有 where 条件。
于 2015-09-28T16:29:48.097 回答
15

ToList() 将成为内存中的所有内容,然后您将对其进行处理。因此, ToList().where (应用一些过滤器)在本地执行。AsQueryable() 将远程执行所有操作,即将其上的过滤器发送到数据库进行应用。Queryable 在您执行之前不会做任何事情。然而,ToList 会立即执行。

另外,看看这个答案为什么使用 AsQueryable() 而不是 List()?.

编辑:此外,在您的情况下,一旦您执行 ToList(),那么每个后续操作都是本地的,包括 AsQueryable()。一旦开始在本地执行,就无法切换到远程。希望这使它更清楚一点。

于 2013-07-31T11:07:51.367 回答
3

在下面的代码中遇到了糟糕的性能。

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 那样使用。

更新感谢Gert Arnold,它可以在一个表达式中进一步简化。

T single =  objects is IQueryable<T> q? 
                    q.First(): 
                    objects.First();
于 2019-07-17T19:06:56.170 回答