Я хотел бы обеспечить класс для управления созданием и последующим удалением временного каталога. Идеально, я хотел бы, чтобы это было применимо в блоке использования, чтобы гарантировать, что каталог удален снова независимо от того, как мы оставляем блок:
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.
}
Создание каталога не является никакой проблемой. Проблема состоит в том, как записать Расположить метод. Когда мы пытаемся удалить каталог, мы могли бы перестать работать; например, потому что у нас все еще есть файл, открытый в нем. Однако, если мы позволяем исключению распространять, оно могло бы замаскировать исключение, которое произошло в блоке использования. В частности, если бы исключение произошло в блоке использования, то это могло бы быть то, которое заставило нас не мочь удалить каталог, но если мы маскируем его, мы потеряли наиболее полезную информацию для решения проблемы.
Кажется, что у нас есть четыре опции:
Одна из этих опций является ясно лучшей? Существует ли лучший способ обеспечить эту функциональность в удобном для пользователя API?
Вместо того, чтобы думать об этом как о специальном классе, реализующем 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
). Если вы попытаетесь удалить
канал, в котором произошел сбой, это вызовет исключение, а замаскирует любое исключение, которое уже находится в стеке. Если я не ошибаюсь, TransactionScope
тоже может это делать.
Конечно, именно такое поведение WCF было нескончаемым источником путаницы; большинство людей считают его очень раздражающим, если не сломанным. Погуглите "WCF dispose mask", и вы поймете, что я имею в виду. Так что, возможно, нам не всегда следует пытаться делать что-то так же, как это делает Microsoft.
Лично я считаю, что Dispose
никогда не должен маскировать исключение, уже находящееся в стеке. Оператор using
фактически является блоком finally
и большую часть времени (всегда есть крайние случаи), вы не хотели бы бросать (и не ловить) исключения в блоке finally
. Причина просто в отладке; может быть чрезвычайно трудно разобраться в сути проблемы - особенно проблема в производственной среде, когда вы не можете пройти через источник - когда у вас даже нет возможности узнать, где именно приложение не работает. Я уже бывал в таком положении раньше и могу с уверенностью сказать, что это полностью сведет вас с ума.
Я бы порекомендовал либо исключить исключение в Dispose
(конечно, зарегистрировать его), либо проверить, не находитесь ли вы уже в сценарии раскрутки стека из-за исключение и использовать последующие исключения только в том случае, если вы знаете, что вы их замаскируете. Преимущество последнего состоит в том, что вы не едите исключения, если вам действительно не нужно; недостаток в том, что вы ввели в свою программу недетерминированное поведение. Еще один компромисс.
Большинство людей, вероятно, просто выберут первый вариант и просто скроют любое исключение, возникающее в , наконец,
(или , используя
).
В конечном итоге, я бы предположил, что лучше всего следовать FileStream
в качестве руководства, что соответствует вариантам 3 и 4: закрывать файлы или удалять каталоги в методе Dispose
, и позволять любым исключениям, возникающим как часть этого действия, всплывать (эффективно поглощая любые исключения, возникающие внутри блока using
), но позволять закрывать ресурс вручную без блока using, если пользователь компонента так решит.
В отличие от документации MSDN по FileStream
, я предлагаю вам тщательно документировать последствия, которые могут произойти, если пользователь решит использовать using
.
Здесь следует задать вопрос, может ли вызывающая сторона с пользой обработать исключение. Если пользователь не может ничего разумно сделать (вручную удалить используемый файл из каталога?), возможно, лучше записать ошибку в журнал и забыть о ней.
Чтобы охватить оба случая, почему бы не иметь два конструктора (или аргумент к конструктору)?
public TemporaryDirectory()
: this( false )
{
}
public TemporaryDirectory( bool throwExceptionOnError )
{
}
Тогда вы можете переложить решение о том, каким должно быть поведение класса, на пользователя.
Одной из распространенных ошибок будет каталог, который не может быть удален, потому что файл внутри него все еще используется: вы можете хранить список не удаленных временных каталогов и разрешить возможность второй явной попытки удаления во время завершения работы программы (например, статический метод TemporaryDirectory.TidyUp()). Если список проблемных каталогов не является пустым, код может принудительно собирать мусор для обработки незакрытых потоков.
Вы не можете полагаться на предположение, что вы можете каким-то образом удалить свой каталог. Какой-то другой процесс / пользователь / кто угодно может тем временем создать в нем файл. Антивирус может быть занят проверкой файлов в нем и т. Д.
Лучшее, что вы можете сделать, - это иметь не только класс временного каталога, но и класс временного файла (который должен быть создан внутри с использованием блока
ваш временный каталог. Классы временных файлов должны (попытаться) удалить соответствующие файлы на Dispose
. Таким образом вы гарантируете, что, по крайней мере, была сделана попытка очистки.
Я бы сказал, что выдача исключения из деструктора для заблокированного файла сводится к использованию исключения для сообщения ожидаемого результата - вы не должны этого делать.
Однако, если произойдет что-то еще, например переменная имеет значение NULL, у вас действительно может быть ошибка, и тогда исключение имеет значение.
Если вы ожидаете, что файлы заблокированы, и есть что-то, что вы или, возможно, вызывающий абонент, можете с ними сделать, тогда вам необходимо включить этот ответ в свой класс. Если можете ответить, то просто сделайте это в одноразовом вызове. Если ваш вызывающий абонент может ответить, предоставьте ему способ, например, событие TempfilesLocked.
Предполагая, что созданный каталог находится во временной папке системы, подобной той, которую возвращает Path.GetTempPath
, я бы реализовал Удалите
, чтобы не генерировать исключение в случае сбоя удаления временного каталога.
Обновление: Я бы выбрал этот вариант, потому что операция может завершиться неудачно из-за внешнего вмешательства, например, блокировки от другого процесса, а также из-за того, что каталог временно помещен в систему каталог, я бы не увидел преимущества в генерации исключения.
Что было бы правильным ответом на это исключение? Пытаться удалить каталог снова неразумно, и если причина в блокировке другого процесса, это то, что он не находится под вашим контролем.
Чтобы использовать тип в с использованием оператора
, вы хотите реализовать шаблон IDisposable .
Для создания самого каталога используйте Environment.GetFolderPath (Environment.SpecialFolder.LocalApplicationData)
в качестве основы и новый Guid в качестве имени.