Почему я не могу отказаться от подписки на событие с использованием выражения Lambda?

В этой статье указано Вы не можете отменить подписку на событие с использованием выражения лямбда.

например. вы можете подписаться следующим образом:

d.Barked += (s, e) => Console.WriteLine("Bark: {0}", e);

но вы не можете отписаться следующим образом:

d.Barked -= (s, e) => Console.WriteLine("Bark: {0}", e); 

Почему? Какая разница между этим и отпиской от делегата, например

EventHandler<string> handler = (s, e) => Console.WriteLine("Bark: {0}", e);
d.Barked += handler;

// ...

d.Barked -= handler;

Ответ 1

Все сводится к: когда два делегата считаются одинаковыми для целей делегирования сложения/вычитания. Когда вы отказываетесь от подписки, она по существу использует логику от Delegate.Remove, которая считает два делегата эквивалентными, если совпадают как теги .Target, так и .Method (по крайней мере, для простого случая делегата с единственным целевым методом; многоадресная рассылка сложнее описать). Итак: что такое .Method и .Target на лямбда (если мы компилируем его для делегата, а не для выражения)?

Компилятор действительно имеет здесь большую свободу, но происходит следующее:

  • если лямбда включает замыкание по параметру или переменной, компилятор создает метод (метод) в классе, сгенерированном компилятором, который представляет контекст захвата (который также может включать токен this); целью является ссылка на этот экземпляр capture-context (который будет определяться областью захвата).
  • если лямбда не включает замыкание по параметру или переменной, но использует состояние для каждого экземпляра через this (неявный или явный), компилятор создает метод экземпляра (метод) текущего тип; целью является текущий экземпляр (this)
  • иначе компилятор создает статический метод (метод), а целевой - нулевой (кстати, в этом случае он также включает в себя отличное поле для кэширования одного статического экземпляра делегата, поэтому в этом случае только один делегат создано на лямбда)

Однако, что он не делает, сравнивает множество лямбда с похожими лицами, чтобы уменьшить их. Итак, что я получаю, когда компилирую ваш код, статические методы два:

[CompilerGenerated]
private static void <Main>b__0(object s, string e)
{
    Console.WriteLine("Bark: {0}", e);
}

[CompilerGenerated]
private static void <Main>b__2(object s, string e)
{
    Console.WriteLine("Bark: {0}", e);
}

(Main здесь только потому, что в моей тестовой установке эти lambdas находятся внутри метода Main, но в конечном итоге компилятор может выбрать любые непроизносимые имена, которые он выбирает здесь)

Первый метод используется первой лямбда; второй метод используется второй лямбдой. Поэтому, в конечном счете, причина, по которой он не работает, заключается в том, что .Method не соответствует.

В регулярных терминах С# это будет выглядеть так:

obj.SomeEvent += MethodOne;
obj.SomeEvent -= MethodTwo;

где MethodOne и MethodTwo имеют один и тот же код внутри них; он ничего не отписывает.

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

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