while on IDataReader.Read doesn't work with yield return but foreach on reader does

混江龙づ霸主 提交于 2019-12-04 15:04:11

It's not while vs foreach that makes the difference. It's the call to .Cast<T>().

In the first sample, you are yielding on the same object in each iteration of the while loop. If you're not careful, you end up having completed the yield iterator before actually using the data, and the DataReader will already be disposed. This can happen if you were to, say, call .ToList() after calling this method. The best you could hope for there would be for every record in the list to have the same value.
(Pro tip: most of the time you don't want to call .ToList() until you absolutely have to. It's better to just work with IEnumerable records).

In the second sample, when you call .Cast<T>() on the datareader, you are effectively making a copy of the data as it iterates through each record. Now you are no longer yielding the same object.

The difference between the two examples is because foreach has different semantics from while which is a plain loop. The underlying GetEnumerator of foreach makes the difference here.

As Joel says, in the first example, the same reader object is yielded on each iteration of the while loop. This is because the both the IDataReader as well as IDataRecord are the same here, which is unfortunate. When a ToList is called on the resulting sequence, the yielding is complete upon which the using blocks closes the reader and connection objects, and you end up with a list of disposed reader objects of the same reference.

In the second example, the foreach on data reader ensures a copy of IDataRecord is yielded. The GetEnumerator is implemented like this:

public IEnumerator GetEnumerator()
{
    return new DbEnumerator(this); // the same in MySQL as well as SQLite ADO.NET connectors
}

where MoveNext of System.Data.Common.DbEnumerator class is implemented like:

IDataRecord _current;

public bool MoveNext() // only the essentials
{
    if (!this._reader.Read())
        return false;

    object[] objArray = new object[_schemaInfo.Length];
    this._reader.GetValues(objArray); // caching into obj array
    this._current = new DataRecordInternal(_schemaInfo, objArray); // a new copy made here
    return true;
}

The DataRecordInternal is the actual implementation of the IDataRecord which is yielded from the foreach which is not the same reference as the reader, but a cached copy of all the values of the row/record.

The System.Linq.Cast in this case is a mere representation preserving cast which does nothing to the overall effect. Cast<T> will be implemented like this:

public static IEnumerable<T> Cast<T>(this IEnumerable source)
{
    foreach (var item in source)
        yield return (T)item; // representation preserving since IDataReader implements IDataRecord
}

An example without Cast<T> call can be shown to not exhibit this problem.

using (var reader = cmd.ExecuteReader())
    foreach (var record in reader as IEnumerable)
        yield return record;

The above example just works fine.


An important distinction to make is that the first example is problematic only if you are not making use of the values read from database in its first enumeration itself. It is only the subsequent enumerations which throw since reader will be disposed by then. An eg,

using (var reader = cmd.ExecuteReader())
    while (reader.Read())
        yield return reader;

...
foreach(var item in ReaderMethod())
{
    item.GetValue(0); // runs fine
} 

...
foreach(var item in ReaderMethod().ToList())
{
    item.GetValue(0); // explosion
} 
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!