收益回报与回报 IEnumerable<T>

Posted

技术标签:

【中文标题】收益回报与回报 IEnumerable<T>【英文标题】:yield return vs. return 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,而不仅仅是一个,而且它们都是同时调用的。 【参考方案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会完成调用。

【讨论】:

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

正确使用“收益回报”

Java中的收益回报

为什么有些程序员明明很努力,但是却回报很低,收益很小,工资始终上不去-出自中华石杉老师

挖矿的收益到底怎样,年回报率能达到500%​?

c# IeNumerable 如果没有则返回[重复]

2021年当前市场S19系列挖矿投资收益分析(年回报率超90%))