Удаление перечислителя, когда не используется использование, foreach или ручной вызов Dispose()

Я использую yield return для перебора записей SqlDataReader:

IEnumerable<Reading> GetReadings() {
    using (var connection = new SqlConnection(_connectionString))
    {
        using (var command = new SqlCommand(_query, connection))
        {
            connection.Open();
            using (var reader = command.ExecuteReader())
            {
                while (reader.Read())
                {
                    yield return new Reading
                    {
                        End = reader.GetDateTime(0),
                        Value = reader.GetDouble(1)
                    };
                }
            }
            connection.Close();
        }
    }
}

Затем я использую адаптированную версию этого принятого ответа. для "сжатия" множества итераторов вместе:

   var enumerators = data.Select(d => new
   {
       d.Key,
       Enumerator = d.Value.GetEnumerator()
   }).ToList();

   while (true)
   {
       foreach (var item in enumerators)
       {
           if (!item.Enumerator.MoveNext())
           {
               yield break;
           }

           /*snip*/
       }

      /*snip*/
   }

В приведенном выше методе Dispose() перечислителя не вызывается явно, и, поскольку они не используются в операторах using или foreach, останется ли базовый итератор в открытом состоянии? В моем случае с открытым SqlConnection.

Должен ли я вызывать Dispose() счетчиков, чтобы убедиться, что вся нижестоящая цепочка закрыта?


person Oliver    schedule 24.01.2014    source источник
comment
Почему бы вам не использовать синтаксис using? Он вызывается автоматически, когда заканчивается контекст блока, который включает yield и return.   -  person Yuck    schedule 24.01.2014
comment
Наличие переменного количества перечислителей в методе стиля zipmany затрудняет обертку каждого из них с помощью using.   -  person Oliver    schedule 24.01.2014


Ответы (3)


При перечислении этого итератора, если Dispose() перечислителя не вызывается явно и не используется в операторе using, останется ли базовый итератор в открытом состоянии?

Позвольте мне перефразировать этот вопрос в форме, на которую будет легче ответить.

При использовании foreach для перечисления через блок итератора, содержащий оператор using, удаляются ли ресурсы, когда управление выходит из цикла?

да.

Какие механизмы обеспечивают это?

Эти три:

  • Оператор using — это просто удобный способ написать try-finally, где finally распоряжается ресурсом.

  • Цикл foreach является также удобным синтаксисом для try-finally, и снова finally вызывает Dispose в перечислителе, когда управление покидает цикл.

  • Перечислитель, созданный блоком итератора, реализует IDisposable. Вызов Dispose() гарантирует выполнение всех finally блоков в блоке итератора, включая finally блоков, полученных из using операторов.

Если я избегаю цикла foreach, вызываю GetEnumerator сам и не вызываю Dispose для перечислителя, есть ли у меня гарантия, что finally блоков перечислителя будут работать?

Неа. Всегда избавляйтесь от счетчиков. Они реализуют IDisposable по какой-то причине.

Теперь ясно?

Если эта тема вас интересует, вам следует прочитать мою длинную серию статей о характеристиках конструкции блоков итераторов в C#.

http://blogs.msdn.com/b/ericlippert/archive/tags/iterators/

person Eric Lippert    schedule 24.01.2014
comment
А как насчет item, который сам является Enumerator? - person Hamlet Hakobyan; 24.01.2014
comment
@HamletHakobyan: IEnumerator<T> реализует IDisposable. Если у вас есть IDisposable и вы никогда не вызываете Dispose(), код удаления не запускается. - person Eric Lippert; 24.01.2014
comment
Да, это очевидно. Мой комментарий зависит от вопроса OP, и я вижу, что вопрос и ответ вводят в заблуждение. - person Hamlet Hakobyan; 24.01.2014

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

Так что все зависит от того, что вы имеете в виду, говоря о «действительном» состоянии.

Если оно заключено в usingдирективу, которая не что иное, как try/finally, у вас есть гарантия, что даже если в итерации произойдет какое-либо исключение, соединение будет закрыто, и его ресурсы будут уничтожены. В других случаях вы должны справиться со всем этим самостоятельно.

person Tigran    schedule 24.01.2014

Просто добавим к этому нюанс:

Перечислители массивов НЕ являются универсальными и не одноразовыми.

Перечислители коллекций ЯВЛЯЮТСЯ универсальными и одноразовыми, но только если сам T является ссылочным типом... не типом значения

Поэтому, если вы используете GetEnumerator(), обязательно используйте

IEnumerator arrayEnumerator = arrayToEnumerate.GetEnumerator();
IEnumerator<T> collectionEnumerator = collectionToEnumerate.GetEnumerator();

затем позже в вашем коде

collectionEnumerator.Dispose(); // compiles if T is reference-type but NOT value-type

Примеры:

object[] theObjectArray = [...]
IEnumerator arrayEnumerator = theObjectArray.GetEnumerator(); // does not return 
IEnumerator<object> and is not disposable
arrayEnumerator.Dispose(); // won't compile

List<double> theListOfDoubles = [...]
IEnumerator<someObject> doubleEnumerator = doubleEnumerator.GetEnumerator();
doubleEnumerator.Dispose(); // won't compile

List<someObject> theListOfObjects = [...]
IEnumerator<someObject> objectEnumerator = theListOfObjects.GetEnumerator(); // disposable
objectEnumerator.Dispose(); // compiles & runs
person Community    schedule 28.07.2020