Комментарии 36
Интересные моменты. Особенно про утиную типизацию. Если на Dispose еще можно было как то случайно нарваться, то про Enumerator вряд ли. Спасибо за заметку.
Если читать книжки по c#, например, C# in a Nutshell, можно узнать действительно все особенности языка, включая эти особенности foreach :)
await тоже работает на утиной типизации (ищет GetAwaiter).
Кажется, оба нюанса есть в книге Скита. По крайней мере второй точно.
Еще тонкость. Для старых компиляторов эквивалентный код был на самом деле такой:
То есть отличалось место определения переменной цикла.
Подробности: habrahabr.ru/post/141270/
Container container = new Container();
Enumerator enumerator = container.GetEnumerator();
try
{
object element;
while (enumerator.MoveNext())
{
element = enumerator.Current;
// содержимое foreach
}
}
finally
{
IDisposable disposable = enumerator as IDisposable;
if (disposable != null)
disposable.Dispose();
}
То есть отличалось место определения переменной цикла.
Подробности: habrahabr.ru/post/141270/
Декомпилятором, очевидно же…
Лично я сначала просто написал:
Лично я сначала просто написал:
public IEnumerable<string> Lines(string path) {
string line;
using (var file = File.OpenText(Path.Combine(basepath, path)))
while ((line = file.ReadLine()) != null)
yield return line;
}
А потом я задумался, в какой же момент этот файл будет закрыт…Без Dispose невозможно было бы отследить изменение коллекции в процессе итеррирования по ней. Ибо нельзя узнать, есть ли читающие пользователи у коллекции или нет. Так же написание многопоточных контейнеров тоже упрощается, так как энумератор это частичка контейнера, которая отдается наружу, и нужно точно знать когда все началось и когда все закончилось.
Можно реализовать следующим способом, отдал энумератор +1, отдал второй еще +1, вызывался диспоз -1 и т.д.
А в методе Add или Remove можно проверять равно ли число 0, если нет, то мы портим коллекцию для читающих пользователей.
Это один из вариантов, можно и другими способами.
А в методе Add или Remove можно проверять равно ли число 0, если нет, то мы портим коллекцию для читающих пользователей.
Это один из вариантов, можно и другими способами.
На самом деле, это реализовано немного по-другому: все операции изменения коллекции инкрементируют её версию. При создании итератора, в нём фиксируется текущая версия коллекции, а далее при продвижении вперед каждый раз проверяется, не изменилась ли версия. Если изменилась, значит коллекция была модифицирована, и, следовательно, надо кинуть исключение.
Допустим, отследить изменение коллекции можно запросто — достаточно счетчика модификаций в коллекции — и его копии в перечислителе. А вот если требуется хотя бы блокировка — тут да, без Dispose никак.
Собственно, именно так оно и реализуется в стандартных коллекциях — поле version в самой коллекции, которое инкрементируется при каждом изменении, и сохраняется в энумераторе в момент его создания — а потом сравнивается в Current/MoveNext. Коллекция о своих энумераторах не знает.
Dispose же нужен для коллекций и прочих перечислений, у которых энумераторы реально владеют какими-то требующими явного высвобождения ресурсами — например, курсором БД, или файловым хэндлом. А также для вызова блоков finally в методах-итераторах.
Dispose же нужен для коллекций и прочих перечислений, у которых энумераторы реально владеют какими-то требующими явного высвобождения ресурсами — например, курсором БД, или файловым хэндлом. А также для вызова блоков finally в методах-итераторах.
Извиняюсь, слово «невозможно» было очень сильным.
В стандартном List<T> например хранится int version, который инкрементится на каждое действие, изменяющее структуру листа. При создании итератора (а он является внутренним классом листа) ему в приватное поле копируется эта версия, а на MoveNext проверяется, есть ли разница между исходным version и текущим и если есть, то вызывается исключение. Реализация Dispose пустая.
А в чем неприятность Dispose в foreach? foreach явно создает новый экземпляр энумератора в начале работы, он же его и подчищает в конце, если необходимо. Если бы он этого не делал, как бы вы реализовывали подчистку после foreach? Ведь сам энумератор не доступен, это внутренняя переменная, видимая только компилятору.
IEnumerator стал Disposable после добавления yield return. Например:
Когда мы закончим итерироваться по этому генертору (причем, мы же можем закончить даже не доходя до конца), нужно закрыть someDisposable. Поэтому компилятор вычисляет такие структуры и кладет их Dispose в Dispose() метод сгенеренного IEnumerator.
using (var disposable = new SomeDisposable())
{
yield return 1;
yield return 2;
// some code
}
}
Когда мы закончим итерироваться по этому генертору (причем, мы же можем закончить даже не доходя до конца), нужно закрыть someDisposable. Поэтому компилятор вычисляет такие структуры и кладет их Dispose в Dispose() метод сгенеренного IEnumerator.
У Сергея Теплякова есть хороший пост на эту тему habrahabr.ru/post/148905/.
Что за тэги «факты, особенности, тонкости»? Вы понимаете значение словосочетания «ключевые слова»? Как по вашим тэгам можно будет что-то найти в этом кривом поисковике?
Дико извиняюсь, поправил, если что то еще нужно добавить, добавлю.
Что за тэги «факты, особенности, тонкости»?
Нужны тэги: «скандалы, интриги, расследования».
Dispose также вызывается в деструкторе, если какой-то умник забудет вызвать его «вручную».
Для всех стандартных классов, реализующих IDisposable, так лучше?
Я не спорю, что человека, который не закрывает ресурсы юзингом, нужно гнать в шею. Но упомянуть про обязательное освобождение в деструкторе, как последнем рубеже обороны, в статье про IDisposable считаю необходимым.
Во-первых, это статья — совсем не про IDIsposable. А во-вторых, не каждая реализация IDisposable нуждается в финализаторе. Например, чаще всего бывает, что вызов Dispose всего лишь делегируется другому объекту. В таком случае вызывать Dispose в финализаторе нет необходимости — все ресурсы соберет другой объект самостоятельно.
В момент вызова деструктора состояние объектов класса не определен, так что вызывать диспоз в деструкторе не всегда возможно. Так же порядок вызов деструкторов не определен. Я могу вызвать диспоз для себя но для дочерних он может быть уже вызван. Собственно поэтому связка деструктор + диспоз это целый паттерн(ы).
Но как сказали выше, статья не об этом =)
Но как сказали выше, статья не об этом =)
Зарегистрируйтесь на Хабре, чтобы оставить комментарий
Интересные моменты в C# (foreach)