2013-06-26 2 views
3

TL; DR - Всегда ли целесообразно выполнять бизнес-логику в IDisposable.Dispose?Take 2: Неправильно ли использовать IDisposable и «использовать» в качестве средства для получения «видимого поведения»?

В поисках ответа я прочитал вопрос: Is it abusive to use IDisposable and "using" as a means for getting "scoped behavior" for exception safety? Он очень приблизился к решению этой проблемы, но я хотел бы напасть на него мертвым. Я недавно столкнулся с какой-то код, который выглядел так:

class Foo : IDisposable 
{ 
    public void Dispose() 
    { 
     ExecuteSomeBusinessBehavior(); 
     NormalCleanup(); 
    } 
} 

и используется в контексте таких как:

try 
{ 
    using (var myFoo = new Foo()) 
    { 
     DoStuff(); 
     foo.DoSomethingFooey(); 
     ... 
     DoSomethingElse(); 
     Etc(); 
    } 
} 
catch (Exception ex) 
{ 
    // Handle stuff 
} 

Увидев этот код, я сразу же начал чесаться. Вот что я вижу, когда смотрю на этот код:

Во-первых, глядя только на контекст использования, неясно, что фактическая бизнес-логика, а не только код очистки, будет выполняться, когда код покидает область использования.

Во-вторых, если какой-либо из кода в области «использования» генерирует исключение, бизнес-логика метода Dispose все равно будет выполняться и делает это до того, как Try/Catch сможет обработать исключение.

Мои вопросы сообществу StackOverflow следующие: Имеет ли смысл вкладывать бизнес-логику в метод IDisposable.Dispose? Есть ли образец, который дает подобные результаты, не делая меня зудом?

+3

Пожалуйста, d o не использовать Dispose для бизнес-логики. Это механизм для очистки неуправляемых ресурсов; используйте его для своей цели. –

+0

@EricLippert: будет ли 'TransactionScope' исключением, которое доказывает правило? – user7116

+0

@ user7116: Я отмечаю, что многие люди не понимают, что в этом высказывании «доказывает» означает «испытуемые». Отсюда выражение «доказательство» для диапазона испытаний оружия. Да, это исключение, которое * доказывает * - субъекты для тестирования - правило.Я считаю, что результатом этого теста является то, что использование несколько подозрительно; почему вызов «Завершить» не выполняет коммит, а «Dispose» просто закрывает соединение? Я также отмечаю, что документация противоречит самому вопросу о том, завершает ли 'Complete' или' Dispose' коммит. –

ответ

3

Реальный смысл IDisposable является то, что объект знает что-то, где-то, который был введен в состояние, которое должно быть вычищен, и он имеет информацию и стимулы, необходимые для выполнения такой очистки. Хотя наиболее распространенными «состояниями», связанными с IDisposable, являются такие вещи, как открытые файлы, неуправляемые графические объекты и т. Д., Это только примеры использования, а не определение «правильного» использования.

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

Рассмотрим, например, объект блокировки считывающего устройства с методом, который возвращает «токен» IDisposable при получении блокировки. Было бы хорошо, чтобы сказать:

using (writeToken = myLock.AcquireForWrite()) 
{ 
    ... Code to execute while holding write lock 
} 

Если бы нужно было вручную кодировать приобретение и освобождение замка без Try/уловом или попытаться /, наконец, заблокировать, исключение брошено в то время как замок был проведен бы вызвать любой код который ждал на замке, чтобы ждать вечно. Это плохо. Использование блока using, как показано выше, приведет к тому, что блокировка будет освобождена при выходе из блока, как обычно, так и через исключение. К сожалению, это может быть также будет плохой штукой.

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

using (writeToken = myLock.AcquireForWrite()) 
{ 
    ... Code to execute while holding write lock 
    writeToken.SignalSuccess(); 
} 

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

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

using (lockToken = myLock.CreateToken()) 
{ 
    lockToken.AcquireWrite(Describe how object may be invalid if this code fails"); 
    ... Code to execute while holding write lock 
    lockToken.ReleaseWrite(); 
} 

Если код выходит без вызова ReleaseWrite, других потоков, которые пытаются получить блокировку будет получать исключения, которые включают в себя указанное сообщение , Неправильная установка вручную пары AcquireWrite и ReleaseWrite оставит заблокированный объект непригодным для использования, но не оставит другого кода, ожидая его использования. Обратите внимание, что неуравновешенный AcquireRead не должен будет аннулировать объект блокировки, поскольку код внутри чтения никогда не помещает объект в недопустимое состояние.

+0

Из самого описания ['IDisposable'] (http://msdn.microsoft.com/en-us/library/system.idisposable.aspx):« Определяет метод для * выпуска выделенных ресурсов * ». Очистка блокировки является приемлемой (если довольно запутанный пример), но «очистка», которая включает в себя выполнение реальной работы, кроме освобождения ресурсов, выходит за рамки ее возможностей. – cHao

+0

Не имеет смысла объединить 'CreateToken()' и 'AcquireWrite()' в один метод? – svick

+1

@svick: IMHO, так как 'ReleaseWrite' должен быть явно заявлен, чтобы избежать освобождения блокировки, когда он должен быть недействительным, он должен быть« визуально »сбалансирован с помощью« AcquireRead », который не является частью инструкции' using'. Кроме того, даже если код получает и освобождает блокировку несколько раз, требуется только одно «использование». Фактически, даже если бы вы использовали несколько блокировок, можно было бы использовать один оператор using с одним менеджером блокировки-токена и иметь 'AcquireWrite',' ReleaseWrite' и т. Д. Принимать блокирующие объекты, которые будут получены или выпущены (сохраняя список несбалансированных приобретения, которые ... – supercat

2

Код бизнес-логики никогда не должен быть написан ни при каких обстоятельствах, чтобы устранить причину метода, вы полагаетесь на ненадежный путь. Что делать, если пользователь не вызвал ваш метод размещения? Вы пропустили вызов полной функциональности? Что делать, если возникло исключение, вызванное вызовом метода вашего метода dispose? И почему вы выполняете бизнес-операцию, когда пользователь просит распоряжаться самим объектом. Так логично, технически это не должно быть сделано.

+2

+1 - это также заслоняет логический поток для чтения кода. –

+0

Обычно, если вы идете по этому маршруту, у вас есть нормальная команда типа '.Close()', и единственное действие, которое у вас есть, '.Dispose()' do - это вызов '.Close()' после его удаления. –

5

(К сожалению, это больше комментариев, но она превышает предел длины комментарий.)

На самом деле, есть пример в рамках .NET где IDisposable используется для создания области и сделать полезным работа при утилизации: TransactionScope.

Цитирую TransactionScope.Dispose:

Вызов этого метода отмечает конец объема сделки. Если объект TransactionScope создал транзакцию, а Complete был вызван в области, объект TransactionScope пытается совершить транзакцию при вызове этого метода.

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

  • вы делаете это совершенно очевидно, что ваш объект создает область, например, назвав его FooScope вместо Foo и

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

+1

+1. Мы сделали что-то подобное для областей сеансов при обертке сторонних библиотек, которые требуют входа/выхода из системы. – TrueWill

0

Я в настоящее время чтения Introduction to Rx, по Lee Campbell, и есть глава под названием IDisposable, где он явно выступает воспользовавшись интеграции с using конструкцией, для того, чтобы «создать переходную сферу».

Некоторые ключевые цитаты из этой главы:

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

(... смотри примеры ниже ...)

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

Библиотека Rx сама использует это либеральное использование интерфейса IDisposable и представляет несколько своих собственные пользовательские реализации:

  • BooleanDisposable
  • CancellationDisposable
  • CompositeDisposable
  • ContextDisposable
  • MultipleAssignmentDisposable
  • RefCountDisposable
  • ScheduledDisposable
  • SerialDisposable
  • SingleAssignmentDisposable»

Он дает два забавных маленьких примеров, в самом деле:


Пример 1 - Timing выполнение кода. «Этот удобный небольшой класс позволяет вам создавать область и измерять время, необходимое для выполнения определенных разделов вашей базы кода».

public class TimeIt : IDisposable 
{ 
    private readonly string _name; 
    private readonly Stopwatch _watch; 

    public TimeIt(‌string name) 
    { 
     _name = name; 
     _watch = Stopwatch‌.StartNew(‌); 
    } 

    public void Dispose(‌) 
    { 
     _watch‌.Stop(‌); 
     Console‌.WriteLine(‌"{0} took {1}", _name, _watch‌.Elapsed); 
    } 
} 

using (‌new TimeIt(‌"Outer scope")) 
{ 
    using (‌new TimeIt(‌"Inner scope A")) 
    { 
     DoSomeWork(‌"A"); 
    } 

    using (‌new TimeIt(‌"Inner scope B")) 
    { 
     DoSomeWork(‌"B"); 
    } 

    Cleanup(‌); 
} 

Выход:

Inner scope A took 00:00:01.0000000 
Inner scope B took 00:00:01.5000000 
Outer scope took 00:00:02.8000000 

Пример 2 - Временное изменение цвета текста консоли

//Creates a scope for a console foreground color‌. When disposed, will return to 
// the previous Console‌.ForegroundColor 

public class ConsoleColor : IDisposable 
{ 
    private readonly System‌.ConsoleColor _previousColor; 

    public ConsoleColor(‌System‌.ConsoleColor color) 
    { 
     _previousColor = Console‌.ForegroundColor; 
     Console‌.ForegroundColor = color; 
    } 

    public void Dispose(‌) 
    { 
     Console‌.ForegroundColor = _previousColor; 
    } 
} 


Console‌.WriteLine(‌"Normal color"); 

using (‌new ConsoleColor(‌System‌.ConsoleColor‌.Red)) 
{ 
    Console‌.WriteLine(‌"Now I am Red"); 

    using (‌new ConsoleColor(‌System‌.ConsoleColor‌.Green)) 
    { 
     Console‌.WriteLine(‌"Now I am Green"); 
    } 

    Console‌.WriteLine(‌"and back to Red"); 
} 

Выход:

Normal color 
Now I am Red 
Now I am Green 
and back to Red