问题 收益率与收益率IEnumerable


我注意到一些关于阅读的好奇心 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
}

8768
2017-08-15 15:14


起源

我想你的问题已在答案中得到解答 MSDN 关于'收益'的内容的文章。 - vipersassassin
那是因为一个方法 yield return 变成了状态机。最好不要在内部混合延迟初始化 using 出于这个原因的陈述。 - juharr
您应该显示读取数据的部分代码。要清楚,第一个版本允许读取,而第二个版本抛出读取器关闭的异常? - Tim Schmelter
@vipersassassin:好吧,两者都用 yield return这就是为什么OP想知道为什么他会得到不同的结果。 - Tim Schmelter
@ johnny5两种解决方案都推迟读取结果的持续时间。两种解决方案都称 ConvertTo,而不仅仅是一个,他们都同时这样做。 - Servy


答案:


概要: 两种版本的方法 延缓, 但是因为 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 块打开了,但它没有 Dispose() 直到 IEnumerable<> 完成其迭代,此时您已经从数据读取器中读取了所需的一切。


9
2017-08-15 15:21



谢谢你的明确解释!定时器运行时会立即接受。如果我理解正确,那就是延迟执行的问题。扩展方法 Read<T>返回一个“契约”来迭代 yield return 直接在调用时迭代。 - pimbrouwers
@PimBrouwers Read 不推迟执行,不。你的两个解决方案都推迟了呼叫 Read 直到返回的枚举实际被迭代。 也不 他们在调用时读取值。 - Servy
@PimBrouwers另一个版本,不使用 Enumerate  也 推迟执行阅读。如前面提到的, 也不 当您调用该方法时,这些方法将执行此查询的任何读取。并不只是其中一个推迟阅读。这是因为两种方法都使用迭代器块来产生读取值。 - Servy
@PimBrouwers确实没有。推迟对值的实际读取与为什么一个工作而一个不工作无关,因为两者都推迟了对值的读取。两者之间的区别在于它们如何推迟读者的处理(以及读者的执行),而不是他们如何推迟实际阅读。 - Servy
@PimBrouwers两种解决方案都使用 while(rd.Read()) 在他们中。工作解决方案是推迟读者处理直到实际完成读取的解决方案。您只需使用调试器逐步执行代码,即可查看每行(包括dispose调用)的运行时间 - Servy


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


0
2017-08-15 15:22