如果我们产生datareader行而不读取所有记录,数据库连接会关闭吗?


15

在了解yield关键字的工作原理的同时,我遇到了StackOverflow 上的link1link2,它主张yield return在DataReader 上进行迭代时使用它,它也适合我的需求。但是,这使我感到奇怪,如果发生如下情况,如果我yield return按如下所示使用,并且如果我不遍历整个DataReader,那么数据库连接会永远保持打开状态吗?

IEnumerable<IDataRecord> GetRecords()
{
    SqlConnection myConnection = new SqlConnection(@"...");
    SqlCommand myCommand = new SqlCommand(@"...", myConnection);
    myCommand.CommandType = System.Data.CommandType.Text;
    myConnection.Open();
    myReader = myCommand.ExecuteReader(CommandBehavior.CloseConnection);
    try
       {               
          while (myReader.Read())
          {
            yield return myReader;          
          }
        }
    finally
        {
            myReader.Close();
        }
}


void AnotherMethod()
{
    foreach(var rec in GetRecords())
    {
       i++;
       System.Console.WriteLine(rec.GetString(1));
       if (i == 5)
         break;
  }
}

我在示例控制台应用程序中尝试了相同的示例,并在调试时注意到GetRecords()未执行的finally块。我怎样才能确保关闭数据库连接?有没有比使用yield关键字更好的方法?我正在尝试设计一个自定义类,该类将负责在数据库上执行选择的SQL和存储过程,并将返回结果。但是我不想将DataReader返回给调用者。我还要确保在所有情况下都将关闭连接。

编辑将答案更改为Ben的答案,因为期望方法调用者正确使用该方法是不正确的,并且对于DB连接,如果无缘无故多次调用该方法,则将更加昂贵。

感谢Jakob和Ben提供详细说明。


从我看来,您并没有首先打开连接
狗仔队2015年

@Frisbee编辑:)
GawdePrasad 2015年

Answers:


12

是的,您将遇到您描述的问题:在完成对结果的迭代之前,将保持连接打开。我可以考虑以下两种一般方法:

推,不要拉

目前,您正在返回IEnumerable<IDataRecord>,可以从中提取数据结构。相反,您可以切换方法以推出其结果。最简单的方法是传递在Action<IDataRecord>每次迭代中调用的an :

void GetRecords(Action<IDataRecord> callback)
{
    // ...
      while (myReader.Read())
      {
        callback(myReader);
      }
}

请注意,鉴于您要处理的是项目集合,IObservable/ IObserver可能是稍微合适的数据结构,但是除非您需要它,否则简单Action得多。

认真评估

另一种选择是确保返回之前迭代完全完成。

通常,您可以通过将结果放在列表中然后返回来完成此操作,但是在这种情况下,每个项目的额外复杂性是对读者的相同引用。因此,您需要一些东西来从阅读器中提取所需的结果:

IEnumerable<T> GetRecords<T>(Func<IDataRecord,T> extractor)
{
    // ...
     var result = new List<T>();
     try
     {
       while (myReader.Read())
       {
         result.Add(extractor(myReader));         
       }
     }
     finally
     {
         myReader.Close();
     }
     return result;
}

我正在使用一种名为“认真评估”的方法。现在,如果我的SQLCommand的Datareader返回多个输出(如myReader.NextResult())并构造一个列表列表,将产生内存开销吗?还是将数据读取器发送给调用者[我个人不喜欢它]会很有效?[但是连接将打开更长的时间]。我在这里确切地感到困惑的是,在保持连接打开时间比创建列表列表更长的权衡之间。您可以放心地假设将有近500多个人访问我的连接DB的网页。
2015年

1
@GawdePrasad这取决于调用者对阅读器的处理方式。如果将项目本身放入集合中,则不会有太大的开销。如果执行不需要将所有值都保留在内存中的操作,那么就会有内存开销。但是,除非您要存储大量存储空间,否则可能不必担心开销。无论哪种方式,都不应有大量的时间开销。
本·亚伦森

“推,不拉”方法如何解决“直到完成对结果的迭代,您将保持连接打开”的问题?即使使用此方法完成迭代,连接仍保持打开状态,不是吗?
BornToCode

1
@BornToCode看到问题:“如果我使用如下所示的yield return,并且如果我不遍历整个DataReader,那么数据库连接将永远保持打开状态”。问题是您返回一个IEnumerable,并且处理它的每个调用者都必须意识到这个特殊的陷阱:“如果您还没有完成对我的迭代,我将保持打开连接的状态”。在push方法中,您无需承担呼叫者的责任,他们无法选择部分迭代。当控件返回给它们时,连接关闭。
Ben Aaronson

1
这是一个很好的答案。我喜欢推入方法,因为如果您要以分页的方式显示较大的查询,则可以通过传递Func <>而不是Action <>来加快读取速度,从而中止读取。这比使GetRecords函数也进行分页更干净。
Wholkaholism,

10

您的finally块将始终执行。

使用yield return编译器时,将创建一个新的嵌套类以实现状态机。

此类将包含来自finally块的所有代码作为单独的方法。它将finally根据状态跟踪需要执行的块。所有必要的finally块将在Dispose方法中执行。

根据C#语言规范,foreach (V v in x) embedded-statement扩展到

{
    E e = ((C)(x)).GetEnumerator();
    try
    {
        while (e.MoveNext())
        {
            V v = (V)(T)e.Current;
            embedded - statement
        }
    }
    finally {
         // Dispose e
    }
}

因此即使您使用break或退出循环,枚举器也将被处置return

要获得有关迭代器实现的更多信息,您可以阅读Jon Skeet的这篇文章。

编辑

这种方法的问题在于,您依赖类的客户端来正确使用该方法。例如,他们可以直接获取枚举数,并在while不处理它的情况下循环遍历该枚举数。

您应该考虑@BenAaronson建议的解决方案之一。


1
这是极其不可靠的。例如:var records = GetRecords(); var first = records.First(); var count = records.Count()实际上将执行两次该方法,每次打开和关闭连接以及读取器。对其进行两次迭代会做同样的事情。您可能还需要在很长一段时间内传递结果,然后再进行实际迭代,等等。方法签名中没有任何内容暗示这种行为
Ben Aaronson 2015年

2
@BenAaronson我在回答中重点关注有关当前实现的特定问题。如果您看整个问题,我同意使用您在答案中建议的方法要好得多。
Jakub Lortz

1
@JakubLortz足够公平
Ben Aaronson

2
@EsbenSkovPedersen通常,当您尝试制作防白痴的东西时,就会失去一些功能。您需要平衡一下。在这里,我们不会因急于加载结果而损失很多。无论如何,对我来说最大的问题是命名。当我看到一个名为GetRecordsreturning 的方法时IEnumerable,我希望它可以将记录加载到内存中。另一方面,如果它是一个property Records,我宁愿假设它是对数据库的某种抽象。
雅各布·洛尔兹

1
@EsbenSkovPedersen好,请参阅我对nvoigt答案的评论。对于返回IEnumerable的方法,我通常没有问题,该方法在迭代时将做大量工作,但是在这种情况下,它似乎不适合方法签名的含义
Ben Aaronson 2015年

5

不论具体 yield行为如何,您的代码都包含无法正确处理资源的执行路径。如果第二行或第三行抛出异常怎么办?甚至是您的第四个?您将需要一个非常复杂的try / finally链,也可以使用using块。

IEnumerable<IDataRecord> GetRecords()
{
    using(var connection = new SqlConnection(@"..."))
    {
        connection.Open();

        using(var command = new SqlCommand(@"...", connection);
        {
            using(var reader = command.ExecuteReader())
            {
               while(reader.Read())
               {
                   // your code here.
                   yield return reader;
               }
            }
        }
    }
}

人们指出这不是很直观,人们调用您的方法可能不知道两次枚举结果将两次调用该方法。好吧,祝你好运。这就是语言的工作方式。那是信号IEnumerable<T>发出。这是不返回List<T>或的原因T[]。不知道这一点的人需要受教育,不能解决。

Visual Studio具有一项称为静态代码分析的功能。您可以使用它来查找是否正确配置了资源。


该语言允许迭代IEnumerable执行各种操作,例如引发完全不相关的异常或将5GB文件写入磁盘。仅仅因为语言允许,并不意味着IEnumerable可以从没有给出将要发生的迹象的方法中返回这样做的结果。
Ben Aaronson

1
返回的IEnumerable<T> 在表明枚举两次将导致两个呼叫。如果您多次枚举,您甚至会在工具中收到有用的警告。不管返回类型如何,抛出异常的观点同样有效。如果该方法返回a List<T>,则将引发相同的异常。
nvoigt 2015年

我确实理解您的观点,并且在某种程度上我同意-如果您使用的是一种可以给您带来IEnumerable警告的方法。但是我也认为,作为方法编写者,您有责任遵守签名的含义。该方法称为GetRecords,因此返回时,您希望它已获得记录。如果它被调用GiveMeSomethingICanPullRecordsFrom(或更实际GetRecordSource或更相似),那么懒惰IEnumerable将更容易被接受。
Ben Aaronson

@BenAaronson我同意例如中的FileReadLines并且ReadAllLines可以很容易地分辨出来,这是一件好事。(尽管这更多是因为方法重载不能仅因返回类型而异)。也许ReadRecords和ReadAllRecords也适合作为命名方案。
nvoigt

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.