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


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

в C#, я часто хочу сделать

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?

12 103

12 ответов:

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

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

есть много других альтернатив, но, в конечном счете, я не знаю, что будет так аккуратно, как это, так что дерзайте!

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

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

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

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

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

вы должны были привести это к обзору кода в моем офисе, первый вопрос, который я бы задал: "действительно ли правильно блокировать frobble, если возникает исключение?- Из вашей программы совершенно очевидно, что эта штука агрессивно блокирует фроббл, что бы ни случилось. это верно? было выдано исключение. Программа находится в неизвестном состоянии. Мы не знаем, бросали Ли Фу, скрипку или бар, почему они бросали, или какие мутации они выполняли в другое состояние, которое не было очищено. Можете ли вы убедить мне, что в той страшной ситуации всегда правильно для повторной блокировки?

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

использование блока" 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();
}

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

если вы просто хотите, чтобы некоторые чистые, код области, вы можете также использовать лямбда-выражения, а-ля

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

здесь .SafeExecute(Action fribbleAction) метод обертывания try -catch -finally блок.

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

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

re: оператор блокировки с таймаутом...

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

Даг,

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

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

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

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

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

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

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

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

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

или

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

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

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

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

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

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

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

this.Frobble.Fiddle();

как FrobbleJanitor "владеет" Frobble теперь, я интересно, если это не было бы лучше вместо скрипки он Frobble в дворника?

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

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

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

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

цитирую Спецификация Языка C#:

8.13 оператор using

[...]

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

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

Так Что Я думаю, это нормально, потому что блокировка является ресурсом.

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

тогда мы можем рассмотреть почти все, как ресурс. Дескриптор файла. Сетевое подключение... Нить?

какой-нить???

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

будет блестящий to (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

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

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

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

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

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

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

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

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

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

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

Я бы переименовал FrobbleJanitor, хотя, вероятно, что-то вроде Фроббллоксессия.