while on IDataReader.Read 不适用于 yield return 但 foreach on reader 可以

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

这是一种常见的 ADO.NET 模式,用于使用数据 reader 从数据库中检索数据,但奇怪的是它不起作用。

无效:

public static IEnumerable<IDataRecord> SelectDataRecord<T>(string query, string connString)
                                                          where T : IDbConnection, new()
{
    using (var conn = new T())
    {
        using (var cmd = conn.CreateCommand())
        {
            cmd.CommandText = query;
            cmd.Connection.ConnectionString = connString;

            cmd.Connection.Open();
            using (var reader = (DbDataReader)cmd.ExecuteReader())
            {
                // the main part
                while (reader.Read())
                {
                    yield return (IDataRecord)reader;
                }
            }
        }
    }

这确实有效:

public static IEnumerable<IDataRecord> SelectDataRecord<T>(string query, string connString)
                                                          where T : IDbConnection, new()
{
    using (var conn = new T())
    {
        using (var cmd = conn.CreateCommand())
        {
            cmd.CommandText = query;
            cmd.Connection.ConnectionString = connString;

            cmd.Connection.Open();
            using (var reader = (DbDataReader)cmd.ExecuteReader())
            {
                // the main part
                foreach (var item in reader.Cast<IDataRecord>())
                {
                    yield return item;
                }
            }
        }
    }

我看到的唯一相关变化是,在第一个代码中,迭代器是从 while 循环返回的,而在第二个代码中,它是从 foreach 循环返回的。

我这样称呼它:

// I have to buffer for some reason
var result = SelectDataRecord<SQLiteConnection>(query, connString).ToList(); 

foreach(var item in result)
{
    item.GetValue(0); // explosion
}

我尝试使用 SQLite .NET connector as well as MySQL 连接器。结果是一样的,即第一种方法失败,第二种方法成功。

异常

SQLite

An unhandled exception of type 'System.InvalidOperationException' occurred in System.Data.SQLite.dll. Additional information: No current row

MySQL

An unhandled exception of type 'System.Exception' occurred in MySql.Data.dll. Additional information: No current query in data reader

是因为reader.Readreader.GetEnumerator在特定的ADO.NET连接器中的实现差异吗?当我检查 System.Data.SQLite 项目的源代码时,我看不出任何明显的区别,GetEnumerator 在内部调用 Read。我理想地假设在这两种情况下 yield 关键字防止急切执行该方法,并且只有在外部枚举枚举器后才必须执行循环。


更新:

我使用这种模式是为了安全起见(与第二种方法基本相同,但不那么冗长),

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

并不是 whileforeach 的区别。这是对 .Cast<T>().

的调用

在第一个示例中,您在 while 循环的每次迭代中都对 相同的 对象进行了让步。如果您不小心,您最终会在实际使用数据之前完成 yield 迭代器,并且 DataReader 将被释放。如果您在调用此方法后调用 .ToList(),就会发生这种情况。您最好希望列表中的每条记录都具有相同的值。
(专业提示:大多数时候您不想调用 .ToList(),除非绝对必要。最好只使用 IEnumerable 记录)。

在第二个示例中,当您在数据读取器上调用 .Cast<T>() 时,您实际上是在遍历每条记录时制作数据的副本。现在您不再生成相同的对象。

这两个例子的区别是因为 foreachwhile 有不同的语义,后者是一个简单的循环。 foreach 的基础 GetEnumerator 在这里有所不同。

正如 Joel 所说,在第一个示例中while 循环的每次迭代都会产生相同的 reader 对象。这是因为 IDataReaderIDataRecord 在这里是相同的,这是不幸的。当对结果序列调用 ToList 时,屈服完成,using 块关闭 reader 和连接对象,最终得到一个已处置的列表 reader 相同引用的对象。

在第二个示例中,数据reader上的foreach确保[=21的副本 =] 产生了。 GetEnumerator 是这样实现的:

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

其中 MoveNext of System.Data.Common.DbEnumerator class 的实现方式如下:

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;
}

DataRecordInternalIDataRecord 的实际实现,它是从 foreach 产生的,它与 reader 不是同一个引用,而是一个缓存副本row/record.

的所有值

在这种情况下,System.Linq.Cast 只是一种保留表示形式,对整体效果没有任何作用。 Cast<T> 会这样实现:

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

可以证明没有 Cast<T> 调用的示例不会出现此问题。

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

上面的例子工作正常。


一个重要的区别是第一个例子只有当你没有在它的第一个枚举本身中使用从数据库读取的值时才会有问题。只有随后的枚举才会抛出,因为 reader 届时将被处理掉。例如,

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
}