返回 IEnumerable <T> 与 IQueryable <T>

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

两者都将推迟执行吗?何时应优先选择另一个?

答案

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

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

最佳答案是好的,但没有提到解释两个接口 “如何” 不同的表达式树。基本上,有两组相同的 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不会给您带来任何好处。

是的,两者都使用延迟执行。让我们来说明使用 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将自动强制运行时使用 LINQ to Objects 查询您的集合。

返回一个IQueryable (顺便说一句,它实现了IEnumerable )提供了额外的功能,可以将您的查询转换为在基础源(LINQ to SQL,LINQ to XML 等)上可能会更好地执行的查询。

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

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

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

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

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

  1. IEnumerable 是内存中可以枚举的对象的集合 - 一个内存中的序列,可以循环访问(尽管仅可以使用IEnumerator ,它在foreach循环中变得很容易)。它们原样驻留在内存中。
  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 (索引器等)。因此,我建议仅在存储库中使用IQueryable ,并在代码中的其他任何地方使用 IEnumerable。 IQueryable分解并破坏关注点分离原则的可测试性关注 。如果您从存储库中返回表达式,则使用者可以按照他们的意愿使用持久层。

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

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

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

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

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

有一篇博客文章,其中包含简短的源代码示例,其中涉及有关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源中指定的查询提供程序创建查询。

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

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

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

我想澄清一些事情,因为看似矛盾的响应(主要是围绕 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)实体框架实体代理已启用(默认),并且您尝试使用过期的数据上下文访问相关(虚拟)对象。与(1)相同。

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

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

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

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