Отписаться анонимный метод в C#
Можно ли отписаться от анонимного метода от события?
Если я подпишусь на такое событие:
void MyMethod()
{
Console.WriteLine("I did it!");
}
MyEvent += MyMethod;
Я могу отменить подписку следующим образом:
MyEvent -= MyMethod;
Но если я подпишусь, используя анонимный метод:
MyEvent += delegate(){Console.WriteLine("I did it!");};
Можно ли отписаться от этого анонимного метода? Если так, то как?
14 ответов
Action myDelegate = delegate(){Console.WriteLine("I did it!");};
MyEvent += myDelegate;
// .... later
MyEvent -= myDelegate;
Просто держите ссылку на делегата.
Одним из методов является объявление переменной для хранения анонимного метода, который затем будет доступен внутри самого анонимного метода. Это сработало для меня, потому что желаемым поведением было отписаться после обработки события.
Пример:
MyEventHandler foo = null;
foo = delegate(object s, MyEventArgs ev)
{
Console.WriteLine("I did it!");
MyEvent -= foo;
};
MyEvent += foo;
С тех пор, как была выпущена функция локальных функций C# 7.0, подход, предложенный J c, становится действительно изящным.
void foo(object s, MyEventArgs ev)
{
Console.WriteLine("I did it!");
MyEvent -= foo;
};
MyEvent += foo;
Так что, честно говоря, у вас нет анонимной функции в качестве переменной здесь. Но я полагаю, что мотивация использовать его в вашем случае может быть применена к локальным функциям.
Исходя из памяти, спецификация явно не гарантирует поведения в любом случае, когда дело доходит до эквивалентности делегатов, созданных анонимными методами.
Если вам нужно отменить подписку, вы должны либо использовать "обычный" метод, либо сохранить делегат где-то еще, чтобы вы могли отписаться точно с тем же делегатом, который вы использовали для подписки.
В 3.0 можно сократить до:
MyHandler myDelegate = ()=>Console.WriteLine("I did it!");
MyEvent += myDelegate;
...
MyEvent -= myDelegate;
Вместо того, чтобы хранить ссылку на любой делегат, вы можете использовать свой класс для того, чтобы вернуть список вызовов события вызывающей стороне. По сути, вы можете написать что-то вроде этого (при условии, что MyEvent объявлен внутри MyClass):
public class MyClass
{
public event EventHandler MyEvent;
public IEnumerable<EventHandler> GetMyEventHandlers()
{
return from d in MyEvent.GetInvocationList()
select (EventHandler)d;
}
}
Таким образом, вы можете получить доступ ко всему списку вызовов извне MyClass и отписаться от любого обработчика, который вы хотите. Например:
myClass.MyEvent -= myClass.GetMyEventHandlers().Last();
Я написал полный пост об этой технике здесь.
Вид отстойного подхода:
public class SomeClass
{
private readonly IList<Action> _eventList = new List<Action>();
...
public event Action OnDoSomething
{
add {
_eventList.Add(value);
}
remove {
_eventList.Remove(value);
}
}
}
- Переопределить событие добавить / удалить методы.
- Держите список тех обработчиков событий.
- При необходимости очистите их все и добавьте остальные.
Это может не сработать или быть наиболее эффективным методом, но должно выполнить работу.
Если вы хотите контролировать отмену подписки, вам нужно пройти по маршруту, указанному в вашем принятом ответе. Однако, если вы просто заинтересованы в очистке ссылок, когда ваш подписывающийся класс выходит из области видимости, то есть другое (слегка запутанное) решение, которое включает использование слабых ссылок. Я только что опубликовал вопрос и ответ по этой теме.
Одно простое решение:
просто передайте переменную EventHandle в качестве параметра для себя. Событие, если у вас есть случай, когда вы не можете получить доступ к первоначально созданной переменной из-за многопоточности, вы можете использовать это:
MyEventHandler foo = null;
foo = (s, ev, mehi) => MyMethod(s, ev, foo);
MyEvent += foo;
void MyMethod(object s, MyEventArgs ev, MyEventHandler myEventHandlerInstance)
{
MyEvent -= myEventHandlerInstance;
Console.WriteLine("I did it!");
}
Если лучший способ - сохранить ссылку на подписанный eventHandler, это можно сделать с помощью словаря.
В этом примере я должен использовать анонимный метод, чтобы включить параметр mergeColumn для набора DataGridViews.
Использование метода MergeColumn с параметром enable, для которого установлено значение true, включает событие, а использование с параметром false - отключает его.
static Dictionary<DataGridView, PaintEventHandler> subscriptions = new Dictionary<DataGridView, PaintEventHandler>();
public static void MergeColumns(this DataGridView dg, bool enable, params ColumnGroup[] mergedColumns) {
if(enable) {
subscriptions[dg] = (s, e) => Dg_Paint(s, e, mergedColumns);
dg.Paint += subscriptions[dg];
}
else {
if(subscriptions.ContainsKey(dg)) {
dg.Paint -= subscriptions[dg];
subscriptions.Remove(dg);
}
}
}
Недавно я обнаружил эту довольно старую ветку для проекта C# и нашел все ответы очень полезными. Однако был один аспект, который не подходил для моего конкретного варианта использования - все они возлагали бремя отказа от подписки на событие на подписчика. Я понимаю, что можно утверждать, что это дело подписчиков, но это нереально для моего проекта.
Мой основной вариант использования событий - это прослушивание таймеров для последовательной анимации (это игра). В этом сценарии я использую множество анонимных делегатов для объединения последовательностей в цепочки. Хранить ссылку на них не очень практично.
Чтобы решить эту проблему, я создал класс-оболочку вокруг события, которое позволяет вам подписаться на один вызов.
public class EventWrapper<TEventArgs> {
public event EventHandler<TEventArgs> Event;
private readonly HashSet<EventHandler<TEventArgs>> _subscribeOnces;
internal EventWrapper() {
_subscribeOnces = new HashSet<EventHandler<TEventArgs>>();
}
internal void Subscribe(EventHandler<TEventArgs> eventHandler) {
Event += eventHandler;
}
internal void SubscribeOnce(EventHandler<TEventArgs> eventHandler) {
_subscribeOnces.Add(eventHandler);
Event += eventHandler;
}
internal void Unsubscribe(EventHandler<TEventArgs> eventHandler) {
Event -= eventHandler;
}
internal void UnsubscribeAll() {
foreach (EventHandler<TEventArgs> eventHandler in Event?.GetInvocationList()) {
Event -= eventHandler;
}
}
internal void Invoke(Object sender, TEventArgs e) {
Event?.Invoke(sender, e);
if(_subscribeOnces.Count > 0) {
foreach (EventHandler<TEventArgs> eventHandler in _subscribeOnces) {
Event -= eventHandler;
}
_subscribeOnces.Clear();
}
}
internal void Remove() {
UnsubscribeAll();
_subscribeOnces.Clear();
}
}
Дополнительным преимуществом наличия этого в классе является то, что вы можете сделать его частным и предоставлять только те функции, которые вам нужны (тогда как события должны быть общедоступными). Например, предоставляйте только метод SubscribeOnce (но не Subscribe).
public class MyClass {
private EventWrapper<MyEventEventArgs> myEvent = new EventWrapper<MyEventEventArgs>();
public void FireMyEvent() {
myEvent.Invoke(this, new MyEventEventArgs(1000, DateTime.Now));
}
public void SubscribeOnce(EventHandler<MyEventEventArgs> eventHandler) {
myEvent.SubscribeOnce(eventHandler);
}
public class MyEventEventArgs : EventArgs {
public int MyInt;
public DateTime MyDateTime;
public MyEventEventArgs(int myInt, DateTime myDateTime) {
MyInt = myInt;
MyDateTime = myDateTime;
}
}
}
Компромисс здесь - дополнительные накладные расходы на наличие экземпляра этого для каждого события, однако в моем сценарии - это приемлемый компромисс, чтобы гарантировать, что мусор будет эффективно собираться, а код более удобен для обслуживания на стороне подписчика. Полный пример здесь.
Вот простое решение, которое удаляет все назначенные методы из события. Также анонимные методы.
Используйте этот код и настройте имена.
if (MyEvent != null)
foreach (Delegate del in MyEvent.GetInvocationList())
MyEvent -= (EventHandler<MyEventHandlerType>)del;
Пример использования
public class SomeClass
{
public event EventHandler<NiceEventArgs> NiceEvent;
public void RemoveHandlers()
{
if (NiceEvent != null)
foreach (Delegate del in NiceEvent.GetInvocationList())
NiceEvent -= (EventHandler<NiceEventArgs>)del;
}
}
Благодаря ответу Хемме , который я использовал в качестве вдохновения.
Есть способ решить эту проблему, реализовав замыкание самостоятельно вместо лямбда-выражения.
Предположим, что класс, который будет использоваться в качестве переменной захвата, выглядит следующим образом.
public class A
{
public void DoSomething()
{
...
}
}
public class B
{
public void DoSomething()
{
...
}
}
public class C
{
public void DoSomething()
{
...
}
}
Эти классы будут использоваться в качестве переменных захвата, поэтому мы создаем их экземпляры.
A a = new A();
B b = new B();
C c = new C();
Реализуйте класс закрытия, как показано ниже.
private class EventHandlerClosure
{
public A a;
public B b;
public C c;
public event EventHandler Finished;
public void MyMethod(object, MyEventArgs args)
{
a.DoSomething();
b.DoSomething();
c.DoSomething();
Console.WriteLine("I did it!");
Finished?.Invoke(this, EventArgs.Empty);
}
}
Создайте экземпляр класса закрытия, создайте обработчик, затем подпишитесь на событие и подпишитесь на лямбда-выражение, которое отменяет подписку на событие Finished класса закрытия.
var closure = new EventHandlerClosure
{
a = a,
b = b,
c = c
};
var handler = new MyEventHandler(closure.MyMethod);
MyEvent += handler;
closure.Finished += (s, e)
{
MyEvent -= handler;
}
Если вы хотите обратиться к какому-либо объекту с этим делегатом, возможно, вы можете использовать Delegate.CreateDelegate(Type, Object target, MethodInfo methodInfo) .net считают, что делегат равен по target и methodInfo