Делегаты и события

Как вы думаете, где подвох в нижеприведенном коде?

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);

свойство 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), которые для нашего примера очевидно не совпадают. Делегаты, подписанный и используемый для отписки, считаются разными и, как следствие, отписка метода не происходит.

Надеюсь, эта статья поможет вам сэкономить время при отладке подобного кода и избежать таких ловушек при написании собственного.

Комментарии

Анонимный написал(а)…
А в чем смысл обертывания делегатов в делегаты? Можно привести пример из жизни?
Sergey Rozovik написал(а)…
Ужос!
Андрюха, кто это тебе такую засаду подложил? Хотя в том виде, как у тебя записано, довольно очевидно, что отписка не сработает. Но наверное, наличие второго делегата было хорошо замаскировано.
Анонимный написал(а)…
видимо, кто-то переработался :) а ошибка получилась коварная!
Андрей Бороздин написал(а)…
Известно кто -- индусы :). Так что пример вполне реальный. Просто, как я уже написал в статье, я его существенно упростил, но суть проблемы осталась.

Популярные сообщения из этого блога

Команды docker save/load, docker export/import – в чем отличие, как и когда ими пользоваться

Как узнать, кто заблокировал файл или папку

Эрик Гамма переходит на работу в Майкрософт