返回IENumable<T>与IQueryable<T>

返回IQueryable<T>IEnumerable<T>有什么区别,什么时候应该优先于另一个?

IQueryable<Customer> custs = from c in db.Customerswhere c.City == "<City>"select c;
IEnumerable<Customer> custs = from c in db.Customerswhere c.City == "<City>"select c;

两者都将被推迟执行,何时应该优先于另一个?

276338 次浏览

两者都会给你延迟执行,是的。

至于哪个优于另一个,这取决于您的底层数据源是什么。

返回IEnumerable将自动强制运行时使用LINQ to Object来查询您的集合。

返回IQueryable(顺便说一句,它实现了IEnumerable)提供了额外的功能,可以将您的查询转换为在底层源(LINQ toSQL、LINQ to XML等)上可能表现更好的东西。

是的,两者都会给你延期执行

不同的是,#0是允许LINQ-to-SQL(LINQ-to-任何东西)工作的接口。因此,如果您进一步改进对#0的查询,该查询将在数据库中执行,如果可能的话。

对于#0的情况,它将是LINQ到对象,这意味着所有与原始查询匹配的对象都必须从数据库加载到内存中。

在代码中:

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);

这是一个非常重要的区别,在许多情况下,处理#0可以避免从数据库返回太多行。另一个主要的例子是进行分页:如果您在#3上使用#1#2,您将只获得请求的行数;在#4上这样做将导致您的所有行都被加载到内存中。

总的来说,我建议如下:

  • 如果您想让开发人员使用您的方法来细化您在执行前返回的查询,请返回IQueryable<T>

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

想象一下IQueryable是什么——一个数据的“查询”(如果你愿意,你可以细化它)。IEnumerable是一组对象(已经接收或创建),你可以枚举它们。

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

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

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

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

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

我对EF和Linq for Entities相当陌生,所以花了一段时间才弄清楚瓶颈是什么。使用MiniPro的,我找到了查询,然后将所有单独的操作转换为单个IQueryable Linq for Entities查询。IEnumerable花了15秒,IQueryable花了0.5秒执行。涉及三个表,阅读后,我相信IEnumerable查询实际上形成了三个表的交叉产品并过滤结果。

尝试使用IQueryables作为经验法则,并分析您的工作以使您的更改可衡量。

有一篇博客文章包含简短的源代码示例,介绍了滥用IEnumerable<T>如何显着影响LINQ查询性能:实体框架:IQueryable vs. IENumable

如果我们深入挖掘并查看源代码,我们可以看到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.dllpublic 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.dllpublic 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 source中指定。

之前已经说过很多,但回到根源,以更技术的方式:

  1. IEnumerable是内存中可以枚举的对象的集合-一个内存中的序列,可以迭代(这使得在foreach循环中很容易,尽管你只能使用IEnumerator)。它们按原样驻留在内存中。
  2. IQueryable是一个表达式树在某个时候会被翻译成其他东西具有列举最终结果的能力。我想这就是大多数人困惑的地方。

它们显然有不同的含义。

IQueryable表示一个表达式树(简单地说,一个查询),一旦调用发布API,底层查询提供程序就会将其转换为其他内容,例如LINQ聚合函数(求和、计数等)或ToList[数组、字典等]。IQueryable对象还实现了IEnumerableIEnumerable<T>,以便如果它们表示查询可以迭代该查询的结果。这意味着IQueryable不必只是查询。正确的术语是它们表达式树

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

实体框架世界(神秘的底层数据源提供程序或查询提供程序)中,IQueryable表达式被转换为本机T-SQL查询。Nhibernate对它们做了类似的事情。例如,您可以按照LINQ:构建一个可查询的提供程序链接中很好描述的概念编写自己的一个,您可能希望为您的产品商店提供程序服务提供一个自定义查询API。

所以基本上,IQueryable对象一直在构造,直到我们显式释放它们并告诉系统将它们重写为SQL或其他什么,然后发送执行链进行后续处理。

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

两者的正确使用在很大程度上取决于你在特定情况下面临的任务。对于众所周知的存储库模式,我个人选择返回IList,即IEnumerable而不是列表(索引器等)。所以我的建议是仅在存储库中使用IQueryable,并在代码中的其他任何地方使用IENumable。不要说IQueryable打破并破坏关注点分离原则的可测试性问题。如果你从存储库中返回一个表达式,消费者可以随心所欲地使用持久层。

除了混乱:)(来自评论中的讨论)它们都不是内存中的对象,因为它们本身不是真正的类型,它们是类型的标记——如果你想深入了解的话。但将IENumables视为内存中的集合而IQueryables视为表达式树是有道理的(这就是为什么甚至MSDN这样说)。关键是IQueryable接口继承了IQueryable接口,因此如果它代表一个查询,则可以枚举该查询的结果。枚举导致与IQueryable对象关联的表达式树被执行。所以,事实上,如果没有内存中的对象,你就不能真正调用任何IENumable成员。无论如何,如果它不为空,它会进入那里。IQueryables只是查询,而不是数据。

是的,两者都使用延迟执行。让我们使用SQL服务器分析器来说明区别。

当我们运行以下代码时:

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服务器分析器中,我们发现一个命令等于:

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

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

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

当我们在上面的例子中使用IQueryable而不是IEnumerable时(第二行):

在SQL服务器分析器中,我们发现一个命令等于:

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

使用IQueryable运行此代码块大约需要4秒。

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

我想澄清一些事情,因为似乎相互矛盾的反应(主要是围绕IENumable)。

(1)IQueryable扩展了IEnumerable接口。(您可以将IQueryable发送给期望IEnumerable的对象而不会出错。)

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

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

示例用例

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

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

(c)从视图中迭代和访问数据集的记录、子实体和属性。(可能会导致异常!)

可能问题

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

(2)Entity Framework实体代理已启用(默认),您尝试访问具有过期数据上下文的相关(虚拟)对象。与(1)相同。

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

解决方案

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

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

在您正在访问相关实体的情况下,您可能仍然需要数据上下文。要么这样,要么您可以从DbSet中禁用实体代理和显式Include相关实体。

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

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

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

您可能一直在使用这两个而没有意识到,因为它们都是使用相同的语法调用的:

例如,Where(x => x.City == "<City>")同时适用于IEnumerableIQueryable

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

  • 当在IQueryable集合上使用Where()时,编译器会将表达式树传递给Where()。表达式树就像反射系统,只是用于代码。编译器会将代码转换为数据结构,以易于理解的格式描述代码的作用。

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

哦,这听起来像是一个免费的性能提升,我应该在这种情况下使用AsQueryable()吗?不,IQueryable只有在底层数据提供者可以用它做一些事情时才有用。将常规List转换为IQueryable不会给你任何好处。

我们可以以相同的方式使用两者,它们只是在性能上有所不同。

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

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

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

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

例如,我们可以考虑一个例子,我们的数据库中有10,000条用户记录,假设只有900条是活动用户,所以在这种情况下,如果我们使用“IENumable”,那么它首先将所有10,000条记录加载到内存中,然后对其应用IsActive过滤器,最终返回900个活动用户。

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

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

可数接口位于System. Colltions命名空间中。

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

当查询被执行时,IENumable加载所有的数据,如果我们需要过滤它,过滤本身是在客户端完成的。

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

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

选择什么?

如果您需要整个返回的数据集,那么最好使用提供最大速度的IENumable。

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

除此之外,有趣的是,如果您使用IQueryable而不是IEnumerable,您可以获得异常:

如果productsIEnumerable,则以下操作正常:

products.Skip(-4);

但是,如果productsIQueryable并且它试图从DB表访问记录,那么您将收到此错误:

OFFSET子句中指定的偏移量可能不是负数。

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

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

并且OFFSET不能为负值。