C#IEnumerator / yield结构可能不好?

背景:我从数据库中得到了一堆字符串,并且我想返回它们。 传统上,它会是这样的:

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

但后来我认为消费者会想遍历这些项目,并不关心其他事情,而且我不想把自己放到List中,所以如果我返回一个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,获取其中的一半,然后陷入循环......就我所见,我的数据库连接正在进行永远保持开放。

听起来像在某些情况下要求麻烦,如果迭代器没有完成...我错过了什么?


这是一个平衡的行为:您是否想要立即强制所有数据存储到内存中,这样您就可以释放连接,还是希望从流式传输数据中受益,但花费时间来维系连接?

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

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项)(例如:after第10项)。 这可以加快查询数据。 特别是在Oracle环境中,服务器端游标是检索数据的常用方式。


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

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

链接地址: http://www.djcxy.com/p/53785.html

上一篇: C# IEnumerator/yield structure potentially bad?

下一篇: Lambda Expressions and Extension methods