Как мне обрабатывать исключения в моем методе Dispose()?
Я хотел бы предоставить класс для управления созданием и последующим удалением временного каталога. В идеале я бы хотел, чтобы он использовался в блоке использования, чтобы гарантировать, что каталог будет удален снова, независимо от того, как мы покинем блок:
static void DoSomethingThatNeedsATemporaryDirectory()
{
using (var tempDir = new TemporaryDirectory())
{
// Use the directory here...
File.WriteAllText(Path.Combine(tempDir.Path, "example.txt"), "foo\nbar\nbaz\n");
// ...
if (SomeCondition)
{
return;
}
if (SomethingIsWrong)
{
throw new Exception("This is an example of something going wrong.");
}
}
// Regardless of whether we leave the using block via the return,
// by throwing and exception or just normally dropping out the end,
// the directory gets deleted by TemporaryDirectory.Dispose.
}
Создание каталога без проблем. Проблема заключается в том, как написать метод Dispose. Когда мы пытаемся удалить каталог, мы можем выйти из строя; например, потому что у нас все еще есть файл, открытый в нем. Однако, если мы разрешаем распространение исключения, оно может маскировать исключение, которое произошло внутри блока использования. В частности, если в блоке использования произошло исключение, это может привести к тому, что мы не сможем удалить каталог, но если мы его замаскируем, мы потеряли самую полезную информацию для устранения проблемы.
Кажется, у нас есть четыре варианта:
- Попытайтесь и проглатывайте любое исключение при попытке удалить каталог. Нам может быть не известно, что мы не можем очистить наш временный каталог.
- Как-то определить, выполняется ли Dispose как часть раскрутки стека, когда было выбрано исключение, и если да, то либо подавляющее исключение IOException, либо генерирует исключение, которое объединяет исключение IOException и любое другое исключение. Возможно, даже не возможно. (Я думал об этом отчасти потому, что это было бы возможно с Python контекстными менеджерами, которые во многом похожи на .NET IDisposable, используемые с С#, используя утверждение.)
- Никогда не подавляйте IOException из-за невозможности удалить каталог. Если в блоке использования было выбрано исключение, мы скроем его, несмотря на то, что есть хороший шанс, что он имеет большую диагностическую ценность, чем наше исключение IOException.
- Отказаться от удаления каталога в методе Dispose. Пользователи класса должны нести ответственность за запрос удаления каталога. Это кажется неудовлетворительным, так как значительная часть мотивации для создания класса заключалась в уменьшении бремени управления этим ресурсом. Может быть, есть еще один способ обеспечить эту функциональность, не сделав ее очень легко испортить?
Является ли один из этих вариантов наилучшим? Есть ли лучший способ предоставить эту функциональность в удобном для пользователя API?
Ответы
Ответ 1
Вместо того, чтобы думать об этом как о специальном классе, реализующем IDisposable
, подумайте о том, как это будет выглядеть с точки зрения нормального потока программы:
Directory dir = Directory.CreateDirectory(path);
try
{
string fileName = Path.Combine(path, "data.txt");
File.WriteAllText(fileName, myData);
UploadFile(fileName);
File.Delete(fileName);
}
finally
{
Directory.Delete(dir);
}
Как это должно вести себя? Это тот же самый вопрос. Вы оставляете содержимое блока finally
как есть, тем самым потенциально маскируя исключение, которое происходит в блоке try
, или вы обертываете Directory.Delete
в свой собственный блок try-catch
, проглатывая любое исключение в порядке для предотвращения маскировки оригинала?
Я не думаю, что есть правильный ответ. Дело в том, что у вас может быть только одно эмбиентное исключение, поэтому вам нужно выбрать его. Однако .NET Framework устанавливает некоторые прецеденты; одним из примеров является прокси-сервер службы WCF (ICommunicationObject
). Если вы попытаетесь выполнить Dispose
поврежденный канал, он выдает исключение и замаскирует любое исключение, которое уже находится в стеке. Если я не ошибаюсь, TransactionScope
тоже может это сделать.
Конечно, такое поведение в WCF было бесконечным источником путаницы; большинство людей на самом деле считают это очень раздражающим, если не сломано. Google "WCF dispose mask", и вы поймете, что я имею в виду. Поэтому, возможно, мы не должны всегда пытаться делать то же, что и Microsoft.
Лично я считаю, что Dispose
никогда не должен маскировать исключение уже в стеке. Оператор using
фактически является блоком finally
и большую часть времени (всегда есть случаи с краем), вы не хотели бы бросать (а не извлекать) исключения в блок finally
. Причина - просто отладка; это может быть чрезвычайно сложно разобраться в проблеме - особенно проблема в производстве, где вы не можете пройти через источник - когда у вас даже нет возможности узнать, где именно работает приложение. Раньше я был в этом положении, и я могу с уверенностью сказать, что он полностью вас и полностью безумный.
Моя рекомендация заключалась бы в том, чтобы либо есть исключение в Dispose
(конечно, это журнал), либо на самом деле проверьте, не ли вы уже в сценарий раскрутки стека из-за исключения, и есть только последующие исключения, если вы знаете, что будете маскировать их. Преимущество последнего заключается в том, что вы не едите исключений, если вам действительно не нужно; Недостаток заключается в том, что вы внесли в свою программу некоторое недетерминированное поведение. Еще один компромисс.
Большинство людей, вероятно, просто перейдут с прежним вариантом и просто скроют любое исключение, происходящее в finally
(или using
).
Ответ 2
В конечном счете, я бы предложил лучше следовать FileStream
в качестве ориентира, который соответствует вариантам 3 и 4: закрыть файлы или удалить каталоги в вашем методе Dispose
и разрешить любые исключения, которые происходят как часть этого действия, чтобы пузыриться (эффективно проглатывание любых исключений, которые произошли внутри блока using
), но позволяют вручную закрыть ресурс без необходимости использования блок, который должен выбрать пользователь компонента.
В отличие от документации MSDN FileStream
, я предлагаю вам серьезно документировать последствия, которые могут возникнуть, если пользователь решил пойти с инструкцией using
.
Ответ 3
Вопрос, задаваемый здесь, заключается в том, может ли исключение быть полезным обработчиком. Если нет ничего разумного в использовании (вручную удалите используемый файл в каталоге?), Возможно, лучше зарегистрировать ошибку и забыть об этом.
Чтобы охватить оба случая, почему бы не создать два конструктора (или аргумент конструктору)?
public TemporaryDirectory()
: this( false )
{
}
public TemporaryDirectory( bool throwExceptionOnError )
{
}
Затем вы можете отодвинуть решение от пользователя класса относительно того, каким может быть соответствующее поведение.
Одной из распространенных ошибок будет каталог, который не может быть удален, поскольку файл внутри него все еще используется: вы можете сохранить список восстановленных временных каталогов и разрешить возможность второй явной попытки удаления во время выключения программы (например. статический метод TemporaryDirectory.TidyUp()). Если список проблемных каталогов не пуст, код может заставить сборку мусора обрабатывать незакрытые потоки.
Ответ 4
Вы не можете полагаться на предположение, что вы можете как-то удалить свой каталог. Некоторые другие процессы/пользователь/что-то еще могут создать файл в нем. Антивирус может быть занят проверкой файлов в нем и т.д.
Лучшее, что вы можете сделать, это не только временный класс каталога, но и временный класс файлов (который должен быть создан внутри блока using
вашего временного каталога. Временные классы файлов должны (пытаться) удалить соответствующие файлов на Dispose
. Таким образом, вы гарантируете, что по крайней мере была предпринята попытка очистки.
Ответ 5
Предполагая, что созданный каталог находится в системной временной папке, подобной той, которая была возвращена Path.GetTempPath
, тогда я бы выполнил Dispose
, чтобы не генерировать исключение, если удаление временного каталога не выполняется.
Update:
Я бы воспользовался этой опцией, основываясь на том, что операция может завершиться неудачей из-за внешних помех, таких как блокировка из другого процесса, а также потому, что каталог помещен в системный временный каталог. Я бы не видел преимущества бросания исключения.
Каким будет действительный ответ на это исключение? Попытка удалить каталог снова не является разумной, и если причиной является блокировка от другого процесса, то это то, что оно не находится под вашим контролем.
Ответ 6
Я бы сказал, что исключение из деструктора для заблокированного файла сводится к использованию исключения, чтобы сообщить ожидаемый результат - вы не должны этого делать.
Однако, если что-то еще происходит, например. переменная имеет значение NULL, вы можете действительно иметь ошибку, а затем исключение является ценным.
Если вы ожидаете заблокированные файлы и что-то, что вы или потенциально ваш вызывающий, можете сделать с ними, тогда вам нужно включить этот ответ в свой класс. Если вы можете ответить, просто сделайте это в одноразовом звонке. Если ваш вызывающий абонент может ответить, предоставьте вашему абоненту способ сделать это, например. событие TempfilesLocked.
Ответ 7
Чтобы использовать тип в инструкции using
, вы хотите реализовать шаблон IDisposable.
Для создания самой директории используйте Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)
как базу и новый Guid в качестве имени.