返回IEnumerable <T>与IQueryable <T>


1084

Return IQueryable<T>与vs 之间的区别是什么IEnumerable<T>,何时应该优先选择另一个?

IQueryable<Customer> custs = from c in db.Customers
where c.City == "<City>"
select c;

IEnumerable<Customer> custs = from c in db.Customers
where c.City == "<City>"
select c;

Answers:


1778

是的,两者都会使您推迟执行

区别在于,IQueryable<T>该接口允许LINQ-to-SQL(实际上是LINQ-to-anything)正常工作。因此,如果您进一步优化了上的查询IQueryable<T>,则该查询将尽可能在数据库中执行。

对于这种IEnumerable<T>情况,它将是LINQ-to-object,这意味着与原始查询匹配的所有对象都必须从数据库加载到内存中。

在代码中:

IQueryable<Customer> custs = ...;
// Later on...
var goldCustomers = custs.Where(c => c.IsGold);

该代码将执行SQL,仅选择金牌客户。另一方面,以下代码将在数据库中执行原始查询,然后过滤掉内存中的非黄金客户:

IEnumerable<Customer> custs = ...;
// Later on...
var goldCustomers = custs.Where(c => c.IsGold);

这是一个非常重要的区别,IQueryable<T>在许多情况下进行操作可以使您避免从数据库返回太多行。另一个主要的示例是进行分页:如果使用on TakeSkipon IQueryable,则将仅获得请求的行数;在上执行此操作IEnumerable<T>将导致您的所有行都加载到内存中。


32
很好的解释。在任何情况下IEnumerable比IQueryable更可取吗?
fjxx 2011年

8
所以可以说,如果我们使用IQueryable来查询Memory Object,那么IEnumerable和IQueryable之间不会有什么区别吗?
塔里克2012年

11
警告:尽管由于进行了优化,所以IQueryable可能是一个诱人的解决方案,但不应允许它通过存储库或服务层。这是为了保护您的数据库免受“堆栈LINQ表达式”引起的开销。
2013年

48
@fjxx是的。如果要对原始结果(多个最终结果)进行重复过滤。在IQueryable接口上执行此操作将多次访问数据库,而在IEnumerable上执行此操作将在内存中进行过滤,从而使其更快(除非数据量很大)
PerHornshøj-Schierbeck2013年

34
喜欢的另一个原因IEnumerableIQueryable是不是所有的LINQ操作都是由所有LINQ提供支持。因此,只要您知道自己在做什么,就可以使用IQueryable将尽可能多的查询推送到LINQ提供程序(LINQ2SQL,EF,NHibernate,MongoDB等)。但是,如果您让其他代码随心所欲地做任何事情,您IQueryable最终将陷入困境,因为某些客户端代码使用了不受支持的操作。我同意不要将IQueryables放到存储库或等效层上的建议。
阿维什(Avish)2014年

302

最佳答案是好的,但没有提到解释两个接口“如何”不同的表达式树。基本上,有两组相同的LINQ扩展。Where()Sum()Count()FirstOrDefault(),等都有两个版本:一个接受函数和一个接受表达式。

  • IEnumerable版本的签名是:Where(Func<Customer, bool> predicate)

  • IQueryable版本的签名是:Where(Expression<Func<Customer, bool>> predicate)

您可能一直在使用这两种方法而没有意识到,因为两者都使用相同的语法来调用:

例如Where(x => x.City == "<City>")在两个IEnumerableIQueryable

  • 在集合Where()上使用时IEnumerable,编译器将已编译的函数传递给Where()

  • 在集合Where()上使用时IQueryable,编译器将表达式树传递给Where()。表达式树类似于反射系统,但用于代码。编译器将您的代码转换为数据结构,该数据结构以易于消化的格式描述代码的功能。

为什么要打扰这个表达树呢?我只想Where()过滤我的数据。 主要原因是EF和Linq2SQL ORM都可以将表达式树直接转换为SQL,从而使您的代码执行得更快。

哦,这听起来像是免费的性能提升,AsQueryable()在这种情况下,我应该在所有地方使用它吗? 否,IQueryable仅当基础数据提供者可以执行某些操作时才有用。将类似常规的东西转换ListIQueryable不会带来任何好处。


9
IMO比接受的答案要好。但是,我一无所获:IQueryable对于常规对象没有任何好处,可以,但是无论如何它会更糟吗?因为即使它没有任何好处,也没有足够的理由偏爱IEnumerable,所以在各处使用IQueryable的想法仍然有效。
谢尔盖·塔切诺夫

1
Sergey,IQueryable扩展了IEnumerable,因此,当使用IQueryable时,与IEnumerable实例化相比,您将更多的内存加载到内存中!所以这是一个论点。(stackoverflow.com/questions/12064828/…c ++,尽管我认为我可以对此进行推断)
Viking

与Sergei商定这是最佳答案(尽管可以接受)。我会补充说,根据我的经验,IQueryable它不会像解析函数那样进行解析IEnumerable:例如,如果您想知道a DBSet<BookEntity>中的哪些元素不在a中List<BookObject>dbSetObject.Where(e => !listObject.Any(o => o.bookEntitySource == e))则会抛出异常:Expression of type 'BookEntity' cannot be used for parameter of type 'BookObject' of method 'Boolean Contains[BookObject] (IEnumerable[BookObject], BookObject)'。我必须在.ToList()之后加上dbSetObject
Jean-David Lanz

80

是的,两者都使用延迟执行。让我们来说明使用SQL Server事件探查器的区别。

当我们运行以下代码时:

MarketDevEntities db = new MarketDevEntities();

IEnumerable<WebLog> first = db.WebLogs;
var second = first.Where(c => c.DurationSeconds > 10);
var third = second.Where(c => c.WebLogID > 100);
var result = third.Where(c => c.EmailAddress.Length > 11);

Console.Write(result.First().UserName);

在SQL Server事件探查器中,我们发现一个命令等于:

"SELECT * FROM [dbo].[WebLog]"

对具有100万条记录的WebLog表运行该代码块大约需要90秒。

因此,所有表记录都作为对象加载到内存中,然后与每个.Where()一起成为内存中针对这些对象的另一个过滤器。

在上面的示例(第二行)中使用IQueryable代替时IEnumerable

在SQL Server事件探查器中,我们发现一个命令等于:

"SELECT TOP 1 * FROM [dbo].[WebLog] WHERE [DurationSeconds] > 10 AND [WebLogID] > 100 AND LEN([EmailAddress]) > 11"

使用大约需要四秒钟来运行此代码块IQueryable

IQueryable具有一个名为的属性Expression,该属性存储一个树表达式,该树表达式result在我们的示例中使用时开始创建(称为延迟执行),最后该表达式将转换为SQL查询以在数据库引擎上运行。


5
这告诉我,当转换为IEnumerable时,基础IQueryable会丢失其IQueryable扩展方法。
宜坪

56

两者都会让您推迟执行,是的。

至于哪个优先于另一个,则取决于您的基础数据源是什么。

返回an IEnumerable将自动迫使运行时使用LINQ to Objects查询您的集合。

返回IQueryableIEnumerable通过顺便实现)提供了额外的功能,可以将您的查询转换为可能在基础源上更好地执行的查询(LINQ to SQL,LINQ to XML等)。


30

一般来说,我建议以下内容:

  • 返回IQueryable<T>如果你想用你的方法来完善你在执行前返回查询,让开发人员。

  • IEnumerable如果要传输一组要枚举的对象,则返回。

想象一下IQueryable它是什么-数据查询(可以根据需要进行优化)。An IEnumerable是一组可以枚举的对象(已经收到或创建)。


2
“可以枚举”,而不是“可以枚举”。
Casey 2014年

28

之前已经说了很多话,但是以一种更为技术性的方式回到了根源:

  1. IEnumerable 是内存中可以枚举的对象的集合 -一个内存中的序列,可以循环访问(使循环内的访问变得容易foreach,尽管IEnumerator只能使用)。它们原样驻留在内存中。
  2. IQueryable 是一个表达树,它将在某个时刻被转换成其他东西,并能够枚举最终结果。我想这就是让大多数人困惑的地方。

它们显然具有不同的含义。

IQueryable表示一个表达式树(简单地说是一个查询),一旦调用发布API,该表达式树将被基础查询提供程序转换为其他内容,例如LINQ聚合函数(Sum,Count等)或ToList [Array,Dictionary,。 ..]。而且IQueryable对象也实现IEnumerableIEnumerable<T>因此,如果它们表示查询,则可以迭代该查询的结果。这意味着IQueryable不必仅是查询。正确的说法是它们是表达树

现在,如何执行这些表达式以及将这些表达式变成什么都取决于所谓的查询提供程序(我们可以想到的表达式执行程序)。

Entity Framework世界中(这是神秘的基础数据源提供程序或查询提供程序),IQueryable表达式被转换为本地T-SQL查询。Nhibernate与他们做类似的事情。您可以按照LINQ中描述的概念编写您自己的概念例如,构建IQueryable Provider链接,并且您可能想要为产品商店提供程序服务使用自定义查询API。

因此,基本上,IQueryable对象一直都在构造,直到我们明确释放它们,并告诉系统将它们重写为SQL或其他内容,然后向下发送执行链以进行后续处理。

好像推迟执行一样LINQ,每当对序列调用某些API(相同的Count,ToList等)时,它的一个功能就是将表达式树方案保留在内存中,仅在需要时才将其发送到执行中。

两者的正确用法在很大程度上取决于您针对特定情况所面临的任务。对于众所周知的存储库模式,我个人选择返回IList,即IEnumerable在List(索引器等)之上。因此,我的建议是IQueryable仅在存储库中使用,并在代码中的其他任何地方使用IEnumerable。更不用说IQueryable分解和破坏关注点分离原则的可测试性关注点了。如果您从存储库中返回表达式,则使用者可以按照他们的意愿使用持久层。

除了混乱之外,还有一点点补充:)(来自评论中的讨论))它们都不是内存中的对象,因为它们本身不是真正的类型,它们是类型的标记-如果您想深入了解。但是将IEnumerables视为内存中的集合,而将IQueryables视为表达式树是有道理的(这也是MSDN这样说的原因)。关键是IQueryable接口继承了IEnumerable接口,因此,如果它表示查询,则可以枚举该查询的结果。枚举使与IQueryable对象关联的表达式树得以执行。因此,实际上,如果没有对象在内存中,就无法真正调用任何IEnumerable成员。无论如何,如果不是空的,它将进入那里。IQueryables只是查询,而不是数据。


3
IEnumerables始终在内存中的评论不一定是正确的。IQueryable接口实现IEnumerable接口。因此,您可以将表示LINQ-to-SQL查询的原始IQueryable传递到需要IEnumerable的视图中。您可能会惊讶地发现您的数据上下文已过期,或者最终遇到了MARS(多个活动结果集)问题。

因此,实际上,如果没有将对象放在内存中,就无法真正调用任何IEnumerable成员。无论如何,如果不是空的,它将进入那里。IQueryables只是查询,而不是数据。但我真的明白你的意思。我要对此发表评论。
Arman McHitarian 2015年

@AlexanderPritchard它们都不是内存中的对象,因为它们本身不是真正的类型,它们是类型的标记-如果您想深入了解。但是将IEnumerables视为内存中的集合,而将IQueryables视为表达式树是有道理的(这也是MSDN这样说的原因)。关键是IQueryable接口继承了IEnumerable接口,因此,如果它表示查询,则可以枚举该查询的结果。枚举使与IQueryable对象关联的表达式树得以执行。
Arman McHitarian 2015年

24

通常,您要保留查询的原始静态类型,直到它变得重要为止。

因此,您可以将变量定义为'var'而不是IQueryable<>or,IEnumerable<>并且您将知道您没有更改类型。

如果您以开头IQueryable<>,则通常希望将其保留为,IQueryable<>直到有令人信服的理由进行更改为止。这样做的原因是,您希望为查询处理器提供尽可能多的信息。例如,如果您只打算使用10个结果(称为Take(10)),那么您希望SQL Server知道这一点,以便它可以优化其查询计划并仅向您发送要使用的数据。

一个令人信服的理由来改变从类型IQueryable<>IEnumerable<>可能是你正在调用一些扩展功能的实现IQueryable<>在您的特定对象既不能处理或把手低效。在这种情况下,您可能希望将类型转换为IEnumerable<>(例如,通过分配给类型的变量IEnumerable<>或通过使用AsEnumerable扩展方法),以便最终调用的扩展函数是Enumerable类中的类而不是Queryable类中的那些。


18

有一篇博客文章,其中包含简短的源代码示例,内容涉及如何滥用它们IEnumerable<T>会严重影响LINQ查询性能:实体框架:IQueryable与IEnumerable

如果我们更深入地研究源代码,可以发现显然有不同的扩展方法适用于IEnumerable<T>

// Type: System.Linq.Enumerable
// Assembly: System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
// Assembly location: C:\Windows\Microsoft.NET\Framework\v4.0.30319\System.Core.dll
public static class Enumerable
{
    public static IEnumerable<TSource> Where<TSource>(
        this IEnumerable<TSource> source, 
        Func<TSource, bool> predicate)
    {
        return (IEnumerable<TSource>) 
            new Enumerable.WhereEnumerableIterator<TSource>(source, predicate);
    }
}

IQueryable<T>

// Type: System.Linq.Queryable
// Assembly: System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
// Assembly location: C:\Windows\Microsoft.NET\Framework\v4.0.30319\System.Core.dll
public static class Queryable
{
    public static IQueryable<TSource> Where<TSource>(
        this IQueryable<TSource> source, 
        Expression<Func<TSource, bool>> predicate)
    {
        return source.Provider.CreateQuery<TSource>(
            Expression.Call(
                null, 
                ((MethodInfo) MethodBase.GetCurrentMethod()).MakeGenericMethod(
                    new Type[] { typeof(TSource) }), 
                    new Expression[] 
                        { source.Expression, Expression.Quote(predicate) }));
    }
}

第一个返回可枚举的迭代器,第二个通过在IQueryable源中指定的查询提供程序创建查询。


11

我最近遇到IEnumerablev 的问题IQueryable。首先使用的算法执行IQueryable查询以获得一组结果。然后将这些传递到foreach循环,并将这些项实例化为实体框架(EF)类。然后,此EF类用于fromLinq to Entity查询的子句中,导致结果为IEnumerable

我对EF和Linq for Entities还是很陌生,所以花了一段时间才弄清瓶颈所在。使用MiniProfiling,我找到了查询,然后将所有单个操作转换为单个IQueryableLinq for Entities查询。执行IEnumerable花费了15秒,IQueryable花费了0.5秒。涉及到三个表,阅读本文后,我相信IEnumerable查询实际上是在形成三个表的叉积并过滤结果。

尝试将IQueryables用作经验法则​​,并对您的工作进行概要分析,以使更改可衡量。


原因是将IQueryable表达式转换为EF中的本机SQL,并在IEnumerable列表为内存中对象的情况下直接在数据库中执行。当您调用诸如Count,Sum或任何To ...之类的聚合函数并随后在内存中进行操作时,它们有时会从数据库中获取。IQueryable一旦调用了这些API之一,它们也会卡在内存中,但是如果没有调用,则可以将表达式向上传递到各层堆栈中,并使用过滤器,直到调用API。设计良好的DAL作为设计良好的存储库将解决此类问题;)
Arman McHitarian 2014年

10

我想澄清一些事情,因为看似矛盾的响应(主要是围绕IEnumerable)。

(1)IQueryable扩展IEnumerable接口。(您可以发送IQueryable到期望IEnumerable没有错误的内容。)

(2)IQueryableIEnumerableLINQ在迭代结果集时都尝试延迟加载。(请注意,可以在每种类型的接口扩展方法中看到实现。)

换句话说,IEnumerables并不是排他的“内存中”。 IQueryables并不总是在数据库上执行。 IEnumerable必须将内容加载到内存中(一旦检索到,可能会延迟),因为它没有抽象数据提供程序。 IQueryables依赖于抽象提供程序(例如LINQ-to-SQL),尽管它也可以是.NET内存提供程序。

样例用例

(a)IQueryable从EF上下文中检索记录列表。(没有记录在内存中。)

(b)将传递IQueryable给模型为的视图IEnumerable。(有效。IQueryable扩展IEnumerable。)

(c)遍历并从视图访问数据集的记录,子实体和属性。(可能会导致异常!)

可能的问题

(1)IEnumerable尝试延迟加载,并且您的数据上下文已过期。由于提供者不再可用而引发异常。

(2)实体框架实体代理已启用(默认设置),并且您尝试使用过期的数据上下文访问相关(虚拟)对象。与(1)相同。

(3)多个活动结果集(MARS)。如果您IEnumerable在一个foreach( var record in resultSet )块中进行迭代并同时尝试访问record.childEntity.childProperty,则由于数据集和关系实体的延迟加载而可能会遇到MARS。如果未在您的连接字符串中启用它,则会导致异常。

  • 我发现在连接字符串中启用MARS的工作不可靠。我建议您避免使用MARS,除非它被充分理解并且明确需要。

通过调用执行查询并存储结果,resultList = resultSet.ToList() 这似乎是确保实体处于内存中的最直接方法。

如果您要访问相关实体,则可能仍需要数据上下文。要么,要么您可以从中禁用实体代理和显式Include相关的实体DbSet


9

“ IEnumerable”和“ IQueryable”之间的主要区别在于执行过滤器逻辑的位置。一个在客户端(在内存中)执行,另一个在数据库上执行。

例如,我们可以考虑一个示例,在该示例中,数据库中有一个用户的10,000条记录,并且假设只有900个活动用户,因此在这种情况下,如果我们使用“ IEnumerable”,则首先将所有10,000条记录加载到内存中,然后在其上应用IsActive过滤器,最终返回900个活跃用户。

另一方面,在相同情况下,如果我们使用“ IQueryable”,它将直接在数据库上应用IsActive筛选器,直接从那里返回900个活动用户。

参考链接


哪一个经过优化并在性能方面轻巧?
Sitecore山姆

就优化和轻量而言,@ Sam“ IQueryable”更可取。
Tabish Usman '18

6

我们可以以相同的方式使用这两种方法,它们的性能只是不同。

IQueryable仅以有效的方式对数据库执行。这意味着它将创建整个选择查询,并且仅获取相关记录。

例如,我们要选择名称以“ Nimal”开头的前10位客户。在这种情况下,选择查询将生成为select top 10 * from Customer where name like ‘Nimal%’

但是,如果我们使用IEnumerable,则查询将类似select * from Customer where name like ‘Nimal%’,并且前十名将在C#编码级别进行过滤(它将从数据库中获取所有客户记录,并将它们传递到C#中)。


5

除了前两个非常好的答案(由driis和Jacob撰写):

IEnumerable接口在System.Collections命名空间中。

IEnumerable对象表示内存中的一组数据,并且只能向前移动该数据。由IEnumerable对象表示的查询将立即完整执行,因此应用程序可以快速接收数据。

执行查询时,IEnumerable会加载所有数据,并且如果需要过滤,则过滤本身是在客户端完成的。

IQueryable接口位于System.Linq命名空间中。

IQueryable对象提供对数据库的远程访问,并允许您以直接的顺序从头到尾或以相反的顺序浏览数据。在创建查询的过程中,返回的对象是IQueryable,查询已得到优化。结果,在执行过程中消耗的内存更少,网络带宽也更少,但是与此同时,它的处理速度比返回IEnumerable对象的查询要慢一些。

选择什么?

如果您需要整个返回的数据集,则最好使用IEnumerable,它可以提供最快的速度。

如果您不需要整个返回的数据集,而只需要一些过滤后的数据,那么最好使用IQueryable。


0

除上述内容外,有趣的是,如果您使用IQueryable而不是,则可以获取异常IEnumerable

如果productsIEnumerable

products.Skip(-4);

但是如果 products是,IQueryable并且它正在尝试访问数据库表中的记录,则会收到此错误:

OFFSET子句中指定的偏移量不能为负。

这是因为构造了以下查询:

SELECT [p].[ProductId]
FROM [Products] AS [p]
ORDER BY (SELECT 1)
OFFSET @__p_0 ROWS

和OFFSET不能为负值。

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.