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

时间:2015-07-31 13:16:34

标签: c# mysql .net sqlite datareader

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

不起作用:

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连接器以及MySQL连接器。结果是一样的,即第一种方法失败,第二种方法成功。

异常

SQLite的

  

System.Data.SQLite.dll中发生了未处理的“System.InvalidOperationException”类型异常。其他信息:没有当前行

的MySQL

  

MySql.Data.dll中出现未处理的“System.Exception”类型异常。其他信息:数据阅读器中没有当前查询

是否因为特定ADO.NET连接器中reader.Readreader.GetEnumerator之间的实现差异?当我在内部检查System.Data.SQLite项目的来源,GetEnumerator调用Read时,我看不出任何明显的差异。理想情况下,我假设yield关键字阻止了方法的急切执行,并且只有在外部枚举枚举数后才能执行循环。

更新:

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

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

2 个答案:

答案 0 :(得分:2)

while vs foreach并非如此。这是对.Cast<T>()的调用。

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

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

答案 1 :(得分:2)

两个示例之间的区别是因为foreachwhile具有不同的语义,这是一个普通的循环。 GetEnumerator的基础foreach在这里有所不同。

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

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

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

MoveNextSystem.Data.Common.DbEnumerator的实现方式如下:

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

上面的例子运作正常。

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

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
}