Как я должен обрабатывать исключения в моем методе Dispose ()? - PullRequest
15 голосов
/ 24 февраля 2010

Я хотел бы предоставить класс для управления созданием и последующим удалением временного каталога.В идеале, я бы хотел, чтобы его можно было использовать в блоке using, чтобы обеспечить повторное удаление каталога независимо от того, как мы покидаем блок:

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.Когда мы пытаемся удалить каталог, мы можем потерпеть неудачу;например, потому что у нас все еще есть открытый файл.Однако, если мы разрешим распространению исключения, оно может замаскировать исключение, которое произошло внутри блока using.В частности, если исключение произошло внутри блока using, это может быть то, что привело к тому, что нам не удалось удалить каталог, но если мы его замаскировали, мы потеряли наиболее полезную информацию для решения проблемы.

Кажется, у нас есть четыре варианта:

  1. Поймать и проглотить любое исключение при попытке удалить каталог.Мы можем не знать, что нам не удается очистить наш временный каталог.
  2. Каким-то образом определить, выполняется ли удаление как часть разматывания стека при возникновении исключения, и в этом случае либо подавить IOException, либо выдатьИсключение, которое объединяет IOException и любое другое исключение.Может даже не быть возможным.(Я думал об этом отчасти потому, что это было бы возможно с помощью менеджеров контекста Python , которые во многом похожи на IDISposable .NET, используемый с оператором использования C #.)
  3. Никогда не подавлять IOExceptionне удалось удалить каталог.Если в блоке using было сгенерировано исключение, мы его скроем, хотя есть большая вероятность, что оно имеет большее диагностическое значение, чем наше исключение IOException.
  4. Откажитесь от удаления каталога в методе Dispose.Пользователи класса должны нести ответственность за запрос на удаление каталога.Это кажется неудовлетворительным, поскольку большая часть мотивации для создания класса состояла в том, чтобы уменьшить бремя управления этим ресурсом.Может быть, есть другой способ предоставить эту функциональность, не упрощая ее?

Один из этих вариантов явно лучше?Есть ли лучший способ предоставить эту функцию в удобном для пользователя API?

Ответы [ 7 ]

7 голосов
/ 24 февраля 2010

Вместо того, чтобы думать об этом как о специальном классе, реализующем 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 голосов
/ 24 февраля 2010

В конечном счете, я бы посоветовал лучше следовать FileStream в качестве ориентира, что соответствует вариантам 3 и 4: закрыть файлы или удалить каталоги в вашем методе Dispose и разрешить любые исключения, которые происходят как часть этого действия, чтобы пузыриться (эффективно поглощая любые исключения, которые произошли внутри блока using), но позволяют вручную закрывать ресурс без необходимости использования блока использования, если пользователь компонента выберет это.

В отличие от документации MSDN FileStream, я предлагаю вам подробно документировать последствия, которые могут произойти, если пользователь выберет оператор using.

0 голосов
/ 24 февраля 2010

Я бы сказал, что исключение из деструктора для заблокированного файла сводится к использованию исключения для сообщения об ожидаемом результате - вы не должны этого делать.

Однако, если что-то случится, например, переменная равна нулю, у вас действительно может быть ошибка, и тогда исключение является полезным.

Если вы ожидаете заблокированных файлов и что-то, что вы или, возможно, ваш собеседник, можете с ними сделать, вам нужно включить этот ответ в свой класс. Если вы можете ответить, то просто сделайте это в одноразовом звонке. Если ваш абонент может ответить, предоставьте ему способ, например, например. событие TempfilesLocked.

0 голосов
/ 24 февраля 2010

Если предположить, что созданный каталог находится во системной временной папке, такой как возвращенная Path.GetTempPath, то я бы реализовал Dispose, чтобы не выдавать исключение, если удаление временного каталога завершилось неудачей.

Обновление: Я бы выбрал этот вариант, основываясь на том факте, что операция может завершиться сбоем из-за внешнего вмешательства, такого как блокировка другого процесса, а также из-за того, что каталог помещен во системный временный каталог, я не вижу преимущества в создании исключения. 1007 *

Каким будет действительный ответ на это исключение? Попытка удалить каталог снова нецелесообразна, и если причиной является блокировка другого процесса, то это то, что не находится под вашим непосредственным контролем.

0 голосов
/ 24 февраля 2010

Нельзя полагаться на то, что вы можете каким-то образом удалить свой каталог. Какой-то другой процесс / пользователь / что угодно может создать файл в нем за это время. Антивирус может быть занят проверкой файлов в нем и т. Д.

Лучшее, что вы можете сделать, это иметь не только класс временного каталога, но и класс временного файла (который должен быть создан внутри блока using вашего временного каталога. Классы временного файла должны (пытаться) удалить соответствующий файлы на Dispose. Таким образом, вы гарантируете, что была предпринята хотя бы попытка очистки.

0 голосов
/ 24 февраля 2010

Вопрос, который необходимо задать, заключается в том, может ли исключение быть полезным для вызывающей стороны. Если нет ничего разумного в использовании (вручную удалить используемый файл в каталоге?), Может быть, лучше зарегистрировать ошибку и забыть о ней.

Чтобы охватить оба случая, почему бы не иметь два конструктора (или аргумент для конструктора)?

public TemporaryDirectory()
: this( false )
{
}

public TemporaryDirectory( bool throwExceptionOnError )
{
}

Затем вы можете отослать решение пользователю класса относительно того, каким может быть соответствующее поведение.

Одной из распространенных ошибок будет каталог, который нельзя удалить, поскольку файл внутри него все еще используется: вы можете сохранить список невостребованных временных каталогов и разрешить возможность второй явной попытки удаления во время завершения работы программы (например, статический метод TemporaryDirectory.TidyUp ()). Если список проблемных каталогов не пуст, код может заставить сборщик мусора обрабатывать незакрытые потоки.

0 голосов
/ 24 февраля 2010

Чтобы использовать тип в операторе using, необходимо реализовать шаблон IDisposable .

Для создания самого каталога, используйте Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) в качестве базы и новый Guid в качестве имени.

...