收益回报与回报 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<T>
可以运行之前)。另一方面,ReadSomeProc
在将读取器传递回调用者之前不会创建读取器,因此在读取容器的所有值之前它不会释放容器。
当您的方法使用yield return
时,编译器实际上将编译后的代码更改为返回IEnumerable<>
,并且您方法中的代码将不会运行,直到其他代码开始迭代返回的IEnumerable<>
强>.
这意味着下面的代码在释放阅读器并返回值之前甚至不会运行您的Enumerate
方法的第一行。当其他人开始迭代您返回的IEnumerable<>
时,读者已经被处理掉了。
using(SqlDataReader rd = cmd.ExecuteReader())
return rd.Enumerate<T>();
但此代码将执行整个Enumerate()
方法,以便在返回之前产生List<>
的结果:
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<>
时,using
块打开,直到IEnumerable<>
完成它的迭代,它才会打开Dispose()
,此时您将已经阅读了您需要的所有内容来自数据读取器。
【讨论】:
感谢清晰的解释!一旦计时器耗尽就会接受。如果我理解正确,这是延迟执行的问题。扩展方法Read<T>
返回一个“合约”进行迭代,而yield return
直接在调用时进行迭代。
@PimBrouwers Read
不会推迟执行,不会。您的两个解决方案都推迟调用Read
,直到返回的枚举实际被迭代。 它们都不在调用时读取值。
@PimBrouwers 另一个版本,不使用Enumerate
also 延迟读取的执行。如前所述,当您调用该方法时,没有一个 方法会读取此查询。并不是只有其中一个人推迟阅读。这是因为这两种方法都使用迭代器块来产生读取值。
@PimBrouwers 确实没有。延迟实际读取值与为什么一个工作而一个不工作无关,因为两者都延迟读取值相同。两者之间的区别在于它们如何推迟对阅读器的处置(以及阅读器的执行),而不是它们如何推迟实际阅读。
@PimBrouwers 两种解决方案都在其中使用while(rd.Read())
。可行的解决方案是将阅读器的处置推迟到实际阅读完成为止。您可以简单地使用调试器单步执行代码,以查看每一行(包括 dispose 调用)何时运行【参考方案2】:
这是因为“yield return”会返回一个元素并继续迭代,而“normal” return会完成调用。
【讨论】:
以上是关于收益回报与回报 IEnumerable<T>的主要内容,如果未能解决你的问题,请参考以下文章