Понимая, как yield
работает ключевое слово, я наткнулся на link1 и link2 в StackOverflow, который поддерживает использование yield return
итерации по DataReader, и это также удовлетворяет мои потребности. Но меня удивляет, что произойдет, если я буду использовать, yield return
как показано ниже, и если я не буду перебирать весь DataReader, будет ли соединение с БД оставаться открытым навсегда?
IEnumerable<IDataRecord> GetRecords()
{
SqlConnection myConnection = new SqlConnection(@"...");
SqlCommand myCommand = new SqlCommand(@"...", myConnection);
myCommand.CommandType = System.Data.CommandType.Text;
myConnection.Open();
myReader = myCommand.ExecuteReader(CommandBehavior.CloseConnection);
try
{
while (myReader.Read())
{
yield return myReader;
}
}
finally
{
myReader.Close();
}
}
void AnotherMethod()
{
foreach(var rec in GetRecords())
{
i++;
System.Console.WriteLine(rec.GetString(1));
if (i == 5)
break;
}
}
Я попробовал тот же пример в образце консольного приложения и заметил при отладке, что блок finally GetRecords()
не выполняется. Как я могу обеспечить закрытие соединения с БД? Есть ли лучший способ, чем использовать yield
ключевое слово? Я пытаюсь разработать собственный класс, который будет отвечать за выполнение отдельных SQL и хранимых процедур в БД и будет возвращать результат. Но я не хочу возвращать DataReader вызывающей стороне. Также я хочу убедиться, что соединение будет закрыто во всех сценариях.
Изменить Изменен ответ на ответ Бена, так как неверно ожидать, что вызывающие методы будут правильно использовать метод, и в отношении соединения с БД будет дороже, если метод вызывается несколько раз без какой-либо причины.
Спасибо Якобу и Бену за подробное объяснение.
Ответы:
Да, вы столкнетесь с проблемой, которую вы описываете: пока вы не закончите итерацию по результату, вы будете держать соединение открытым. Есть два основных подхода, которые я могу придумать, чтобы справиться с этим:
Тяни, не тяни
В настоящее время вы возвращаете
IEnumerable<IDataRecord>
структуру данных, из которой вы можете извлечь. Вместо этого вы можете переключить свой метод, чтобы вытолкнуть его результаты. Самый простой способ - передать значение,Action<IDataRecord>
которое вызывается на каждой итерации:Обратите внимание, что, учитывая, что вы имеете дело с набором элементов,
IObservable
/IObserver
может быть немного более подходящей структурой данных, но если вам это не нужно, простоеAction
гораздо проще.Охотно оценивать
Альтернативой является просто убедиться, что итерация полностью завершена перед возвратом.
Обычно это можно сделать, просто поместив результаты в список, а затем вернув его, но в этом случае есть дополнительная сложность каждого элемента, являющаяся той же ссылкой на читателя. Так что вам нужно что-то, чтобы извлечь нужный результат из читателя:
источник
Ваш
finally
блок всегда будет выполняться.При использовании
yield return
компилятор создаст новый вложенный класс для реализации конечного автомата.Этот класс будет содержать весь код из
finally
блоков как отдельные методы. Он будет отслеживатьfinally
блоки, которые должны быть выполнены в зависимости от состояния. Все необходимыеfinally
блоки будут выполнены вDispose
методе.Согласно спецификации языка C #,
foreach (V v in x) embedded-statement
расширен допоэтому перечислитель будет удален, даже если вы выйдете из цикла с помощью
break
илиreturn
.Чтобы получить больше информации о реализации итератора, вы можете прочитать эту статью от Jon Skeet
редактировать
Проблема такого подхода состоит в том, что вы полагаетесь на клиентов вашего класса, чтобы использовать метод правильно. Они могли бы, например, получить перечислитель напрямую и проходить по нему в
while
цикле, не утилизируя его.Вы должны рассмотреть одно из решений, предложенных @BenAaronson.
источник
var records = GetRecords(); var first = records.First(); var count = records.Count()
фактически выполнит этот метод дважды, открывая и закрывая соединение и читатель каждый раз. Итерирование дважды делает одно и то же. Вы также можете долго передавать результат, прежде чем фактически итерировать его и т. Д. Ничто в сигнатуре метода не подразумевает такого поведенияGetRecords
returningIEnumerable
, я ожидаю, что он загрузит записи в память. С другой стороны, если бы это было свойствоRecords
, я бы предпочел предположить, что это какая-то абстракция над базой данных.Независимо от конкретного
yield
поведения, ваш код содержит пути выполнения, которые не будут правильно распоряжаться вашими ресурсами. Что если ваша вторая строка выдает исключение или вашу третью? Или даже ваш четвертый? Вам понадобится очень сложная цепочка try / finally, или вы можете использоватьusing
блоки.Люди отметили, что это не интуитивно понятно, что люди, вызывающие ваш метод, могут не знать, что при перечислении результата дважды вызовет метод дважды. Ну, удачи. Так работает язык. Это сигнал, который
IEnumerable<T>
посылает. Есть причина, по которой это не возвращаетсяList<T>
илиT[]
. Люди, которые не знают этого, должны быть образованными, а не работать вокруг.Visual Studio имеет функцию под названием статический анализ кода. Вы можете использовать его, чтобы узнать, правильно ли вы распорядились ресурсами.
источник
IEnumerable
и делать все что угодно, например, выбрасывать совершенно не связанные исключения или записывать файлы 5 ГБ на диск. Тот факт, что язык допускает это, не означает, что он может возвращатьIEnumerable
метод, который делает это, из метода, который не указывает, что это произойдет.IEnumerable<T>
является признаком того, что его двойное перечисление приведет к двум вызовам. Вы даже получите полезные предупреждения в своих инструментах, если перечислите их несколько раз. Пункт о создании исключений одинаково действителен независимо от типа возвращаемого значения. Если этот метод вернет a,List<T>
он выдаст те же исключения.IEnumerable
тогдашний предлог. Но я также думаю, что как автор метода вы обязаны придерживаться того, что подразумевает ваша подпись. Метод вызываетсяGetRecords
, поэтому, когда он вернется, вы ожидаете, что он получит записи . Если бы это назвалиGiveMeSomethingICanPullRecordsFrom
(или, более реалистичноGetRecordSource
или подобный), тогда ленивыйIEnumerable
был бы более приемлемым.File
,ReadLines
иReadAllLines
может быть легко сказано отдельно, и это хорошо. (Хотя это больше из-за того, что перегрузка метода не может отличаться только типом возвращаемого значения). Может быть, ReadRecords и ReadAllRecords подойдут здесь в качестве схемы именования.То, что вы сделали, кажется неправильным, но будет работать нормально.
Вы должны использовать IEnumerator <> , так как он также наследует IDisposable .
Но поскольку вы используете foreach, компилятор все еще использует IDisposable для генерации IEnumerator <> для foreach.
на самом деле foreach включает в себя многое изнутри.
Проверьте /programming/13459447/do-i-need-to-consider-disposing-of-any-ienumerablet-i-use
источник