Dispose срещу итераторни блокове

Тези два въпроса почти отговарят на моя въпрос, но не съвсем. Считайте това за последващ въпрос към тези.

Разбирам, че foreach цикъл ще Dispose на изброител, когато приключи с него.

Въпросът ми е следният:

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

РЕДАКТИРАНЕ: Този фрагмент демонстрира отговора на @JonSkeet.

РЕДАКТИРАНЕ #2: Този фрагмент, базиран на коментарите на @JonSkeet, демонстрира идеална употреба. Итераторният блок е отговорен за живота на необходимите ресурси, а не за самия изброим обект. По този начин, enumerable може да бъде изброен многократно, ако е необходимо - всеки enumerable има свой собствен ресурс, с който да работи.


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