返回 IEnumerable<T> 与 IQueryable<T> 的区别

1213

返回 IQueryable<T>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;

它们都将被延迟执行,但应该在什么情况下使用其中之一?

14个回答

1957

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

区别在于IQueryable<T>是允许LINQ-to-SQL(实际上是任何LINQ-to-)工作的接口。因此,如果你在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> 可以在许多情况下避免从数据库中返回太多行数据。另一个很好的例子就是分页:如果你在 IQueryable 上使用 TakeSkip,你将只会得到请求的行数;但是,在 IEnumerable<T> 上做同样的操作将导致所有的行都被加载到内存中。


52
很好的解释。有什么情况下IEnumerable比IQueryable更可取吗? - fjxx
10
如果我们使用IQueryable来查询内存对象,那么IEnumerable和IQueryable之间就没有任何区别,这是可以说的。 - Tarik
16
警告:尽管 IQueryable 可能因其所述的优化而成为一种诱人的解决方案,但不应允许它超越存储库或服务层。这是为了保护您的数据库免受“堆叠 LINQ 表达式”所造成的负担。 - Yorro
70
是的,如果你想对原始结果进行多次过滤(得到几个最终结果),在IQueryable接口上执行会导致多次访问数据库,而在IEnumerable上执行则会在内存中进行过滤,速度更快(除非数据量非常大)。 - Per Hornshøj-Schierbeck
42
使用IEnumerable而非IQueryable的另一个原因是,并非所有的LINQ提供程序都支持所有的LINQ操作。只要你知道自己在做什么,就可以使用IQueryable将尽可能多的查询推送到LINQ提供程序(如LINQ2SQL,EF,NHibernate,MongoDB等)。但是,如果让其他代码随意处理你的IQueryable,最终会遇到麻烦,因为某个客户端代码使用了不受支持的操作。我赞同建议,在存储库或等效层之外不要泄露IQueryable - Avish
显示剩余6条评论

393
顶部的答案很好,但它没有提到表达式树,这解释了这两个接口的差异。基本上,有两组相同的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 不会带来任何好处。

15
在我的看法中,这比被接受的答案更好。然而,我有一个疑问:IQueryable 对于普通对象不提供任何好处,没问题,但是它有什么不好的地方吗?因为如果它只是没有提供任何好处,那就不足以优先选择 IEnumerable,所以在所有地方都使用 IQueryable 的想法仍然是有效的。 - Sergei Tachenov
2
Sergey,IQueryable扩展IEnumerable,因此使用IQueryable时,比IEnumerable实例化加载更多内存!所以这里有一个论点。(https://stackoverflow.com/questions/12064828/memory-allocation-of-base-class-and-derived-class-constructor c++ though I thought I could extrapolate this) - Viking
同意Sergei关于这是最佳答案的观点(尽管已接受的答案也不错)。我想补充一下,根据我的经验,IQueryable不能像IEnumerable那样很好地解析函数:例如,如果您想知道DBSet<BookEntity>中哪些元素不在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)'。我不得不在dbSetObject后添加.ToList() - Jean-David Lanz
1
这是非常有用的信息,以便理解它们之间的区别,但是顶部答案更准确,因为问题是“何时应该优先考虑其中一个?”,而不是“它们有什么区别?” - Scopperloit
@SergeiTachenov 在任何地方都使用 IQueryable 也有一个限制:编译器无法将每个 C# 表达式转换为表达式树。当您在期望委托类型(例如 Func<T, bool>)的上下文中使用 lambda 语法时,编译器会创建一个常规的 C# 方法,因此您可以使用任何 C# 语法。 - Zev Spitz

104

是的,两者都使用延迟执行。让我们用 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]"

对于一个拥有一百万条记录的WebLog表格,运行该代码块大约需要90秒。

因此,所有的表格记录都会以对象的形式被加载到内存中,在每个.Where()方法中都会对这些对象再次进行内存过滤。

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

在SQL Server分析器中,我们发现一个等同于以下命令的指令:

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

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

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


11
当将类型转换为IEnumerable时,底层的IQueryable会失去它的IQueryable扩展方法。 - Yiping
这个评论非常有用,可以帮助理解其中的区别。我脑海中有一个疑问,为什么在使用IQueryable时会创建"SELECT TOP 1 * FROM ..."而不是"SELECT TOP 1 UserName FROM ..."? - MrAlbino

68

两者都可以实现延迟执行。

至于哪个更好,这取决于您的基础数据源是什么。

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

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


41

一般而言,我建议如下:

  • 如果你想让开发者在执行查询之前对其进行进一步筛选,返回 IQueryable<T>

  • 如果你想传输一个对象集合以供枚举,则返回 IEnumerable

IQueryable 想象为数据的“查询”(你可以在需要时进行进一步筛选)。IEnumerable 是一组对象(已经收到或创建),可以对其进行枚举。


2
可以枚举。 - Casey
嗨,塞巴斯蒂安,IQueryable和IEnumerable在所有数据库上是否具有相同的效果,例如PostgreSQL(关系型数据库)和MongoDb(NoSQL数据库)? - Thomas Raj

40
之前已经说了很多,但回归到更加技术性的方面:
  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: Building an IQueryable Provider 链接中很好描述的概念编写自己的代码,为你的产品存储提供者服务创建自定义查询 API。
因此,基本上,IQueryable 对象一直被构建,直到我们明确释放它们并告诉系统将它们重写为 SQL 或其他格式,并将其发送到执行链以进行进一步处理。

似乎为了延迟执行,LINQ特性将表达式树方案存储在内存中,并仅在需要时通过调用一些API(例如Count、ToList等)对序列进行执行。

正确使用这两个特性取决于您面临的具体任务。对于众所周知的存储库模式,我个人选择返回IList,即IEnumerable而不是列表(索引器等)。因此,我的建议是仅在存储库中使用IQueryable,而在代码的其他任何位置都使用IEnumerable。不要忽视IQueryable破坏并破坏关注点分离原则的可测试性问题。如果您从存储库中返回表达式,则消费者可以按照他们的意愿与持久层交互。

在这个混乱中增加一点点 :)(来自评论中的讨论)它们都不是内存中的对象,因为它们本身不是真正的类型,它们只是类型的标记 - 如果你想深入了解。但这是有道理的(这就是为什么即使MSDN也这样说),将IEnumerables视为内存集合,而将IQueryables视为表达式树。重点是IQueryable接口继承了IEnumerable接口,以便如果它表示一个查询,则可以枚举该查询的结果。枚举会导致与IQueryable对象关联的表达式树被执行。因此,实际上,您无法在没有将对象放入内存的情况下调用任何IEnumerable成员。无论如何,如果它不为空,它将进入其中。IQueryables只是查询,不是数据。


4
“IEnumerables are always in-memory”这个评论并不一定正确。IQueryable接口实现了IEnumerable接口。因此,您可以将表示LINQ-to-SQL查询的原始IQueryable直接传递到期望一个IEnumerable的视图中!您可能会惊讶地发现,您的数据上下文已过期,或者遇到MARS(多个活动结果集)的问题。 - user1630889
实际上,如果没有将对象加载到内存中,您无法真正调用任何IEnumerable成员。如果不为空,则会在其中进行操作。IQueryables只是查询,而不是数据。 但我确实理解您的观点。我会在此添加一条注释。 - Arman
@AlexanderPritchard 它们实际上不是真正的类型,而是一种类型标记,因此它们都不是内存中的对象。但如果你想深入了解,这是有意义的(这也是为什么即使MSDN也是这样说的),把IEnumerables看作内存中的集合,而把IQueryables看作表达式树。重点在于 IQueryable 接口继承自 IEnumerable 接口,因此如果它表示查询,则可以枚举该查询的结果。枚举会导致与 IQueryable 对象关联的表达式树被执行。 - Arman

26

一般来说,在查询的静态类型变得重要之前,您希望保留原始的静态类型。

因此,您可以将变量定义为'var',而不是IQueryable<>IEnumerable<>之一,这样您就知道您没有更改类型。

如果您最初使用的是IQueryable<>,通常希望将其保持为IQueryable<>,直到出现某些令人信服的理由才更改它。原因是您希望尽可能多地向查询处理器提供信息。例如,如果您只使用10个结果(调用了Take(10)),那么您希望SQL Server知道这一点,以便它可以优化其查询计划并仅发送您将使用的数据。

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


19

这篇博客文章提供了一个简短的源代码示例,说明如何误用 IEnumerable<T> 可以极大地影响 LINQ 查询性能:Entity Framework:IQueryable vs. 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 源中创建查询提供程序来创建查询。


17
“IEnumerable”和“IQueryable”的主要区别在于过滤逻辑的执行位置。其中一个在客户端执行(内存中),而另一个在数据库上执行。例如,假设我们的数据库中有10,000条用户记录,其中只有900条记录是活跃用户,在使用“IEnumerable”时,它会先加载所有10,000条记录到内存中,然后再将IsActive过滤器应用于它,最终返回900个活跃用户。而另一方面,如果我们在同样的情况下使用“IQueryable”,它将直接在数据库上应用IsActive过滤器,从那里直接返回900个活跃用户。

哪一个在性能方面更加优化和轻量级? - Sitecore Sam
@Sam,“IQueryable”在优化和轻量级方面更受欢迎。 - Tabish Usman

12

由于看似相互矛盾的回复(主要是关于IEnumerable的),我想澄清一些事情。

(1) IQueryable扩展了IEnumerable接口。(您可以将IQueryable发送到期望IEnumerable的内容而不出错。)

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

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

示例用例

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

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

(c) 在视图中迭代访问数据集、子实体和属性。 (可能会引起异常!)

可能存在的问题

(1) IEnumerable 尝试惰性加载,而您的数据上下文已过期。由于提供程序不再可用,因此会引发异常。

(2) 启用了 Entity Framework 实体代理(默认),并且您尝试使用过期的数据上下文访问相关(virtual)对象。与(1)相同。

(3) 多活动结果集(MARS)。如果您在 foreach(var record in resultSet) 块中迭代访问 record.childEntity.childProperty,同时尝试访问数据集和关系实体的惰性加载,您可能会遇到MARS。如果它未在连接字符串中启用,则会引发异常。

解决方案

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

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

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接