关于C#:返回IEnumerable 与iqueryable

Returning IEnumerable<T> vs. IQueryable<T>

返回的IQueryable与返回的IEnumerable有什么区别?

1
2
3
4
5
6
7
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是一个接口,它允许linq to sql(linq.-to anythy)工作。因此,如果您进一步改进了对IQueryable的查询,那么如果可能,该查询将在数据库中执行。

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

在代码中:

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

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

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

这是一个非常重要的区别,在许多情况下,使用IQueryable可以避免从数据库返回过多的行。另一个主要的例子是进行分页:如果在IQueryable上使用TakeSkip,则只会得到请求的行数;在IEnumerable上进行分页将导致所有行都加载到内存中。


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

  • IEnumerable版本签名为:Where(Func predicate)

  • IQueryable版本签名为:Where(Expression> predicate)

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

例如,Where(x => x.City =="")IEnumerableIQueryable上工作。

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

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

为什么要麻烦这个表达树的事情?我只想让Where()过滤我的数据。主要原因是,EF和Linq2SQL窗体都可以将表达式树直接转换为SQL,在SQL中,代码执行速度更快。

哦,这听起来像是一个免费的性能提升,在这种情况下,我应该到处使用AsQueryable()?不,只有当基础数据提供者可以对它做些什么时,IQueryable才有用。把普通的List转换成IQueryable不会给你带来任何好处。


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

当我们运行以下代码时:

1
2
3
4
5
6
7
8
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 Profiler中,我们发现一个命令等于:

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

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

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

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

在SQL Server Profiler中,我们发现一个命令等于:

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

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

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


两个都会让你延期执行,是的。

至于哪个比另一个更受欢迎,这取决于基础数据源是什么。

返回IEnumerable将自动强制运行时使用linq to对象查询集合。

返回一个IQueryable(顺便说一下,它实现了IEnumerable)提供了额外的功能,可以将查询转换为对底层源(linq to sql、linq to xml等)性能更好的查询。


一般而言,我建议如下:

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

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

想象一下一个IQueryable是什么,它是一个数据的"查询"(如果你想的话,可以对其进行优化)。IEnumerable是一组对象(已接收或已创建),您可以对其进行枚举。


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

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

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

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


前面已经说过很多,但要回到根源上来,用一种更技术的方式:

  • IEnumerable是内存中可以枚举的对象的集合—内存中的序列使迭代成为可能(使在foreach循环中很容易进行,尽管您只能使用IEnumerator循环)。它们仍然存在于记忆中。
  • IQueryable是一个表达式树,在某个时刻,它将被转换成另一个具有枚举最终结果能力的东西。我想这就是让大多数人困惑的原因。
  • 它们显然有不同的含义。

    IQueryable表示一个表达式树(一个简单的查询),一旦调用了发布API,底层查询提供者就会将它转换成其他的东西,比如linq聚合函数(sum、count等)或tolist[数组、字典等]。而IQueryable对象也实现了IEnumerableIEnumerable,这样,如果它们表示一个查询,则可以迭代该查询的结果。这意味着iQueryable不必只是查询。正确的说法是它们是表达树。

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

    在实体框架世界(即神秘的基础数据源提供程序或查询提供程序)中,IQueryable表达式被转换为本地T-SQL查询。Nhibernate对它们做了类似的事情。您可以按照Linq中非常好地描述的概念编写自己的一个:例如,构建一个IQueryable提供者链接,并且您可能希望为产品存储提供者服务提供一个定制的查询API。

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

    就像延迟执行一样,只要针对序列调用某些API(相同的计数、tolist等),就可以在内存中保留表达式树方案,并按需将其发送到执行中,这是LINQ功能。

    两者的正确使用在很大程度上取决于您在特定情况下所面临的任务。对于众所周知的存储库模式,我个人选择返回IList,即IEnumerableover list(索引器等)。所以我的建议是只在存储库中使用IQueryable,在代码中的任何其他地方使用ienumerable。没有提到可测试性的问题,IQueryable崩溃并破坏了关注点分离原则。如果您从存储库中返回一个表达式,那么用户可能会按照自己的意愿使用持久层。

    有点混乱:(来自评论中的讨论)它们中没有一个是内存中的对象,因为它们本身不是真正的类型,它们是类型的标记——如果你想深入了解的话。但把IEnumerable看作内存集合而把iqueryables看作表达式树是有意义的(这就是为什么甚至msdn都这么说的原因)。重点是iQuery接口继承了IEnumerable接口,因此如果它表示一个查询,则可以枚举该查询的结果。枚举导致执行与IQueryable对象关联的表达式树。因此,实际上,如果没有内存中的对象,就不能真正调用任何IEnumerable成员。如果你这样做,不管怎样,如果它不是空的,它会进入那里。iqueryables只是查询,而不是数据。


    有一篇博文简要介绍了使用IEnumerable会如何显著影响linq查询性能:实体框架:iqueryable与IEnumerable。

    如果我们深入挖掘并深入研究其来源,我们可以看到,对于IEnumerable有明显不同的扩展方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // 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源中指定。


    这些是IQueryableIEnumerable之间的一些区别。

    difference between returning IQueryable<T>vs.ienumerable<t>


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

    我对实体的ef和linq还比较陌生,所以花了一段时间才弄清楚瓶颈是什么。使用小型分析,我找到了查询,然后将所有单个操作转换为单个IQueryablelinq for entities查询。IEnumerable用了15秒,IQueryable用了0.5秒执行。涉及到三个表,阅读后,我认为IEnumerable查询实际上形成了一个三表交叉积,并过滤了结果。

    尝试使用iqueryables作为经验法则,并对您的工作进行概要分析,以使您的更改是可测量的。


    我想澄清一些事情,由于似乎冲突的反应(主要围绕IEnumerable)。

    (1)IQueryable扩展IEnumerable接口。(你可以发送一个IQueryable给某个需要IEnumerable而没有错误的东西。)

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

    换言之,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,则可能会由于数据集和关系实体的延迟加载而以mars结束。如果未在连接字符串中启用,这将导致异常。

    解决方案

    • 我发现在连接字符串中启用mars不可靠。我建议你避开火星,除非它被很好的理解和明确的要求。

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

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


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

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

    另一方面,同样的情况下,如果我们使用"iQueryable",它将直接在数据库上应用isactive过滤器,直接从数据库返回900个活动用户。

    参考链路


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

    IEnumerable
    interface is in the System.Collections namespace.

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

    执行查询时,IEnumerable将加载所有数据,如果需要对其进行筛选,则筛选本身在客户端完成。

    IQueryable interface is located in the System.Linq namespace.

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

    选择什么?

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

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


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

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

    例如,我们想把名字以"nimal"开头的前10位客户作为例子。在这种情况下,选择查询将生成为select top 10 * from Customer where name like ‘Nimal%’

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


    IEnumrable将数据存储到内存中

    但如果是iqueuable,它不会存储在内存中。

    有关详细信息,请使用SQL事件探查器进行检查

    第一次击中你使用iQueryable查询并查看执行的查询

    然后尝试从IEnumable