.ToList()、. AsEnumerable()和AsQueryable()有什么区别?


182

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

我的问题是这三种方法的技术区别是什么?我看到它们在许多情况下都可以工作。我也看到像使用它们的组合.ToList().AsQueryable()

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

  2. 是否有任何性能问题或某种原因导致使用一个而不是另一个?

  3. 例如,为什么要用一个.ToList().AsQueryable()代替.AsQueryable()


Answers:


354

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

这些方法是做什么的?

AsEnumerableAsQueryable投或转换到IEnumerableIQueryable分别。我说投放或转换是有原因的:

  • 当源对象已经实现目标接口时,将返回源对象本身,但将其强制转换为目标接口。换句话说:类型没有改变,但是编译时类型却是。

  • 当源对象未实现目标接口时,源对象将转换为实现目标接口的对象。因此,类型和编译时类型都被更改。

让我用一些例子来说明这一点。我有这个小方法可以报告对象的编译时类型和实际类型(由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 –将an转换IEnumerable<T>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再也无法返回原始源了。

这个含义是,使用AsQueryable没有办法神奇地注入查询提供其具体特点为枚举。假设你做

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

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

我故意显示此示例,因为在这里我看到了一些问题,例如人们尝试Include通过调用将功能“注入” 到集合中AsQueryable。它可以编译并运行,但是它什么也不做,因为基础对象不再具有Include实现。

执行

两个AsQueryableAsEnumerable不执行(或枚举)源对象。他们仅更改其类型或表示。这两个涉及的接口IQueryableIEnumerable,都不过是“等待发生的枚举”。它们在被迫执行之前没有执行,例如,如上所述,通过调用ToList()

这意味着,执行一个IEnumerable通过调用获得AsEnumerable一个上IQueryable对象,将执行的底层IQueryable。后续执行IEnumerable将再次执行IQueryable。这可能是非常昂贵的。

具体实施

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

实际上,特定AsEnumerable扩展方法的常见示例是DataTableExtensions.AsEnumerableDataTable没有实现IQueryableIEnumerable,因此常规扩展方法不适用。


感谢您的回答,能否请您分享对OP- 3的第三个问题的回答。为什么要使用.ToList()。AsQueryable()而不是.AsQueryable()?
kgzdev

@ikram我想不出任何有用的地方。正如我所解释的,申请AsQueryable()通常是基于误解。但是,我会让自己在脑后一下,看看我是否可以在这个问题上添加更多内容。
Gert Arnold

1
好答案。如果将IQueryable上调用AsEnumerable()获得的IEnumerable枚举多次,会发生什么情况?该查询将执行多次,还是将已经从数据库加载到内存中的数据重用?
antoninod

@antoninod好主意。做完了
Gert Arnold

46

ToList()

  • 立即执行查询

AsEnumerable()

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

AsQueryable()

  • 懒惰的(稍后执行查询)
  • 参数: Expression<Func<TSource, bool>>
  • 将Expression转换为T-SQL(使用特定的提供程序),远程查询并将结果加载到您的应用程序内存中。
  • 这就是DbSet(在Entity Framework中)还继承IQueryable以获得高效查询的原因。
  • 不要加载每个记录,例如,如果Take(5),它将在后台生成select top 5 * SQL。这意味着此类型对SQL数据库更友好,这就是为什么此类型通常具有更高的性能,并且在处理数据库时推荐使用的原因。
  • 因此AsQueryable()通常比起AsEnumerable()最初生成T-SQL(包括您的Linq中的所有where条件)时要快得多。

14

ToList()将成为内存中的所有内容,然后您将对其进行处理。因此,ToList()。where(应用一些过滤器)在本地执行。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那样使用。

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

T single =  objects is IQueryable<T> q? 
                    q.First(): 
                    objects.First();
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.