Страницы

Поиск по вопросам

четверг, 11 октября 2018 г.

Опасно ли разворачивать foreach через using as

В 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 он упаковывается, что означает копирование структуры. Значит мы уничтожаем не ту же структуру, которую использовали. На этом моменте и возникает различие между приведёнными вариантами: в оригинальном структура скопирована после использования, а в модифицированном - перед
Может ли такое различие привести к опасным последствиям? Если да, то к каким?


Ответ

Фактически, ваш вопрос сводится к тому, что надо придумать непротиворечивый и не надуманный вариант реализации 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 в данном случае.

Комментариев нет:

Отправить комментарий