Делегаты и события
Как вы думаете, где подвох в нижеприведенном коде?
button1.Click += new EventHandler(new EventHandler(OnBtnClick));
button1.Click -= new EventHandler(new EventHandler(OnBtnClick));
С подобным кодом я недавно встретился при отладке. Разумеется, он был не в таком очевидном виде. Подписка/отписка методов была завуалирована использованием разных делегатов, но с одинаковой сигнатурой и цепочкой вызовов методов, где эти делегаты и передавались, а на отладку проблемы мной было потрачено несколько часов.
Итак, вернемся к примеру. Этот код компилируется без ошибок и предупреждений. Метод, подписанный на событие, вызывается при генерации события, а вот отписка от события не работает.
Для лучшего понимания проблемы с отписыванием метода от события, приведу пару замечаний о структуре делегатов. Если упрощенно рассматривать делегат, то среди всего прочего он содержит два интересных нам свойства:
public object Target
public MethodInfo Method
button1.Click += new EventHandler(new EventHandler(OnBtnClick));
button1.Click -= new EventHandler(new EventHandler(OnBtnClick));
С подобным кодом я недавно встретился при отладке. Разумеется, он был не в таком очевидном виде. Подписка/отписка методов была завуалирована использованием разных делегатов, но с одинаковой сигнатурой и цепочкой вызовов методов, где эти делегаты и передавались, а на отладку проблемы мной было потрачено несколько часов.
Итак, вернемся к примеру. Этот код компилируется без ошибок и предупреждений. Метод, подписанный на событие, вызывается при генерации события, а вот отписка от события не работает.
Для лучшего понимания проблемы с отписыванием метода от события, приведу пару замечаний о структуре делегатов. Если упрощенно рассматривать делегат, то среди всего прочего он содержит два интересных нам свойства:
public object Target
public MethodInfo Method
Target – это ссылка на объект, которому принадлежит метод, передаваемый в конструктор делегата. Он может быть null, если подписывается статический метод.
Method – содержит ссылку на подписываемый метод. При обычной подписке метода на событие, пример которой приведен ниже,
button1.Click += new EventHandler (form.OnBtnClick);Method – содержит ссылку на подписываемый метод. При обычной подписке метода на событие, пример которой приведен ниже,
свойство Target будет содержать ссылку на form, а свойство Method – ссылку на подписываемый метод OnBtnClick.
Для случая, когда при создании делегата ему передается другой делегат (как в исходном примере), свойство Target ссылается на передаваемый делегат, а Method содержит ссылку на метод Invoke, который генерируется для каждого делегата.
При отписке делегата от события, для которого вызывается статический метод Delegate.Remove, у делегатов (уже подписанного и используемого для отписки) вызывается метод Equals, чтобы убедиться что они обертывают собой один и тот же метод одного и того же объекта. У делегатов (классы Delegate и MulticastDelegate) операторы равенства и методы Equals перегружены (их код можно посмотреть, например, в Reflector). В методе Equals сравниваются свойства Target и Method, а операторы равенства вызывают Equals. Сравнение свойства Target выполняется оператором ==, а поскольку свойство Target имеет тип object, то вызывается оператор сравнения для object. Оператор == для object в свою очередь сравнивает значения ссылок (Reference Equals), которые для нашего примера очевидно не совпадают. Делегаты, подписанный и используемый для отписки, считаются разными и, как следствие, отписка метода не происходит.
Надеюсь, эта статья поможет вам сэкономить время при отладке подобного кода и избежать таких ловушек при написании собственного.
При отписке делегата от события, для которого вызывается статический метод Delegate.Remove, у делегатов (уже подписанного и используемого для отписки) вызывается метод Equals, чтобы убедиться что они обертывают собой один и тот же метод одного и того же объекта. У делегатов (классы Delegate и MulticastDelegate) операторы равенства и методы Equals перегружены (их код можно посмотреть, например, в Reflector). В методе Equals сравниваются свойства Target и Method, а операторы равенства вызывают Equals. Сравнение свойства Target выполняется оператором ==, а поскольку свойство Target имеет тип object, то вызывается оператор сравнения для object. Оператор == для object в свою очередь сравнивает значения ссылок (Reference Equals), которые для нашего примера очевидно не совпадают. Делегаты, подписанный и используемый для отписки, считаются разными и, как следствие, отписка метода не происходит.
Надеюсь, эта статья поможет вам сэкономить время при отладке подобного кода и избежать таких ловушек при написании собственного.
Комментарии
Андрюха, кто это тебе такую засаду подложил? Хотя в том виде, как у тебя записано, довольно очевидно, что отписка не сработает. Но наверное, наличие второго делегата было хорошо замаскировано.