В 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 в данном случае.
Комментариев нет:
Отправить комментарий