C # IEnumerator / структура выхода потенциально плохая? - PullRequest
32 голосов
/ 29 апреля 2009

Справочная информация: у меня есть куча строк, которые я получаю из базы данных, и я хочу их вернуть. Традиционно это будет примерно так:

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 из того, что я заполняю из вызова БД, проходит через половину этого, а затем застревает в цикле ... насколько я вижу, происходит мое соединение с БД оставаться открытым навсегда.

Похоже, что в некоторых случаях возникает проблема, если итератор не завершает работу ... я что-то упустил?

Ответы [ 11 ]

44 голосов
/ 29 апреля 2009

Это балансирование: вы хотите немедленно принудительно поместить все данные в память, чтобы вы могли освободить соединение, или вы хотите получить выгоду от потоковой передачи данных, за счет того, что соединение все это время стоит

С моей точки зрения, это решение должно приниматься самим абонентом, который знает больше о том, что они хотят сделать. Если вы пишете код с использованием блока итератора, вызывающая сторона может очень легко превратить эту потоковую форму в полностью буферизованную форму:

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

Если, с другой стороны, вы выполняете буферизацию самостоятельно, вызывающий абонент не сможет вернуться к потоковой модели.

Таким образом, я бы, вероятно, использовал потоковую модель и прямо сказал бы в документации , что она делает, и посоветовал бы вызывающему абоненту принять соответствующее решение. Возможно, вы даже захотите предоставить вспомогательный метод для вызова потоковой версии и преобразования ее в список.

Конечно, если вы не доверяете своим абонентам в принятии соответствующего решения, и у вас есть веские основания полагать, что они никогда не захотят потоковую передачу данных (например, в любом случае они никогда не будут возвращать много), затем перейдите для списка подход. В любом случае, документируйте это - это может очень хорошо повлиять на использование возвращаемого значения.

Другой вариант для работы с большими объемами данных, конечно, заключается в использовании пакетов - это несколько отходит от первоначального вопроса, но это другой подход, который следует учитывать в ситуации, когда потоковая передача обычно привлекательна.

10 голосов
/ 29 апреля 2009

Вы не всегда небезопасны с 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 заключается в том, что (при использовании серверного курсора) ваш код не должен считывать все данные (например, 1000 элементов) из базы данных, если ваш потребитель хочет выйти из цикла раньше (пример: после 10-го пункта). Это может ускорить запрос данных. Особенно в среде Oracle, где серверные курсоры являются распространенным способом получения данных.

8 голосов
/ 29 апреля 2009

Вы ничего не пропустили. Ваш пример показывает, как НЕ использовать доходность. Добавьте элементы в список, закройте соединение и верните список. Ваша сигнатура метода все еще может возвращать IEnumerable.

Редактировать: Тем не менее, Джон имеет точку зрения (так удивлен!): Бывают редкие случаи, когда потоковое вещание на самом деле лучше всего делать с точки зрения производительности. В конце концов, если мы говорим здесь о 100 000 (1 000 000? 10 000 000?) Строк, вы не хотите загружать все это в память в первую очередь.

6 голосов
/ 30 апреля 2009

В качестве отступления - обратите внимание, что подход IEnumerable<T> - это по существу , что поставщики LINQ (LINQ-to-SQL, LINQ-to-Entities) делают для жизни. Подход имеет преимущества, как говорит Джон. Однако есть и определенные проблемы - в частности (для меня) с точки зрения (комбинации) разделения | абстракция.

Я имею в виду следующее:

  • в сценарии MVC (например) вы хотите, чтобы ваш шаг "получения данных" равнялся фактическому получению данных , чтобы вы могли проверить его работу на контроллере , а не на просмотр (без необходимости звонить .ToList() и т. Д.)
  • вы не можете гарантировать, что другая реализация DAL будет способна для потоковой передачи данных (например, вызов POX / WSE / SOAP обычно не может передавать записи); и вы не обязательно хотите, чтобы поведение было смешным (например, соединение все еще открыто во время итерации с одной реализацией и закрыто для другой)

Это немного связано с моими мыслями здесь: Прагматичный LINQ .

Но я должен подчеркнуть - бывают моменты, когда потоковое вещание крайне желательно. Это не простая вещь "всегда против никогда" ...

3 голосов
/ 24 сентября 2010

Чуть более краткий способ форсировать оценку итератора:

using System.Linq;

//...

var stuff = GetStuff(connectionString).ToList();
1 голос
/ 30 апреля 2009

Единственный способ, которым это может вызвать проблемы, - это если вызывающий абонент злоупотребляет протоколом IEnumerable<T>. Правильный способ его использования - вызвать Dispose, когда он больше не нужен.

Реализация, сгенерированная yield return, принимает вызов Dispose в качестве сигнала для выполнения любых открытых finally блоков, которые в вашем примере будут вызывать Dispose для объектов, которые вы создали в операторах using .

Существует ряд языковых функций (в частности, foreach), которые позволяют очень просто использовать IEnumerable<T> правильно.

1 голос
/ 29 апреля 2009

Нет, вы находитесь на правильном пути ... yield блокирует читателя ... вы можете проверить это, выполнив другой вызов базы данных при вызове IEnumerable

0 голосов
/ 22 мая 2009

Вы всегда можете использовать отдельный поток для буферизации данных (возможно, в очередь), в то же время выполняя запрос на возврат данных. Когда пользователь запрашивает данные (возвращаемые через yeild), элемент удаляется из очереди. Данные также постоянно добавляются в очередь через отдельный поток. Таким образом, если пользователь запрашивает данные достаточно быстро, очередь никогда не будет переполнена, и вам не придется беспокоиться о проблемах с памятью. Если этого не произойдет, то очередь заполнится, что может быть не так уж и плохо. Если есть какое-то ограничение, которое вы хотели бы наложить на память, вы можете установить максимальный размер очереди (в этот момент другой поток будет ожидать удаления элементов, прежде чем добавлять новые в очередь). Естественно, вы захотите убедиться, что вы правильно обрабатываете ресурсы (то есть очередь) между двумя потоками.

В качестве альтернативы вы можете заставить пользователя передать логическое значение, указывающее, следует ли буферизовать данные. Если true, данные буферизуются и соединение закрывается как можно скорее. Если false, данные не буферизуются, и соединение с базой данных остается открытым до тех пор, пока это необходимо пользователю. Наличие логического параметра заставляет пользователя сделать выбор, который гарантирует, что он знает о проблеме.

0 голосов
/ 29 апреля 2009

Что вы можете сделать, это использовать вместо этого 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();
    }
}

Таким образом, вы запрашиваете все за один снимок и немедленно закрываете соединение, но все еще лениво повторяете результат. Кроме того, вызывающая сторона этого метода не может привести результат к списку и сделать что-то, чего не следует делать.

0 голосов
/ 29 апреля 2009

Я сталкивался с этой стеной несколько раз. Запросы к базе данных SQL не так легко преобразовать, как файлы. Вместо этого запрашивайте столько, сколько вам нужно, и возвращайте его в любом контейнере, который вы хотите (IList<>, DataTable и т. Д.). IEnumerable здесь вам не поможет.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...