#c_sharp #инспекция_кода #foreach #language_lawyer #дизайн_языка
В C# цикл foreach разворачивается в нечто такое: Container container = new Container(); Enumerator enumerator = container.GetEnumerator(); try { while (enumerator.MoveNext()) { var element = enumerator.Current; // содержимое foreach } } finally { IDisposable disposable = enumerator as IDisposable; if (disposable != null) disposable.Dispose(); } Насколько безопасно и корректно изменить эту реализацию на такую: Container container = new Container(); Enumerator enumerator = container.GetEnumerator(); using (enumerator as IDisposable) { while (enumerator.MoveNext()) { var element = enumerator.Current; // содержимое foreach } } Понятно, что для reference-типов никакой разницы нет. Но если энумератор окажется value-типом, то при приведении к IDisposable он упаковывается, что означает копирование структуры. Значит мы уничтожаем не ту же структуру, которую использовали. На этом моменте и возникает различие между приведёнными вариантами: в оригинальном структура скопирована после использования, а в модифицированном - перед. Может ли такое различие привести к опасным последствиям? Если да, то к каким?
Ответы
Ответ 1
Фактически, ваш вопрос сводится к тому, что надо придумать непротиворечивый и не надуманный вариант реализации Enumerator причём такой, чтобы была реальная необходимость реализации в нём Dispose в процессе итерирования данные, которые в нём хранятся, менялись Сделав над собой героическое усилие и заставив себя оставить в тылу мысль о том, что "так" делать нельзя никогда, попробуем. Для того, чтобы обосновать необходимость реализации IDisposable можно представить себе итератор, который возвращает данные через какой-нибудь неуправляемой handle. Подойдёт, например, файл. public struct LineEnumerator : IEnumerator { private System.IO.FileStream _file; public LineEnumerator(string path) { _file = System.IO.File.OpenRead(path); } IDisposable.Dispose() { if (_file != null) { _file.Dispose(); _file = null; } } ... } После того, как мы непоправимо испортили себе карму таким итератором, выполнить второе условие очень легко. Представьте итератор, который смотрит не на файл, а на папку и по мере итерирования открывает файлы в указанной папке последовательно. Кончаются данные в одном файле, он закрывает его и открывает следующий. Таким образом, в процессе итерирования handle периодически меняется. public struct LineEnumerator : IEnumerator { private string _directoryPath; private System.IO.FileStream _file; public LineEnumerator(string directoryPath) { _directoryPath = directoryPath; } public bool MoveNext() { if (CheckEOF(_file)) { OpenNextFile(_directoryPath, ref _file); } ... } IDisposable.Dispose() { if (_file != null) { _file.Dispose(); _file = null; } } ... } Заблаговременно делать копию с такого итератора уже нельзя, если мы не хотим потерять handle на файл. Отсюда мораль: нет ни одной причины реализовывать такой нумератор как struct. Я бы даже сказал, что нет ни одного оправдания использованию struct по сравнению с class в данном случае.Ответ 2
Надо понимать, что теоретически enumerator может оказаться сопрограммой, реализующей алгоритм произвольной сложности. Поэтому копировать внутреннее состояние в общем случае нельзя.
Комментариев нет:
Отправить комментарий