【问题标题】:yield return vs. return IEnumerable<T>收益回报与回报 IEnumerable<T>
【发布时间】:2018-01-23 13:15:55
【问题描述】:

我注意到在我无法理解的 using 语句中读取 IDataReader 时有些奇怪。虽然我确信答案很简单。

为什么在using (SqlDataReader rd) { ... } 内,如果我直接执行yield return,阅读器在阅读期间保持打开状态。但是,如果我执行直接return 调用 SqlDataReader 扩展方法(如下所述),读者在实现可枚举之前关闭?

public static IEnumerable<T> Enumerate<T>(this SqlDataReader rd)
{
    while (rd.Read())
        yield return rd.ConvertTo<T>(); //extension method wrapping FastMember

    rd.NextResult();
}

为了完全清楚我的要求,我不确定为什么以下内容存在根本不同:

根据@TimSchmelter 的要求,一个充实的例子:

/*
 * contrived methods
 */
public IEnumerable<T> ReadSomeProc<T>() {
    using (var db = new SqlConnection("connection string"))
    {
        var cmd = new SqlCommand("dbo.someProc", db);

        using(var rd = cmd.ExecuteReader())
        {
            while(rd.Read())
                yield return rd.ConvertTo<T>(); //extension method wrapping FastMember
        }
    }
}


//vs
public IEnumerable<T> ReadSomeProcExt<T>() {
    using (var db = new SqlConnection("connection string"))
    {
        var cmd = new SqlCommand("dbo.someProc", db);

        using(var rd = cmd.ExecuteReader())
        {
            return rd.Enumerate<T>(); //outlined above
        }
    }
}

/*
 * usage
 */
var lst = ReadSomeProc<SomeObect>();

foreach(var l in lst){
    //this works
}

//vs
var lst2 = ReadSomeProcExt<SomeObect>();

foreach(var l in list){
    //throws exception, invalid attempt to read when reader is closed
}

【问题讨论】:

  • 我认为您的问题已在MSDN 关于“产量”作用的文章中得到解答。
  • 那是因为带有yield return 的方法变成了状态机。出于这个原因,最好不要在 using 语句中混合惰性初始化。
  • 您应该在读取数据的地方显示部分代码。明确一点,第一个版本允许读取,而第二个版本抛出一个异常,即阅读器已关闭?
  • @vipersassassin:嗯,两者都使用yield return,这就是为什么 OP 想知道为什么他会得到不同的结果。
  • @johnny5 两种解决方案都将读取结果的时间相同。两种解决方案都调用ConvertTo,而不仅仅是一个,而且它们都是同时调用的。

标签: c# ado.net ienumerable sqldatareader yield-return


【解决方案1】:

总结:两个版本的方法defer,但是因为ReadSomeProcExt不延迟执行,读取器在执行被传递回调用者之前被释放(即在Enumerate&lt;T&gt; 可以运行之前)。另一方面,ReadSomeProc 在将读取器传递回调用者之前不会创建读取器,因此在读取容器的所有值之前它不会释放容器。

当您的方法使用yield return 时,编译器实际上将编译后的代码更改为返回IEnumerable&lt;&gt;,并且您方法中的代码将不会运行,直到其他代码开始迭代返回的IEnumerable&lt;&gt;强>.

这意味着下面的代码在释放阅读器并返回值之前甚至不会运行您的Enumerate 方法的第一行。当其他人开始迭代您返回的IEnumerable&lt;&gt; 时,读者已经被处理掉了。

using(SqlDataReader rd = cmd.ExecuteReader()){
    return rd.Enumerate<T>();
}

但此代码将执行整个Enumerate() 方法,以便在返回之前产生List&lt;&gt; 的结果:

using(SqlDataReader rd = cmd.ExecuteReader()){
    return rd.Enumerate<T>().ToList();
}

另一方面,使用此代码调用该方法的人在计算结果之前不会真正执行该方法:

using(SqlDataReader rd = cmd.ExecuteReader()){
    while(rd.Read())
        yield return rd.ConvertTo<T>(); //extension method wrapping FastMember
}

但是当他们执行返回的IEnumerable&lt;&gt; 时,using 块打开,直到IEnumerable&lt;&gt; 完成它的迭代,它才会打开Dispose(),此时您将已经阅读了您需要的所有内容来自数据读取器。

【讨论】:

  • 感谢清晰的解释!一旦计时器耗尽就会接受。如果我理解正确,这是延迟执行的问题。扩展方法Read&lt;T&gt; 返回一个“合约”进行迭代,而yield return 直接在调用时进行迭代。
  • @PimBrouwers Read 不会推迟执行,不会。您的两个解决方案都推迟调用Read,直到返回的枚举实际被迭代。 它们都不在调用时读取值。
  • @PimBrouwers 另一个版本,不使用Enumerate also 延迟读取的执行。如前所述,当您调用该方法时,没有一个 方法会读取此查询。并不是只有其中一个人推迟阅读。这是因为这两种方法都使用迭代器块来产生读取值。
  • @PimBrouwers 确实没有。延迟实际读取值与为什么一个工作而一个不工作无关,因为两者都延迟读取值相同。两者之间的区别在于它们如何推迟对阅读器的处置(以及阅读器的执行),而不是它们如何推迟实际阅读。
  • @PimBrouwers 两种解决方案都在其中使用while(rd.Read())。可行的解决方案是将阅读器的处置推迟到实际阅读完成为止。您可以简单地使用调试器单步执行代码,以查看每一行(包括 dispose 调用)何时运行
【解决方案2】:

这是因为“yield return”会返回一个元素并继续迭代,而“normal” return会完成调用。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2010-09-22
    • 1970-01-01
    • 1970-01-01
    • 2010-12-01
    • 1970-01-01
    • 2011-05-27
    • 2012-01-14
    • 1970-01-01
    相关资源
    最近更新 更多