Можно ли запускать события из Dispose()?

В моем текущем проекте я использую классы, которые реализуют следующий интерфейс ITransaction, показанный ниже. Это общий интерфейс для транзакции, которую можно отменить. У меня также есть класс TransactionSet, который используется для попытки нескольких транзакций или наборов транзакций и в конечном итоге может использоваться для создания дерева транзакций.

Некоторые реализации ITransaction сохраняют временные ссылки на экземпляры объектов или файлы, которые они могут использовать позже, если есть вызов Undo(). Успешная транзакция может быть позже подтверждена, после чего Undo() больше не разрешено, и, таким образом, больше нет необходимости во временных данных. В настоящее время я использую Dispose() в качестве метода подтверждения для очистки любых временных ресурсов.

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

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

public enum TransactionStatus
{
    NotRun, // the Transaction has not been run, or has been undoed back to the original state
    Successful, ///the action has been run and was successful
    Error //there was an attempt to run the action but it failed
}

/// <summary>
/// Generic transaction interface
/// </summary>
public interface ITransaction
{
    TransactionStatus Status { get; }

    /// <summary>
    /// Attempts the transaction returns true if successful, false if failed.
    /// If failed it is expected that everything will be returned to the original state.
    /// Does nothing if status is already Successful
    /// </summary>
    /// <returns></returns>
    bool Go();

    /// <summary>
    /// Reverts the transaction
    /// Only does something if status is successful.
    /// Should return status to NotRun
    /// </summary>
    void Undo();

    /// <summary>
    /// A message describing the cause of the error if Status == Error
    /// Otherwise equal String.Empty
    /// </summary>
    string ErrorMessage { get; }
}

person Eric Anastas    schedule 15.07.2010    source источник


Ответы (5)


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

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

person jrista    schedule 15.07.2010
comment
У ряда классов .net есть событие Disposing, и без него их было бы очень сложно использовать. Если объект будет содержать ссылку на IDisposable, который может использоваться или не использоваться в другом месте (например, элемент управления, содержащий BackgroundImage), можно использовать событие Disposing, чтобы позволить владельцу основного объекта очистить вложенный объект, когда это необходимо. - person supercat; 15.03.2011

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

person Rex M    schedule 15.07.2010
comment
+1. Да, Dispose() не финализатор; делай там что хочешь. - person Esteban Araya; 15.07.2010
comment
+1. Обратите внимание, что рекомендуемый шаблон реализации IDisposable содержит методы Dispose() и Dispose(bool). Когда вызывается Dispose(false), метод вызывается из финализатора, и события не могут вызываться. - person Stephen Cleary; 15.07.2010

Зная, что этот вопрос был задан 4 года назад, но я не удовлетворен ответами, я добавляю один, который сочетает в себе некоторые моменты, обсуждаемые в ответах и ​​комментариях, с дополнительными аспектами.

Завершение: Как отметил @jrista, давайте проясним, что IDisposable не имеет ничего общего с сборщиком мусора или завершением как таковым — это просто соглашение и настоятельно рекомендуемая практика. Используя шаблон удаления, вы можете вызовите метод Dispose из Finalizer (как указал @Stephen Cleary). В этом случае вы абсолютно не должны вызывать никаких событий, он также не должен обращаться к другим управляемым объектам в этом отношении.

Если оставить в стороне проблемы Dispose/Finalizer, поскольку вашим классам не нужен Finalizer, потому что они не обертывают неуправляемые ресурсы, есть дополнительные проблемы.

Утечки памяти/сопоставление времени жизни. Это часто упоминаемая проблема с событиями, которая может относиться и к реализации вашей транзакции. Когда у вас есть издатель событий, чье время жизни превышает время жизни подписчика на событие, у вас может возникнуть утечка памяти, если этот подписчик не отпишется от события, потому что издатель будет продолжать удерживать его. Если ваши транзакции достаточно долгоживущие и вы подписываете на них много недолговечных объектов, вам следует подумать о реализации dispose в этих объектах и ​​последующей отмене подписки на транзакцию. См. раздел Должен ли я всегда отключать обработчики событий в методе Dispose?

Принцип наименьшего удивления. Стоит ли «злоупотреблять» Dispose для совершения транзакции? Я бы сказал нет, хотя есть прецеденты. Возьмем Streamнапример. Обычно Stream.Dispose реализуется для вызова Flush и, таким образом, фиксации данных на базовом носителе. Однако обратите внимание, что у нас все еще есть явный метод Flush, поэтому вы должны добавить его. Я считаю, что "готовность к фиксации" нарушает принцип наименьшего удивления, явный метод Commit намного понятнее (вы все равно можете вызывать его из Dispose, если это поведение по умолчанию, которого вы хотите).

Каскады событий/недопустимые состояния объектов. Я думаю, что это самый сильный аргумент в пользу того, чтобы не создавать события в Dispose. События имеют тенденцию каскадироваться (т. е. одно событие запускает другие события и код), и если вы не будете осторожны, вы можете оказаться в ситуации, когда часть кода решит, что было бы неплохо выполнить обратный вызов удаляемого объекта. и поэтому может находиться в недопустимом состоянии. Неинтересно отлаживать, особенно если к объекту могут обращаться несколько потоков! Хотя опять же, для этого есть прецеденты, такие как Component.Disposed.

Я бы посоветовал не вызывать события из метода Dispose. Когда вы завершаете жизненный цикл издателя событий, действительно ли имеет значение, что все его подписчики соответствующим образом обновляют свое состояние? Я обнаруживаю, что в большинстве случаев я все равно избавляюсь от всего графа объектов (т.е. издатель живет дольше, чем подписчики). В некоторых ситуациях вы также можете захотеть активно подавить любые условия сбоя, возникающие во время удаления (например, при закрытии TCP-соединения).

person Johannes Rudolph    schedule 10.09.2014

Dispose следует просто убирать. Я бы реализовал методы Confirm() и Rollback(), если dispose вызывается без первого вызова любого из них, это ошибка, которая должна быть как минимум зарегистрирована.

person Loren Pechtel    schedule 15.07.2010

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

person David.Chu.ca    schedule 15.07.2010