Подтвердить что ты не робот

С# IEnumerator/структура доходности потенциально плохая?

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

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

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

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

4b9b3361

Ответ 1

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

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

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

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

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

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

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

Ответ 2

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

Ответ 3

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

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

Ответ 4

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

Я имею в виду, что:

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

Это немного связано с моими мыслями: Pragmatic LINQ.

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

Ответ 5

Слегка более сжатый способ принудительной оценки итератора:

using System.Linq;

//...

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

Ответ 6

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

Ответ 7

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

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

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

Ответ 8

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

Ответ 9

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

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

Ответ 10

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

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

Ответ 11

Не используйте выход здесь. ваш образец в порядке.