返回 IEnumerable<T> 与 IQueryable<T>

Posted

技术标签:

【中文标题】返回 IEnumerable<T> 与 IQueryable<T>【英文标题】:Returning IEnumerable<T> vs. IQueryable<T> 【发布时间】:2011-02-22 00:39:09 【问题描述】:

返回IQueryable&lt;T&gt;IEnumerable&lt;T&gt; 有什么区别,什么时候应该优先选择一个?

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;

两者都会被推迟执行,什么时候应该优先于另一个?

【问题讨论】:

【参考方案1】:

是的,两者都会给你deferred execution。

不同之处在于IQueryable&lt;T&gt; 是允许LINQ-to-SQL(LINQ.-to-anything)工作的接口。因此,如果您在IQueryable&lt;T&gt; 上进一步优化您的查询,如果可能,该查询将在数据库中执行。

对于IEnumerable&lt;T&gt; 的情况,它将是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&lt;T&gt; 可以避免从数据库返回太多行。另一个主要的例子是进行分页:如果你在IQueryable 上使用TakeSkip,你只会得到请求的行数;在IEnumerable&lt;T&gt; 上执行此操作将导致您的所有行都加载到内存中。

【讨论】:

很好的解释。是否存在 IEnumerable 比 IQueryable 更可取的情况? 那么我们可以说如果我们使用IQueryable来查询Memory Object,那么IEnumerable和IQueryable就没有什么区别了? 警告:虽然 IQueryable 可能是一个诱人的解决方案,因为它已经说明了优化,但不应允许它通过存储库或服务层。这是为了保护您的数据库免受“堆叠 LINQ 表达式”造成的开销。 @fjxx 是的。如果您想对原始结果(几个最终结果)进行重复过滤。在 IQueryable 接口上执行此操作将多次往返数据库,而在 IEnumerable 上执行此操作将在内存中进行过滤,使其更快(除非数据量很大) 另一个更喜欢IEnumerable 而不是IQueryable 的原因是,并非所有 LINQ 提供程序都支持所有 LINQ 操作。因此,只要您知道自己在做什么,就可以使用IQueryable 将尽可能多的查询推送到 LINQ 提供程序(LINQ2SQL、EF、NHibernate、MongoDB 等)。但是,如果您让其他代码对您的IQueryable 执行任何操作,您最终会遇到麻烦,因为某处的某些客户端代码使用了不受支持的操作。我同意不要将IQueryables 发布到存储库或等效层之外“进入野外”的建议。【参考方案2】:

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

IEnumerable 版本签名为:Where(Func&lt;Customer, bool&gt; predicate)

IQueryable 版本签名为:Where(Expression&lt;Func&lt;Customer, bool&gt;&gt; predicate)

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

例如Where(x =&gt; x.City == "&lt;City&gt;") 适用于 IEnumerableIQueryable

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

IQueryable 集合上使用Where() 时,编译器将表达式树传递给Where()。表达式树类似于反射系统,但用于代码。编译器将您的代码转换为一种数据结构,该数据结构以易于理解的格式描述您的代码所做的事情。

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

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

【讨论】:

IMO 它比公认的答案更好。但是,我没有得到一件事:IQueryable 对常规对象没有任何好处,好吧,但它是否更糟?因为如果它只是没有任何好处,那还不足以成为更喜欢 IEnumerable 的理由,所以到处使用 IQueryable 的想法仍然有效。 Sergey,IQueryable 扩展了 IEnumerable,因此当使用 IQueryable 时,您将比 IEnumerable 实例化加载到内存中的更多!所以这里有一个论点。 (***.com/questions/12064828/… c++ 虽然我认为我可以推断这一点) 同意谢尔盖关于这是最好的答案(尽管接受的答案很好)。我要补充一点,根据我的经验,IQueryable 解析函数的能力不如 IEnumerable 那样:例如,如果您想知道 DBSet&lt;BookEntity&gt; 的哪些元素不在 List&lt;BookObject&gt; 中,@ 987654348@ 抛出异常:Expression of type 'BookEntity' cannot be used for parameter of type 'BookObject' of method 'Boolean Contains[BookObject] (IEnumerable[BookObject], BookObject)'。我必须在dbSetObject 之后添加.ToList() 这是了解差异的非常有用的信息,但最佳答案更准确,因为问题是“什么时候应该优先考虑另一个?”,而不是“有什么区别?”。 @SergeiTachenov 在任何地方使用IQueryable 确实有一个限制:编译器无法将每个单独的 C# 表达式转换为表达式树。当您在需要委托类型的上下文(例如Func&lt;T, bool&gt;)中使用 lambda 语法时,编译器会创建一个常规 C# 方法,因此您可以使用任何 C# 语法。【参考方案3】:

是的,两者都使用延迟执行。让我们使用 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在数据库引擎上运行的查询。

【讨论】:

这教我在转换为 IEnumerable 时,底层的 IQueryable 失去了它的 IQueryable 扩展方法。【参考方案4】:

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

至于哪个优先,这取决于你的底层数据源是什么。

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

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

【讨论】:

【参考方案5】:

一般来说,我会推荐以下内容:

如果您想让开发人员使用您的方法在执行之前优化您返回的查询,请返回 IQueryable&lt;T&gt;

如果要传输一组对象以进行枚举,请返回 IEnumerable

想象一下IQueryable 是什么 - 对数据的“查询”(如果您愿意,可以对其进行细化)。 IEnumerable 是一组可以枚举的对象(已经接收或创建)。

【讨论】:

“可以枚举”,而不是“可以 IEnumerable”。【参考方案6】:

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

    IEnumerable 是您可以枚举的内存中对象的集合 - 一个可以迭代的内存中序列(使得在 foreach 循环中变得容易,尽管您可以去仅限IEnumerator)。它们原样驻留在内存中。 IQueryable 是一个表达式树,它会在某个时候被翻译成其他东西能够枚举最终结果。我想这是让大多数人感到困惑的地方。

它们显然有不同的内涵。

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

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

在Entity Framework 世界(即神秘的底层数据源提供者或查询提供者)中,IQueryable 表达式被转换为原生T-SQL 查询。 Nhibernate 对他们做了类似的事情。例如,您可以按照 LINQ: Building an IQueryable Provider 链接中很好地描述的概念编写自己的 API,并且您可能希望为您的产品商店提供商服务提供自定义查询 API。

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

好像延迟执行它是LINQ 的一个特性,可以将表达式树方案保存在内存中,并仅在需要时将其发送到执行中,只要确定API 是按顺序调用的(相同的 Count、ToList 等)。

两者的正确使用很大程度上取决于您在特定情况下面临的任务。对于众所周知的存储库模式,我个人选择返回IList,即IEnumerable 而不是列表(索引器等)。因此,我的建议是仅在存储库中使用IQueryable,并在代码中的其他任何地方使用 IEnumerable。没有说IQueryable 崩溃并破坏separation of concerns 原则的可测试性问题。如果您从存储库中返回一个表达式,消费者可以按照他们的意愿使用持久层。

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

【讨论】:

IEnumerables 总是在内存中的评论不一定正确。 IQueryable 接口实现了 IEnumerable 接口。因此,您可以将表示 LINQ-to-SQL 查询的原始 IQueryable 直接传递到需要 IEnumerable 的视图中!您可能会惊讶地发现您的数据上下文已过期,或者您最终遇到了 MARS(多个活动结果集)问题。 所以,事实上,如果没有内存中的对象,您就不能真正调用任何 IEnumerable 成员。如果你这样做,它会进入那里,无论如何,如果它不是空的。 IQueryables 只是查询,而不是数据。但我真的明白你的意思。我将对此添加评论。 @AlexanderPritchard 它们都不是内存中的对象,因为它们本身不是真正的类型,它们是类型的标记——如果你想深入的话。但是将 IEnumerables 视为内存中的集合而将 IQueryables 视为表达式树是有道理的(这就是为什么甚至 MSDN 都这么说的原因)。关键是 IQueryable 接口继承了 IEnumerable 接口,因此如果它表示一个查询,则可以枚举该查询的结果。枚举导致与 IQueryable 对象关联的表达式树被执行。【参考方案7】:

一般来说,您希望保留查询的原始静态类型,直到重要为止。

因此,您可以将变量定义为“var”而不是IQueryable&lt;&gt;IEnumerable&lt;&gt;,这样您就会知道您没有更改类型。

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

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

【讨论】:

【参考方案8】:

有一篇带有简短源代码示例的博客文章介绍了滥用 IEnumerable&lt;T&gt; 如何显着影响 LINQ 查询性能:Entity Framework: IQueryable vs. IEnumerable。

如果我们深入挖掘并查看源代码,我们可以看到对IEnumerable&lt;T&gt; 执行了明显不同的扩展方法:

// 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&lt;T&gt;:

// 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源中指定的查询提供程序创建查询。

【讨论】:

【参考方案9】:

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

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

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

【讨论】:

哪一款在性能上优化和轻量化? @Sam "IQueryable" 在优化和轻量化方面更受欢迎。【参考方案10】:

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

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

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

【讨论】:

原因是 IQueryable 表达式在 EF 中被转换为原生 SQL 并在 DB 中直接执行,而 IEnumerable 列表是内存中的对象。当您调用 Count、Sum 或任何 To 等聚合函数时,它们会在某个时刻从数据库中获取,然后在内存中进行操作。 IQueryables 一旦你调用了其中一个 API,也会卡在内存中,但如果没有,你可以将表达式向上传递到层的堆栈并使用过滤器,直到 API 调用。设计良好的 DAL 作为设计良好的存储库将解决此类问题 ;)【参考方案11】:

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

(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 相关实体。

【讨论】:

【参考方案12】:

我们可以将两者用于相同的方式,它们只是性能不同。

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

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

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

【讨论】:

【参考方案13】:

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

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

IEnumerable 对象表示内存中的一组数据,并且只能在这些数据上向前移动。 IEnumerable 对象表示的查询立即完整地执行,因此应用程序接收数据很快。

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

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

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

选择什么?

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

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

【讨论】:

【参考方案14】:

除了上述之外,有趣的是,如果你使用IQueryable而不是IEnumerable,你可以获得异常:

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

products.Skip(-4);

但是,如果 productsIQueryable 并且它正在尝试访问数据库表中的记录,那么您将收到以下错误:

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

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

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

并且OFFSET不能有负值。

【讨论】:

以上是关于返回 IEnumerable<T> 与 IQueryable<T>的主要内容,如果未能解决你的问题,请参考以下文章

返回一个通用的 IEnumerable<T>

从 IEnumerable<T> 函数返回字符串

使用 Linq 选择类的属性以返回 IEnumerable<T>

如何从 C# 中的同一方法返回字符串以及 IEnumerable<T>? [复制]

使用 OrderByDescending 每一步返回 IEnumerable<T> 列表集合中的最大值

循环 IEnumerable<T> 抛出不支持的异常