Блоки Dispose и Iterator

Эти два вопроса почти отвечают на мой собственный вопрос, но не совсем. Считайте, что это дополнительный вопрос к ним.

Я понимаю, что цикл foreach будет Dispose перечислителя, когда с ним будет покончено.

Мой вопрос таков:

Если рассматриваемый перечислитель на самом деле является блоком итератора С# (т.е. GetEnumerator()/yield) класса IEnumerable<T>, который сам реализует IDisposable, могу ли я быть уверен, что сам объект будет удален? Или мне нужно прибегнуть к явному вызову GetEnumerator()/MoveNext()/Current внутри блока using?

EDIT: Этот фрагмент демонстрирует ответ @JonSkeet.

РЕДАКТИРОВАНИЕ №2: Этот фрагмент, основанный на комментариях @JonSkeet, демонстрирует идеальное использование. Блок итератора отвечает за время жизни необходимых ресурсов, а не сам перечислимый объект. Таким образом, перечислимое может быть перечислено несколько раз, если это необходимо - у каждого перечисляемого есть свой собственный ресурс для работы.


person CSJ    schedule 05.03.2014    source источник
comment
Можете ли вы опубликовать SSCCE? (Выполнение этого и его запуск в любом случае могут ответить на вопрос.) Я не совсем понимаю вопрос: никакие действия с перечислителем не приведут к удалению объекта коллекции, к которому он принадлежит.   -  person TypeIA    schedule 05.03.2014
comment
Вызывающий не может сказать, как был реализован метод итератора. Другой язык .NET может даже не знать этой концепции.   -  person usr    schedule 05.03.2014
comment
Вы спрашиваете, будет ли выполнение foreach поверх IEnumerator<T> where T : IEnumerable<U>, IDisposable удалять объекты типа U? Конечно нет, зачем это делать? Если я перечислю набор открытых сокетов, должны ли сокеты автоматически закрываться?   -  person Jon    schedule 05.03.2014
comment
Не совсем понятно, что вы имеете в виду - вы пытаетесь избавиться от каждого объекта, возвращаемого через итератор?   -  person Jon Skeet    schedule 05.03.2014
comment
@JonSkeet Нет, я пытаюсь убедиться, что Enumerable утилизирован. Представьте, что его конструктор получает дескриптор файла, а блок итератора возвращает каждую строку в файле.   -  person CSJ    schedule 05.03.2014
comment
@CSJ Ты уже это знаешь. Я знаю, что вы это знаете, потому что вы так прямо сказали в своем вопросе, я понимаю, что цикл foreach будет утилизировать перечислитель, когда он будет с ним сделан.   -  person Servy    schedule 05.03.2014
comment
@Servy Нет, я знаю, что анонимная вещь, выполняющая перечисление, будет удалена; неясно, будет ли также удален объект, который обслуживал перечислитель (т. е. перечисляемый). Это мой вопрос.   -  person CSJ    schedule 05.03.2014
comment
@CSJ Он не реализует IDisposable и не имеет метода Dispose, поэтому его нельзя удалить. IEnumerator<T> расширяет IDisposable, а не IEnumerable<T>.   -  person Servy    schedule 05.03.2014
comment
@Servy В своем исходном вопросе я утверждаю, что мой IEnumerable также явно реализует IDisposable.   -  person CSJ    schedule 05.03.2014
comment
@CSJ Это блок итератора. Вы не смогли бы заставить его сделать это, даже если бы попытались. Это класс, созданный компилятором. Если бы у вас был пользовательский объект, то есть не блок итератора, то он мог бы реализовать IDisposable, а foreach не избавился бы от него.   -  person Servy    schedule 05.03.2014


Ответы (2)


Если вы хотите избавиться от IEnumerable<T>, вам нужно сделать это самостоятельно с помощью оператора using (или вручную попробовать/наконец). Цикл foreach не автоматически удаляет IEnumerable<T> — только итератор, который он возвращает.

Итак, вам понадобится:

using (var enumerable = new MyEnumerable())
{
    foreach (var element in enumerable)
    {
        ...
    }
}

Не имеет никакого значения, реализует ли MyEnumerable IEnumerable<T> с помощью блока итератора или какого-либо другого кода.

person Jon Skeet    schedule 05.03.2014
comment
Я боялся этого. Альтернативой является выполнение любой очистки внутри самого блока итератора перед его выходом (например, путем вызова this.Dispose() непосредственно перед yield break, например). - person CSJ; 05.03.2014
comment
@CSJ: вам нужно будет сделать что-то вроде using (this) { ... } - не забывайте, что foreach может не перебирать все содержимое итератора. Однако я настоятельно рекомендую вам не этого делать. Было бы очень неидиоматично, если не сказать больше, чтобы итерация по последовательности избавлялась от самой последовательности. Звучит как довольно странный дизайн для начала... - person Jon Skeet; 05.03.2014
comment
Понимаю. Кажется, меня больше интересует создание перечислителя, чем перечисляемого. - person CSJ; 05.03.2014
comment
@CSJ Опять же, невозможно написать блок итератора, который генерирует одноразовый IEnumerable, поэтому его удаление внутри блока итератора является спорным вопросом. Не то, чтобы это был единственный контрольно-пропускной пункт. Это должен быть блок без итератора IEnumerable. Кроме того, IEnumerator должен был каким-то образом узнать, что IEnumerable создало. Это потенциально решаемая проблема, но все же плохая практика и совершенно ненужная. Если вы находитесь в такой ситуации, просто не создавайте одноразовый ресурс в файле IEnumerable. - person Servy; 05.03.2014
comment
@Servy: На самом деле, на практике IEnumerable<T>, возвращаемый из блока итератора, действительно реализует IDisposable. Это немного взломать ради эффективности. Но я согласен, что не следует ожидать этого :) - person Jon Skeet; 05.03.2014
comment
@JonSkeet Ну, в любом случае, вы не можете запустить какой-либо свой код, когда он удален, так что с его точки зрения это может быть и не IDisposable. - person Servy; 05.03.2014
comment
Я пытаюсь создать опыт, похожий на `foreach (строка var в File.ReadAllLines()) { ... }, не заставляя вызывающего что-либо удалять, но и не оставляя никаких дескрипторов. Похоже, возвращаемый объект должен быть перечислителем, а не перечисляемым. - person CSJ; 05.03.2014
comment
@CSJ: Если вы только откроете дескрипторы в блоке итератора и сделаете это в операторе using, все будет в порядке. Когда итератор удален, это вызовет любые соответствующие блоки finally (включая неявные из операторов using) в блоке итератора. - person Jon Skeet; 05.03.2014
comment
@CSJ Суть в том, что IEnumerable не открывает никаких ресурсов. Это IEnumerator, который фактически открывает файл. File.ReadLines возвращает IEnumerable (а не IEnumerator), который каждый раз, когда запрашивается создание нового IEnumerator, открывает новый файл. Следуйте этому шаблону. Вам не нужно (и, скорее всего, вы не захотите), чтобы ваш метод сам возвращал IEnumerator. - person Servy; 05.03.2014
comment
@Servy: я полностью согласен с мнением, но File.ReadLines - плохой пример, потому что он так плохо реализован. Если бы это было реализовано должным образом, все, что вы говорите, было бы правдой :) - person Jon Skeet; 05.03.2014
comment
@JonSkeet Здесь мы немного оффтопим, но чем это отличается от этого? - person Servy; 05.03.2014
comment
@Servy: Судя по всему, он фактически открывает файл немедленно и поддерживает одно средство чтения, сколько бы раз вы ни вызывали для него GetEnumerator. Попробуйте var lines = File.ReadLines("test.txt"); foreach (var x in lines) { foreach (var y in lines) { Console.WriteLine(x + " " + y); } } и посмотрите, что произойдет с test.txt из n строк. Он должен показать n^2 строк... но это не так :( - person Jon Skeet; 05.03.2014
comment
@JonSkeet А, да. По-видимому, он отлично обрабатывает последующие чтения: lines.Count();lines.Count(); в порядке. Проблема в том, что вызов GetEnumerator когда другой итератор в данный момент выполняет итерацию файла, возвращает тот же дескриптор, а не отдельный. Вызов GetEnumerator после завершения предыдущего вызова не проблема. Немного более простой способ демонстрации этого, чем ваш код, - это foreach (var line in lines) Console.WriteLine(lines.Count());, который генерирует исключение объекта, вместо того, чтобы просто печатать количество строк N раз. - person Servy; 05.03.2014
comment
@Servy: Верно. И то, что var lines = File.ReadLines("test.txt"); открывает файл вообще, тоже проблема. Бах. - person Jon Skeet; 05.03.2014
comment
@JonSkeet Я не думаю, что это так. По крайней мере, я не могу доказать, что это так. Я полагаю, что он не откроет файл, пока вы сначала не начнете повторять его. Просто он хранит файл в некотором состоянии, совместно используемом всеми IEnumerator экземплярами. Скорее всего, это потому, что он возвращает объект, который реализует и IEnumerable, и IEnumerator, а Enumerable возвращает себя. Так что (если я прав) это не так уж плохо (хотя и не идеально). - person Servy; 05.03.2014
comment
@Servy: я посмотрю, смогу ли я доказать это тебе... посмотри на это пространство :) - person Jon Skeet; 05.03.2014
comment
@JonSkeet неважно, ты прав. Он открывает файл. Простой пример: var handle = File.OpenWrite("output.txt"); var lines = File.ReadLines("output.txt"); (выдает исключение, в идеале этого не должно быть, пока lines не создаст итератор. - person Servy; 05.03.2014
comment
@Servy: Верно. Я собирался попробовать наоборот: вызвать ReadLines, а затем Delete :) - person Jon Skeet; 05.03.2014
comment
@Servy, JonSkeet: очень интересная дискуссия, в которой четко указаны соответствующие обязанности счетчика и перечисляемого. Итак, эталонный пример, которого следует избегать. - person CSJ; 05.03.2014
comment
Кстати, вызов Dispose на итераторе до вызова GetEnumerator не имеет никакого эффекта; вызов его после этого времени удалит самый первый созданный перечислитель, если он еще не был удален. - person supercat; 06.03.2014
comment
@supercat: под итератором вы подразумеваете возврат, возвращаемый методом, реализованным с помощью блока итератора? Если так, то я согласен. - person Jon Skeet; 06.03.2014
comment
@JonSkeet: Это действительно то, что я имел в виду. Такое поведение в основном безобидно, но оно иллюстрирует одну из трудностей, связанных с предположением, что экземпляры объектов, реализующих IDisposable, нуждаются в очистке. - person supercat; 06.03.2014

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

В этом весь смысл интерфейсов, таких как IDisposable. Вам не нужно заботиться о базовой реализации. Он всегда удаляет все. Что они решат делать с этим звонком, зависит от них.

Что касается IEnumerable<T> (не IEnumerator<T>), сгенерированного блоком итератора, он никогда не будет реализовывать IDisposable и, следовательно, не может быть удален. Если у вас есть пользовательский объект (а не блок итератора), который реализует IEnumerable<T> и IDisposable, то он не будет удален при использовании в foreach. Будет только IEnumerator<T> созданный через GetEnumerator.

person Servy    schedule 05.03.2014
comment
На самом деле итераторы, сгенерированные с помощью блока итератора, реализуют dispose, однако вам нужно попробовать\наконец, чтобы увидеть это. Вот пример SSCCE - person Scott Chamberlain; 05.03.2014
comment
@ScottChamberlain Это IEnumerator, а не IEnumerable. Большая разница. IEnumerable нет метода удаления. - person Servy; 05.03.2014
comment
Ааа, пропустил это. Прости. - person Scott Chamberlain; 05.03.2014