33

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

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 似乎没有提到这一点),它显然是一个懒惰的评估器,它保持填充器的状态,期待有人问下一个值,然后只运行它直到它返回下一个值。

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

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

4

11 回答 11

44

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

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

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

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

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

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

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

于 2009-04-29T19:36:29.850 回答
10

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是(使用服务器端游标时),如果您的消费者想要更早地退出循环(例如:在第 10 项)。这可以加快查询数据的速度。尤其是在 Oracle 环境中,服务器端游标是检索数据的常用方法。

于 2009-04-29T19:48:33.143 回答
8

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

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

于 2009-04-29T19:28:20.540 回答
6

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

我的意思是:

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

这与我在这里的想法有点联系:Pragmatic LINQ

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

于 2009-04-30T04:17:08.367 回答
3

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

using System.Linq;

//...

var stuff = GetStuff(connectionString).ToList();
于 2010-09-23T22:19:09.303 回答
1

这会导致问题的唯一方法是调用者滥用IEnumerable<T>. 使用它的正确方法是Dispose在不再需要它时调用它。

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

有许多语言特性(特别是foreach)使其非常容易IEnumerable<T>正确使用。

于 2009-04-29T21:19:31.350 回答
1

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

于 2009-04-29T19:29:00.913 回答
0

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

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

于 2009-05-21T20:21:14.747 回答
0

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

于 2009-04-29T19:37:14.543 回答
-1

您可以做的是改用 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 并做他们不应该做的事情。

于 2009-04-29T19:39:20.357 回答
-2

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

于 2009-04-29T19:29:26.840 回答