【问题标题】:C# IEnumerator/yield structure potentially bad?C# IEnumerator/yield 结构可能不好?
【发布时间】:2010-10-22 16:16:26
【问题描述】:

背景:我有一堆从数据库中获取的字符串,我想返回它们。传统上,它会是这样的:

public List<string> GetStuff(string connectionString)
{
    List<string> categoryList = new List<string>();
    using (SqlConnection sqlConnection = new SqlConnection(connectionString))
    {
        string commandText = "GetStuff";
        using (SqlCommand sqlCommand = new SqlCommand(commandText, sqlConnection))
        {
            sqlCommand.CommandType = CommandType.StoredProcedure;

            sqlConnection.Open();
            SqlDataReader sqlDataReader = sqlCommand.ExecuteReader();
            while (sqlDataReader.Read())
            {
                categoryList.Add(sqlDataReader["myImportantColumn"].ToString());
            }
        }
    }
    return categoryList;
}

但是我认为消费者会想要遍历这些项目并且不太关心其他内容,而且我不想将自己放入一个列表中,本质上,所以如果我返回一个 IEnumerable一切都很好/灵活。所以我在想我可以使用“收益回报”类型的设计来处理这个......就像这样:

public IEnumerable<string> GetStuff(string connectionString)
{
    using (SqlConnection sqlConnection = new SqlConnection(connectionString))
    {
        string commandText = "GetStuff";
        using (SqlCommand sqlCommand = new SqlCommand(commandText, sqlConnection))
        {
            sqlCommand.CommandType = CommandType.StoredProcedure;

            sqlConnection.Open();
            SqlDataReader sqlDataReader = sqlCommand.ExecuteReader();
            while (sqlDataReader.Read())
            {
                yield return sqlDataReader["myImportantColumn"].ToString();
            }
        }
    }
}

但是现在我正在阅读更多关于产量的内容(在这样的网站上......msdn 似乎没有提到这一点),它显然是一个懒惰的评估器,它保持填充器的状态在预期中有人要求下一个值,然后只运行它直到它返回下一个值。

在大多数情况下这看起来不错,但使用 DB 调用,这听起来有点冒险。作为一个有点人为的例子,如果有人从我从数据库调用中填充的 IEnumerable 中请求一个 IEnumerable,通过它的一半,然后陷入循环......据我所知,我的数据库连接正在进行永远保持开放。

在某些情况下,如果迭代器没有完成,这听起来像是自找麻烦……我错过了什么吗?

【问题讨论】:

  • 感谢您的编辑,乔恩...这就是我即时打字的结果。
  • 只要您的消费者在 IEnumerator 上调用Dispose,您就安全了。请参阅下面的帖子。
  • 这有点不相关,我不确定当时是否属实,但对于未来的读者,SqlDataReader 实现了IDisposable 所以你也应该将它包装在 using 语句中(或者一个新的 c# 8 using 声明)

标签: c# .net database resources yield


【解决方案1】:

强制评估迭代器的更简洁的方法:

using System.Linq;

//...

var stuff = GetStuff(connectionString).ToList();

【讨论】:

    【解决方案2】:

    我曾多次撞到这堵墙。 SQL 数据库查询不像文件那样容易流式传输。相反,只查询您认为需要的数量,并将其作为您想要的任何容器返回(IList&lt;&gt;DataTable 等)。 IEnumerable 在这里帮不了你。

    【讨论】:

      【解决方案3】:

      您始终可以使用单独的线程来缓冲数据(可能是队列),同时也可以通过 yield 来返回数据。当用户请求数据(通过 yield 返回)时,从队列中删除一个项目。数据也通过单独的线程不断地添加到队列中。这样,如果用户请求数据的速度足够快,队列永远不会很满,您不必担心内存问题。如果他们不这样做,那么队列将填满,这可能还不错。如果您想对内存施加某种限制,您可以强制执行最大队列大小(此时另一个线程将等待项目被删除,然后再将更多项目添加到队列中)。自然,您需要确保在两个线程之间正确处理资源(即队列)。

      作为替代方案,您可以强制用户传入一个布尔值来指示是否应缓冲数据。如果为真,则缓冲数据并尽快关闭连接。如果为 false,则不会缓冲数据,并且只要用户需要,数据库连接就会保持打开状态。拥有布尔参数会强制用户做出选择,从而确保他们了解问题。

      【讨论】:

        【解决方案4】:

        顺便说一句 - 请注意,IEnumerable&lt;T&gt; 方法本质上是 LINQ 提供程序(LINQ-to-SQL、LINQ-to-Entities)的谋生手段。正如乔恩所说,这种方法具有优势。然而,也存在一定的问题——尤其是(对我而言)在(组合)分离方面 |抽象。

        我的意思是:

        • 在 MVC 场景中(例如),您希望您的“获取数据”步骤实际获取数据,以便您可以在 控制器 上测试它是否正常工作,而不是视图(不必记得致电.ToList() 等)
        • 您不能保证另一个 DAL 实现将能够流式传输数据(例如,POX/WSE/SOAP 调用通常不能流式传输记录);并且您不一定想让行为变得令人困惑(即在迭代期间连接仍然打开一个实现,而关闭另一个实现)

        这与我的想法有点相关:Pragmatic LINQ

        但我应该强调 - 流媒体肯定是非常受欢迎的时候。这不是一个简单的“总是与从不”的事情......

        【讨论】:

          【解决方案5】:

          这会导致问题的唯一方法是调用者滥用IEnumerable&lt;T&gt; 的协议。正确的使用方法是在不再需要时调用Dispose

          yield return 生成的实现将Dispose 调用作为信号来执行任何打开的finally 块,在您的示例中,该块将对您在using 语句中创建的对象调用Dispose .

          有许多语言特性(尤其是foreach)使得正确使用IEnumerable&lt;T&gt; 变得非常容易。

          【讨论】:

          • 如果您可以找到一些关于 Dispose 如何被/在通过 yield return 关键字实现的枚举器中使用的文档,那将会很有帮助。
          【解决方案6】:

          IEnumerable 并不总是不安全的。如果你离开框架调用GetEnumerator(这是大多数人会做的),那么你是安全的。基本上,你和使用你的方法的代码的谨慎一样安全:

          class Program
          {
              static void Main(string[] args)
              {
                  // safe
                  var firstOnly = GetList().First();
          
                  // safe
                  foreach (var item in GetList())
                  {
                      if(item == "2")
                          break;
                  }
          
                  // safe
                  using (var enumerator = GetList().GetEnumerator())
                  {
                      for (int i = 0; i < 2; i++)
                      {
                          enumerator.MoveNext();
                      }
                  }
          
                  // unsafe
                  var enumerator2 = GetList().GetEnumerator();
          
                  for (int i = 0; i < 2; i++)
                  {
                      enumerator2.MoveNext();
                  }
              }
          
              static IEnumerable<string> GetList()
              {
                  using (new Test())
                  {
                      yield return "1";
                      yield return "2";
                      yield return "3";
                  }
              }
          
          }
          
          class Test : IDisposable
          {
              public void Dispose()
              {
                  Console.WriteLine("dispose called");
              }
          }
          

          您是否可以将数据库连接保持打开状态也取决于您的架构。如果调用者参与事务(并且您的连接是自动登记的),那么框架无论如何都会保持连接打开。

          yield 的另一个优点是(使用服务器端游标时),如果您的消费者想要退出循环,您的代码不必从数据库中读取所有数据(例如:1,000 个项目)较早(例如:第 10 项之后)。这可以加快查询数据的速度。尤其是在 Oracle 环境中,服务器端游标是检索数据的常用方法。

          【讨论】:

          • +1 了解有关处置的详细信息,但我认为这不是问题 - 我相信 Beska 担心调用者循环的某些迭代需要很长时间处理,在不需要时让数据库连接保持打开状态。
          • 谢谢,更新了我关于保持连接打开的愿景。
          【解决方案7】:

          你没有错过任何东西。您的示例显示了如何不使用收益回报。将项目添加到列表中,关闭连接并返回列表。您的方法签名仍然可以返回 IEnumerable。

          编辑:也就是说,Jon 有一个观点(太惊讶了!):从性能的角度来看,流式传输实际上是最好的选择。毕竟,如果我们在这里讨论的是 100,000 行(1,000,000?10,000,000?)行,您不希望先将它们全部加载到内存中。

          【讨论】:

          • 是的...我只是强调它的 IEnumerable 方面,因为这就是让我首先想到使用 yield 的原因。感谢您的回答...很高兴看到我没有完全找错树。
          • 别担心,伙计,很高兴能帮上忙。如果这已经回答了您的问题,请不要忘记将其标记为答案,以便它从未回答的问题列表中删除。
          • 哦,我几乎总是将我的问题标记为已回答......但我想坚持这个问题,因为 Jon 的观点略有不同,而且我想看看结果如何。
          • 嗯......就像在骗取代表?我没有想到这一点......我宁愿不要让人们认为我只是想勉强获得代表。我是否应该将此标记为已关闭并希望人们继续关注它?它看起来不像是社区 wiki 的东西......
          • 无论如何它都不会出现在未回答的问题列表中,因为它有投票的答案。
          【解决方案8】:

          您可以做的是改用 SqlDataAdapter 并填充 DataTable。像这样的:

          public IEnumerable<string> GetStuff(string connectionString)
          {
              DataTable table = new DataTable();
              using (SqlConnection sqlConnection = new SqlConnection(connectionString))
              {
                  string commandText = "GetStuff";
                  using (SqlCommand sqlCommand = new SqlCommand(commandText, sqlConnection))
                  {
                      sqlCommand.CommandType = CommandType.StoredProcedure;
                      SqlDataAdapter dataAdapter = new SqlDataAdapter(sqlCommand);
                      dataAdapter.Fill(table);
                  }
          
              }
              foreach(DataRow row in table.Rows)
              {
                  yield return row["myImportantColumn"].ToString();
              }
          }
          

          这样,您可以一次性查询所有内容,并立即关闭连接,但您仍然懒惰地迭代结果。此外,此方法的调用者不能将结果转换为 List 并做他们不应该做的事情。

          【讨论】:

          • 我不明白这个例子中“懒惰地迭代结果”的意义。
          • 我认为关键是 OP 不会被绑定到 List (这就是他首先采用 yield 方法的原因),但同时这不会t 保持数据库连接打开。
          • 好吧,无论采用哪种方法,我都不需要绑定到 List;无论哪种方式,我都可以返回 IEnumerable 。我只是在考虑转向比 List 更通用的东西,这让我开始思考产量以及它的潜在后果。
          • 不,只是其他人建议将列表返回为 IEnumerable。虽然它可能不会发生,但方法的调用者可以将其转换为列表,而如果使用 yield,则不能。
          • 我想这是真的,但这不是不这样做的理由。无缘无故到处使用迭代器是愚蠢的,因为你担心调用者的一些奇怪行为(无论如何都不会破坏任何东西的行为。)
          【解决方案9】:

          这是一种平衡行为:您是想立即将所有数据强制存储到内存中,以便释放连接,还是想从流式传输数据中受益,但代价是一直占用连接?

          在我看来,这个决定可能应该取决于调用者,他们更了解自己想要做什么。如果您使用迭代器块编写代码,调用者可以非常轻松地将流形式转换为完全缓冲的形式:

          List<string> stuff = new List<string>(GetStuff(connectionString));
          

          另一方面,如果您自己进行缓冲,则调用者无法返回到流模型。

          所以我可能会使用流模型并在文档中明确地说明它的作用,并建议调用者做出适当的决定。您甚至可能希望提供一个辅助方法来基本上调用流式版本并将其转换为列表。

          当然,如果你不相信你的调用者会做出正确的决定,并且你有充分的理由相信他们永远不会真正想要流式传输数据(例如,无论如何它永远不会返回太多),然后去对于列表方法。无论哪种方式,都要记录下来 - 它很可能会影响返回值的使用方式。

          当然,处理大量数据的另一种选择是使用批处理 - 这有点偏离最初的问题,但在流式传输通常很有吸引力的情况下,这是一种不同的考虑方法。

          【讨论】:

          • 您概述的选择是正确的,但我认为应该更加重视默认不流式传输的决定。将连接或资源捆绑起来会导致可伸缩性问题。默认行为应该是理智的,不会引起问题。
          【解决方案10】:

          这里不要使用yield。你的样品很好。

          【讨论】:

            【解决方案11】:

            不,你走在正确的道路上...... yield 将锁定阅读器......你可以在调用 IEnumerable 时测试它进行另一个数据库调用

            【讨论】:

            • 您在连接字符串中启用 MARS 以允许多个打开的 SqlDataReader 性能受到影响。但是,这种模式仍然存在问题。
            猜你喜欢
            • 2023-03-21
            • 2011-11-04
            • 2013-07-06
            • 1970-01-01
            • 1970-01-01
            • 2019-03-25
            • 2019-06-19
            • 2021-05-07
            • 2016-06-21
            相关资源
            最近更新 更多