虽然在IDataReader.Read上不能使用yield return,但读取器上的foreach会有效

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

不起作用:

public static IEnumerable SelectDataRecord(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 SelectDataRecord(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()) { yield return item; } } } } 

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

我称之为:

 // I have to buffer for some reason var result = SelectDataRecord(query, connString).ToList(); foreach(var item in result) { item.GetValue(0); // explosion } 

我尝试使用SQLite .NET连接器以及MySQL连接器。 结果是相同的,即第一种方法失败,第二种方法成功。

例外

SQLite的

System.Data.SQLite.dll中发生了未处理的“System.InvalidOperationException”类型exception。 附加信息: 没有当前行

MySQL的

MySql.Data.dll中出现未处理的“System.Exception”类型exception。 附加信息: 数据读取器中没有当前查询

是因为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; 

它不是与foreach的。 这是对.Cast()的调用。

在第一个示例中,您在while循环的每次迭代中都会产生相同的对象。 如果你不小心,你最终在实际使用数据之前完成了yield迭代器,并且DataReader已经被处理掉了。 如果您在调用此方法后调用.ToList() ,则会发生这种情况。 您希望最好的是列表中的每个记录都具有相同的值。
(专业提示:大多数时候你不想调用.ToList()直到你必须这样做。最好只使用IEnumerable记录)。

在第二个示例中,当您在datareader上调用.Cast()时,您实际上在迭代每个记录时复制数据。 现在你不再产生同样的对象了。

这两个例子之间的区别是因为foreachwhile有不同的语义, while是一个普通的循环。 foreach的底层GetEnumerator在这里有所不同。

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

在第二个示例中 ,数据读取器上的foreach确保生成 IDataRecord 的副本GetEnumerator的实现方式如下:

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

其中System.Data.Common.DbEnumerator类的MoveNext实现如下:

 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产生的,它与读者的引用不同,但是是行/记录的所有值的缓存副本。

在这种情况下, System.Linq.Cast仅仅是保留强制转换的表示,它对整体效果没有任何作用。 Cast将实现如下:

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

没有Cast调用的示例可以显示为不显示此问题。

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

上面的例子运行正常。


要做的一个重要区别是,只有当您没有在第一个枚举本身中使用从数据库读取的值时, 第一个示例才有问题。 只有随后的枚举才能读取,因为读者将被处理掉。 例如,

 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 }