背景:我有一串串,我要从一个数据库,并且我想要返回他们。传统上,它将是这样的:

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

但后来我图消费者要想循环项目,并不关心有多少人和我想的不框自己在一个列表,本身,所以,如果我回一个综合的一切都是好的灵活性。所以我想我可以使用的"收益率"类型的设计,以处理这个...事情是这样的:

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似乎没有提到这一点),这显然是一个懒惰的评估,保持状态填充器周围,在预期的人要求为下一个值,然后才运行,直到返回未来的价值。

这似乎现在大多数情况下,但与数据库呼吁,这听起来有点冒险。作为一个有点人为的例子,如果有人问,为综合自我填充数据库通话,获得通过它的一半,然后被困在一个循环...尽我可以看到我的数据库连接是要留永远开放。

听起来像是自找麻烦在某些情况下,如果迭代不完成...我丢失的东西吗?

有帮助吗?

解决方案

这是一个平衡行为:你想立即强制所有数据进入内存,以便你可以释放连接,或者你想从流数据中受益,但代价是在所有时间内连接连接?

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

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

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

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

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

处理大量数据的另一个选择是使用批次,当然 - 这有点远离原始问题,但在流式传输通常具有吸引力的情况下,这是一种不同的方法。

其他提示

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环境中,服务器端游标是检索数据的常用方法。

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

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

作为一个除了-注意 IEnumerable<T> 方法是 基本上 什么皇宫提供商(皇宫-对-SQL,皇宫-对-实体)做为生活。这种方法的优点,因为乔恩说。然而,有明确的问题,特别是(对我)的条款(结合)分离|抽象概念。

我的意思是:

  • 在视的情况(例如)你想要你的"得到的数据"的步骤 实际上得到的数据, ,这样就可以测试它的工作原理在 控制器, 不该 (没有必要记住要呼叫 .ToList() 等)
  • 你不能不保证另一个达尔实施将 能够 流数据(例如,一个痘/华尔街/皂呼通常不能流的记录);和你不一定要让的行为容易混淆的不同(即连接仍然开放的迭代期间,与一个执行情况,并关闭为另一种)

这种联系在一位与我的想法: 务实的皇宫.

但我应该压力肯定有时候流是非常可取的。它不是一个简单的"总是vs永远"的事情...

稍微简化一下强制评估迭代器的方法:

using System.Linq;

//...

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

不,你走在正确的道路上......收益率会锁定读者......你可以在调用IEnumerable时测试它做另一个数据库调用

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

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

有许多语言功能(特别是 foreach ),可以很容易地正确使用 IEnumerable&lt; T&gt;

你总是可以使用一个单独的线程来缓冲数据(可能是一个队列),同时还要做一个事情来返回数据。当用户请求数据(通过yeild返回)时,项目将从队列中删除。数据也通过单独的线程连续添加到队列中。这样,如果用户足够快地请求数据,队列永远不会很满,您不必担心内存问题。如果他们没有,那么队列将填满,这可能不是那么糟糕。如果您希望对内存施加某种限制,则可以强制执行最大队列大小(此时另一个线程会在向队列添加更多内容之前等待项目被删除)。当然,您需要确保在两个线程之间正确处理资源(即队列)。

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

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

您可以使用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并执行他们不应该执行的操作。

不要在这里使用产量。你的样本很好。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top