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

Bes*_*ska 32 .net c# database resources 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;
}
Run Code Online (Sandbox Code Playgroud)

但后来我认为消费者想要遍历这些项目而不关心其他的东西,而且我不想将自己打包到List中,所以如果我返回一个IEnumerable一切都很好/灵活.所以我在想我可以使用"yield return"类型设计来处理这个......就像这样:

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();
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

但是现在我正在阅读更多关于收益率的信息(在这样的网站上...... msdn似乎没有提到这一点),它显然是一个懒惰的评估者,它保持了populator的状态,期待有人问为下一个值,然后只运行它直到它返回下一个值.

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

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

Jon*_*eet 44

这是一种平衡行为:您是否希望立即将所有数据强制进入内存,以便释放连接,或者您是否希望从流式传输数据中受益,而代价是在所有时间内连接连接?

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

List<string> stuff = new List<string>(GetStuff(connectionString));
Run Code Online (Sandbox Code Playgroud)

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

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

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

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


tof*_*fi9 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");
    }
}
Run Code Online (Sandbox Code Playgroud)

是否可以让数据库连接保持打开状态取决于您的体系结构.如果调用者参与了一个事务(并且您的连接是自动登记的),那么无论如何该框架将保持连接打开.

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

  • 有关处置的详细信息,但我不认为这是关注点 - 我*相信*Beska担心调用者循环的一些迭代需要很长时间才能处理,如果没有,则保持数据库连接打开真的需要. (3认同)

Ric*_*lay 8

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

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


Mar*_*ell 6

另外请注意,该IEnumerable<T>方法基本上是LINQ提供程序(LINQ-to-SQL,LINQ-to-Entities)的用途.乔恩说,这种方法具有优势.然而,在分离(组合)方面,特别是(对我而言)也存在明确的问题 抽象.

我的意思是:

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

这与我的想法有点联系:务实的LINQ.

但我应该强调 - 肯定有时候流媒体是非常需要的.这不是一个简单的"永远与永不"的事情......