虽然在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.Read
和reader.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
时,您实际上在迭代每个记录时复制数据。 现在你不再产生同样的对象了。
这两个例子之间的区别是因为foreach
与while
有不同的语义, while
是一个普通的循环。 foreach
的底层GetEnumerator
在这里有所不同。
正如Joel所说, 在第一个例子中 ,在while
循环的每次迭代中都会产生相同的reader对象。 这是因为IDataReader
和IDataRecord
在这里都是相同的,这是不幸的。 当在结果序列上调用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; }
DataRecordInternal
是IDataRecord
的实际实现,它是从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 }