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

StackOverflow https://stackoverflow.com/questions/803878

Вопрос

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

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, все будет хорошо / гибко.Итак, я подумал, что мог бы использовать дизайн типа "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();
            }
        }
    }
}

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

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

Звучит как напрашиваться на неприятности в некоторых случаях, если итератор этого не делает finish...am Я что-то упускаю?

Это было полезно?

Решение

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

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

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

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

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

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

Что я имею в виду здесь, так это то, что:

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

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

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

Немного более краткий способ принудительной оценки итератора:

using System.Linq;

//...

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

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

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

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

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

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

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

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top