Оскорбительно ли использовать IDisposable и "использование" в качестве средства для получения "видимого поведения" для безопасности исключений?

Что-то, что я часто использовал в С++, позволяло классу A обрабатывать запись состояния и условие завершения для другого класса B, через конструктор и деструктор A, чтобы убедиться, что если что-то в этой области исключение, тогда B будет иметь известное состояние, когда область действия была удалена. Это не чисто RAII, насколько акроним идет, но тем не менее установленный шаблон.

В С# я часто хочу сделать

class FrobbleManager
{
    ...

    private void FiddleTheFrobble()
    {
        this.Frobble.Unlock();
        Foo();                  // Can throw
        this.Frobble.Fiddle();  // Can throw
        Bar();                  // Can throw
        this.Frobble.Lock();
    }
}

Что нужно сделать, как это сделать

private void FiddleTheFrobble()
{
    this.Frobble.Unlock();

    try
    {            
        Foo();                  // Can throw
        this.Frobble.Fiddle();  // Can throw
        Bar();                  // Can throw
    }
    finally
    {
        this.Frobble.Lock();
    }
}

если я хочу гарантировать состояние Frobble, когда возвращается FiddleTheFrobble. Код будет приятнее с

private void FiddleTheFrobble()
{
    using (var janitor = new FrobbleJanitor(this.Frobble))
    {            
        Foo();                  // Can throw
        this.Frobble.Fiddle();  // Can throw
        Bar();                  // Can throw
    }
}

где FrobbleJanitor выглядит грубо, например

class FrobbleJanitor : IDisposable
{
    private Frobble frobble;

    public FrobbleJanitor(Frobble frobble)
    {
        this.frobble = frobble;
        this.frobble.Unlock();
    }

    public void Dispose()
    {
        this.frobble.Lock();
    }
}

И как я хочу это делать. Теперь реальность догоняет, так как для требуется требуется, чтобы FrobbleJanitor использовался с using. Я мог бы рассмотреть эту проблему с обзором кода, но что-то меня злит.

Вопрос:. Было ли рассмотрено выше оскорбительное использование using и IDisposable?

Ответ 1

Я так не думаю, обязательно. IDisposable technically is предназначен для использования для вещей, которые имеют не управляемые ресурсы, но тогда директива using - это просто опрятный способ реализации общего шаблона try .. finally { dispose }.

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

Вероятно, вы должны использовать другой интерфейс поверх IDisposable, чтобы немного оттолкнуть его, объясняя другим разработчикам, почему этот интерфейс подразумевает IDisposable.

Есть много других альтернатив для этого, но, в конечном счете, я не могу придумать никого, что будет таким же аккуратным, как это, так что идите!

Ответ 2

Я считаю, что это злоупотребление оператором using. Я знаю, что я нахожусь в меньшинстве на этой должности.

Я считаю, что это злоупотребление по трем причинам.

Во-первых, потому что я ожидаю, что "использование" используется для использования ресурса и избавления от него, когда вы закончите с ним. Изменение состояния программы не использует ресурс, а его изменение не утилизируется. Поэтому "использование" для изменения и восстановления состояния является злоупотреблением; код вводит в заблуждение случайному читателю.

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

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

Вы должны были довести это до проверки кода в моем офисе, первый вопрос, который я задал бы: "Действительно ли правильно блокировать frobble, если возникает исключение?" С вашей программы совершенно очевидно, что эта вещь агрессивно блокирует фроббл независимо от того, что происходит. Это правильно? Выброшено исключение. Программа находится в неизвестном состоянии. Мы не знаем, бросили ли Foo, Fiddle или Bar, почему они бросили или какие мутации они выполнили в другое состояние, которое не было очищено. Можете ли вы убедить меня в том, что в этой ужасной ситуации всегда нужно делать повторную блокировку?

Возможно, это так, может быть, нет. Я хочу сказать, что с кодом, который был изначально написан, обозреватель кода знает, чтобы задать вопрос. С кодом, который использует "использование" , я не знаю, чтобы задать вопрос; Я предполагаю, что блок "использования" выделяет ресурс, использует его немного и вежливо распоряжается им, когда это делается, а не то, что закрывающая скобка блока "using" меняет состояние моей программы в исключительном циркульстве, когда сколь угодно много были нарушены условия согласованности состояния программы.

Использование блока "using" для получения семантического эффекта делает этот фрагмент программы:

}

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

Во-вторых, я бы спросил, видел ли я ваш исходный код: "что произойдет, если исключение будет отправлено после разблокировки, но до того, как будет введена попытка?" Если вы используете неоптимизированную сборку, компилятор, возможно, вставил инструкцию no-op перед попыткой, и исключение исключения прерывания может произойти в no-op. Это редко, но это происходит в реальной жизни, особенно на веб-серверах. В этом случае разблокировка происходит, но блокировка никогда не происходит, потому что исключение было выбрано перед попыткой. Вполне возможно, что этот код уязвим для этой проблемы и на самом деле должен быть написан

bool needsLock = false;
try
{
    // must be carefully written so that needsLock is set
    // if and only if the unlock happened:

    this.Frobble.AtomicUnlock(ref needsLock);
    blah blah blah
}
finally
{
    if (needsLock) this.Frobble.Lock();
}

Опять же, может, и так, может быть, нет, но я знаю, чтобы задать вопрос. С помощью "используемой" версии она подвержена одной и той же проблеме: исключение прерывания потока может быть выбрано после блокировки Frobble, но до того, как будет введена защищенная область, связанная с использованием. Но с использованием "использующей" версии я предполагаю, что это "так что?". ситуация. Это печально, если это происходит, но я полагаю, что "использование" доступно только для того, чтобы быть вежливым, а не мутировать жизненно важное состояние программы. Я предполагаю, что если какое-то ужасное прерывание прерывания потока происходит в самое неподходящее время, тогда, ну, сборщик мусора очистит этот ресурс, в конечном счете, запустив финализатор.

Ответ 3

Если вам нужен только чистый, ограниченный код, вы также можете использовать lambdas, la

myFribble.SafeExecute(() =>
    {
        myFribble.DangerDanger();
        myFribble.LiveOnTheEdge();
    });

где метод .SafeExecute(Action fribbleAction) обертывает блок try - catch - finally.

Ответ 4

Эрик Гуннерсон, который был в команде разработчиков языка С#, дал этот ответ в почти том же вопросе:

Даг спрашивает:

  

re: инструкция блокировки с таймаутом...

         

Я сделал этот трюк, прежде чем разбираться с общими шаблонами по многочисленным методам. Обычно обнаружение блокировки, но есть некоторые другие. Проблема заключается в том, что он всегда чувствует себя взломанным, поскольку объект на самом деле не одноразовый, а " call-back-at-the-end-of-the-scope -able".

  

Дуга,

Когда мы решили [sic] использовать инструкцию, мы решили назвать ее "использованием", а не чем-то более конкретным для размещения объектов, чтобы он мог использоваться именно для этого сценария.

Ответ 5

Это скользкий склон. IDisposable имеет контракт, который подкрепляется финализатором. Финализатор бесполезен в вашем случае. Вы не можете заставить клиента использовать инструкцию using, только поощряйте его делать это. Вы можете заставить его использовать такой метод:

void UseMeUnlocked(Action callback) {
  Unlock();
  try {
    callback();
  }
  finally {
    Lock();
  }
}

Но это имеет тенденцию становиться немного неудобным без ламд. Тем не менее, я использовал IDisposable, как и вы.

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

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

Последние два требуют, чтобы вызывающий ящик явно написал блок try. Теперь использование инструкции мешает. Это может вызвать побуждение клиента к коме, которая заставляет его поверить, что ваш класс заботится о состоянии и не требует дополнительной работы. Это почти никогда не бывает точным.

Ответ 6

Пример реального мира - это BeginForm ASP.net MVC. В принципе вы можете написать:

Html.BeginForm(...);
Html.TextBox(...);
Html.EndForm();

или

using(Html.BeginForm(...)){
    Html.TextBox(...);
}

Html.EndForm вызывает Dispose и Dispose просто выдает тег </form>. Хорошая вещь об этом заключается в том, что скобки {} создают видимую "область", которая упрощает просмотр того, что внутри формы, а что нет.

Я бы не злоупотреблял этим, но, по существу, IDisposable - это просто способ сказать: "У вас есть вызов этой функции, когда вы закончите с ней". MvcForm использует его, чтобы убедиться, что форма закрыта, Stream использует ее, чтобы убедиться, что поток закрыт, вы можете использовать его, чтобы убедиться, что объект разблокирован.

Лично я бы использовал его только в том случае, если следующие два правила верны, но thet задаются мной произвольно:

  • Dispose должна быть функцией, которая всегда должна выполняться, поэтому не должно быть никаких условий, кроме Null-Checks
  • После Dispose() объект не должен быть повторно использован. Если бы я хотел использовать объект многократного использования, я бы скорее дал ему методы open/close, а не dispose. Поэтому я бросаю InvalidOperationException при попытке использовать расположенный объект.

В конце концов, все дело в ожиданиях. Если объект реализует IDisposable, я предполагаю, что ему нужно выполнить некоторую очистку, поэтому я его вызываю. Я думаю, что обычно это имеет функцию "Shutdown".

При этом мне не нравится эта строка:

this.Frobble.Fiddle();

Поскольку FrobbleJanitor "владеет" Frobble теперь, мне интересно, не лучше ли вместо этого называть Fiddle на Frobble в "Дворнике"?

Ответ 7

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

Ответ 8

Включение в это: я согласен с большинством здесь, что это хрупкий, но полезный. Я хотел бы указать вам на класс System.Transaction.TransactionScope, который делает что-то вроде того, что вы хотите сделать.

Вообще мне нравится синтаксис, он удаляет много беспорядка из настоящего мяса. Пожалуйста, подумайте о том, чтобы дать вспомогательному классу хорошее имя, хотя, может быть... Сфера применения, как пример выше. Имя должно отдать, что оно инкапсулирует фрагмент кода. * Область, * Блок или что-то подобное должно это делать.

Ответ 9

Примечание. Моя точка зрения, вероятно, смещена от моего фона на С++, поэтому мое значение ответа должно оцениваться с учетом этого возможного смещения...

Что говорит спецификация языка С#?

Цитирование Спецификация языка С#:

8.13 Оператор using

[...]

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

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

Итак, я думаю, это нормально, потому что Lock - это ресурс.

Возможно, ключевое слово using было выбрано плохо. Возможно, его следовало называть scoped.

Тогда мы можем рассматривать почти что-либо как ресурс. Ручка файла. Сетевое соединение... Нить?

Нить

Использование (или злоупотребление) ключевого слова using?

Будет ли блестящий (ab) использовать ключевое слово using, чтобы убедиться, что работа потока завершена до выхода из области?

Трава Саттер, кажется, думает, что это блестящий, так как он предлагает интересное использование шаблона IDispose для ожидания завершения работы потока:

http://www.drdobbs.com/go-parallel/article/showArticle.jhtml?articleID=225700095

Вот код, скопированный из статьи:

// C# example
using( Active a = new Active() ) {    // creates private thread
       …
       a.SomeWork();                  // enqueues work
       …
       a.MoreWork();                   // enqueues work
       …
} // waits for work to complete and joins with private thread

Пока код С# для объекта Active не указан, на С# используется шаблон IDispose для кода, версия С++ которого содержится в деструкторе. И, посмотрев версию С++, мы видим деструктор, ожидающий завершения внутреннего потока до выхода, как показано в этом другом выписке статьи:

~Active() {
    // etc.
    thd->join();
 }

Итак, что касается Herb, это блестящий.

Ответ 10

Я считаю, что ответ на ваш вопрос - нет, это не было бы злоупотреблением IDisposable.

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

Поскольку вы создаете новый FrobbleJanitor явным образом каждый раз, когда вы попадаете в оператор using, вы никогда не используете один и тот же объект FrobbeJanitor дважды. И поскольку целью является управление другим объектом, Dispose представляется подходящим для задачи освобождения этого ( "управляемого" ) ресурса.

(Btw. стандартный примерный код, демонстрирующий правильную реализацию Dispose, почти всегда предполагает, что управляемые ресурсы также должны быть освобождены, а не только неуправляемые ресурсы, такие как дескрипторы файловой системы.)

Единственное, о чем я лично беспокоюсь, это то, что он менее ясен, что происходит с using (var janitor = new FrobbleJanitor()), чем с более явным блоком try..finally, где операции Lock и Unlock видны напрямую. Но какой подход взят, вероятно, сводится к личным предпочтениям.

Ответ 11

Это не оскорбительно. Вы используете для них то, для чего они созданы. Но вам, возможно, придется рассматривать один на другой в соответствии с вашими потребностями. Например, если вы выбираете "артистизм", вы можете использовать "использование", но если ваш фрагмент кода выполняется много раз, то по соображениям производительности вы можете использовать конструкцию "try".. finally. Поскольку "использование" обычно включает в себя создание объекта.

Ответ 12

Я думаю, вы сделали все правильно. Перегрузка Dispose() была бы проблемой, которую тот же самый класс позже очистил, на самом деле он должен был сделать, и время жизни этой очистки изменилось, чтобы быть другим, а затем время, когда вы ожидаете блокировки. Но поскольку вы создали отдельный класс (FrobbleJanitor), который отвечает только за блокировку и разблокировку Frobble, все достаточно развязано, вы не столкнетесь с этой проблемой.

Я бы переименовал FrobbleJanitor, хотя, возможно, что-то вроде FrobbleLockSession.