2009-04-29 2 views
33

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

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

+0

Спасибо за редактирование, Джон ... это то, что я получаю для набора текста на лету. – Beska

+1

Пока ваш потребитель называет «Dispose» на IEnumerator, вы в безопасности. См. Мои публикации ниже. – tofi9

ответ

44

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

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

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

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

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

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

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

+0

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

8

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

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

+1

Yep ... Я только подчеркивал IEnumerable аспект этого, потому что именно это заставило меня подумать об использовании урожая в первую очередь. И спасибо за ответ ... рад видеть, что я не лаюсь полностью на неправильное дерево. – Beska

+0

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

+0

О, я почти всегда отмечаю свои вопросы, как ответил ... но я хотел бы немного поработать над этим, так как Джон взвесил с несколько иной точки зрения, и я хотел бы посмотреть, как это играет вне. – Beska

1

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

+0

Вы включаете MARS в строке подключения, чтобы разрешить несколько открытых SqlDataReaders при достижении производительности. Но, тем не менее, у этой модели есть проблемы. – spoulson

-2

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

+0

Э? Что случилось с этим ответом? –

0

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

-1

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

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

+3

Я не понимаю, что точка «лениво итерации результата» находится в этом примере. – mquander

+0

Я думаю, дело в том, что OP не будет привязан к List <> (именно поэтому он пошел с подходом к доходности в первую очередь), но в то же время это не приведет к тому, что соединение с базой данных будет открыто. – Andy

+0

Ну, при любом подходе мне не нужно привязываться к списку <>; Я мог бы вернуть IEnumerable <> в любом случае. Я просто думал о продвижении к чему-то более универсальному, чем List <>, и именно это заставило меня задуматься о уроне и о возможных последствиях этого. – Beska

10

Вы не всегда небезопасны с IEnumerable. Если вы оставите фрейм-код GetEnumerator (это то, что сделает большинство людей), тогда вы в безопасности. В принципе, вы же безопасны, как carefullness кода, используя свой метод:

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

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

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

+3

+1 для получения дополнительной информации об утилизации, но я не думаю, что это было проблемой - я верю * Беска беспокоит некоторые итерации цикла звонящего, занимающие очень много времени для обработки, оставляя соединение с базой данных открытым, действительно нужно. –

+0

Спасибо, обновлено с моим видением о том, чтобы открыть соединение. – tofi9

1

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

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

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

+0

Если вы можете разместить некоторую документацию о том, как Dispose используется/в перечислениях, реализованных с помощью ключевых слов возврата доходности, это было бы полезно. – jpierson

6

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

Что я имею в виду вот что:

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

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

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

0

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

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

3

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

using System.Linq; 

//... 

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

 Смежные вопросы

  • Нет связанных вопросов^_^