Страницы

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

суббота, 7 декабря 2019 г.

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

#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 может оказаться сопрограммой, реализующей алгоритм произвольной сложности. Поэтому копировать внутреннее состояние в общем случае нельзя.

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

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