Anuluj subskrypcję anonimowej metody w C #

222

Czy można anulować anonimową metodę ze zdarzenia?

Jeśli zasubskrybuję takie wydarzenie:

void MyMethod()
{
    Console.WriteLine("I did it!");
}

MyEvent += MyMethod;

Mogę anulować subskrypcję w ten sposób:

MyEvent -= MyMethod;

Ale jeśli zasubskrybuję za pomocą anonimowej metody:

MyEvent += delegate(){Console.WriteLine("I did it!");};

czy można zrezygnować z tej anonimowej metody? Jeśli tak to jak?

Eric
źródło
4
Jak dla dlaczego nie można zrobić: stackoverflow.com/a/25564492/23354
Marc Gravell

Odpowiedzi:

230
Action myDelegate = delegate(){Console.WriteLine("I did it!");};

MyEvent += myDelegate;


// .... later

MyEvent -= myDelegate;

Wystarczy zachować odniesienie do delegata w pobliżu.

Jacob Krall
źródło
141

Jedną z technik jest zadeklarowanie zmiennej przechowującej anonimową metodę, która byłaby wówczas dostępna w samej metodzie anonimowej. To działało dla mnie, ponieważ pożądanym zachowaniem było anulowanie subskrypcji po zakończeniu wydarzenia.

Przykład:

MyEventHandler foo = null;
foo = delegate(object s, MyEventArgs ev)
    {
        Console.WriteLine("I did it!");
        MyEvent -= foo;
    };
MyEvent += foo;
J c
źródło
1
Korzystając z tego rodzaju kodu, Resharper narzeka na dostęp do zmodyfikowanego zamknięcia ... czy to podejście jest niezawodne? To znaczy, czy jesteśmy pewni, że zmienna „foo” w ciele metody anonimowej naprawdę odwołuje się do samej metody anonimowej?
BladeWise,
7
Znalazłem odpowiedź na moje wątpliwości i jest tak, że „foo” naprawdę będzie zawierało odniesienie do anonimowej metody. Przechwycona zmienna jest modyfikowana, ponieważ jest przechwytywana przed przypisaniem do niej anonimowej metody.
BladeWise
2
Właśnie tego potrzebowałem! Brakowało mi = null. (MyEventHandler foo = deleguj {... MyEvent- = foo;}; MyEvent + = foo; nie działało ...)
TDaver
Resharper 6.1 nie narzeka, jeśli zadeklarujesz go jako tablicę. Wydaje się to trochę dziwne, ale zamierzam ślepo zaufać moim narzędziom w tym przypadku: MyEventHandler [] foo = {null}; foo [0] = ... {... MyEvent - = foo [0]; }; MyEvent + = foo [0];
Mike Post
21

Z pamięci specyfikacja wyraźnie nie gwarantuje takiego zachowania, jeśli chodzi o równoważność delegatów utworzonych za pomocą anonimowych metod.

Jeśli musisz anulować subskrypcję, powinieneś albo użyć „normalnej” metody, albo zachować delegata w innym miejscu, abyś mógł zrezygnować z dokładnie tego samego delegata, którego subskrybowałeś.

Jon Skeet
źródło
Ja Jon, co rozumiesz? Nie rozumiem. czy rozwiązanie ujawnione przez „J c” nie będzie działać poprawnie?
Eric Ouellet,
@EricOuellet: Ta odpowiedź jest w zasadzie implementacją „zachowaj delegata w innym miejscu, abyś mógł wypisać się z dokładnie tym samym delegatem, którego subskrybowałeś”.
Jon Skeet,
Jon, przepraszam, wiele razy czytałem twoją odpowiedź, próbując dowiedzieć się, co masz na myśli i gdzie rozwiązanie „J c” nie używa tego samego delegata do subskrybowania i anulowania subskrypcji, ale nie mogę tego zmylić. Może mógłbyś wskazać mi artykuł, który wyjaśnia, co mówisz? Wiem o twojej reputacji i naprawdę chciałbym zrozumieć, co masz na myśli, cokolwiek możesz połączyć, byłoby naprawdę mile widziane.
Eric Ouellet,
1
Znalazłem: msdn.microsoft.com/en-us/library/ms366768.aspx, ale oni nie zalecają używania anonimowych, ale nie twierdzą, że jest jakiś poważny problem?
Eric Ouellet,
Znalazłem ... Wielkie dzięki (patrz odpowiedź Michaela Blome'a): social.msdn.microsoft.com/Forums/en-US/csharplanguage/thread/...
Eric Ouellet
16

W 3.0 można skrócić do:

MyHandler myDelegate = ()=>Console.WriteLine("I did it!");
MyEvent += myDelegate;
...
MyEvent -= myDelegate;

źródło
15

Od czasu wydania funkcji funkcji lokalnych w języku C # 7.0 podejście sugerowane przez J c staje się naprawdę czyste.

void foo(object s, MyEventArgs ev)
{
    Console.WriteLine("I did it!");
    MyEvent -= foo;
};
MyEvent += foo;

Tak więc, szczerze mówiąc, nie masz tutaj anonimowej funkcji jako zmiennej. Ale przypuszczam, że motywację do użycia go w twoim przypadku można zastosować do funkcji lokalnych.

mazharenko
źródło
1
Aby zwiększyć czytelność, możesz przenieść MyEvent + = foo; wiersz przed deklaracją foo.
Mark Zhukovsky,
9

Zamiast utrzymywać odniesienie do dowolnego delegata, możesz instrumentować swoją klasę, aby zwrócić listę wywołań wydarzenia z powrotem do osoby dzwoniącej. Zasadniczo możesz napisać coś takiego (zakładając, że MyEvent jest zadeklarowany w MyClass):

public class MyClass 
{
  public event EventHandler MyEvent;

  public IEnumerable<EventHandler> GetMyEventHandlers()  
  {  
      return from d in MyEvent.GetInvocationList()  
             select (EventHandler)d;  
  }  
}

Aby uzyskać dostęp do całej listy wywołań spoza MyClass i anulować subskrypcję dowolnego programu obsługi. Na przykład:

myClass.MyEvent -= myClass.GetMyEventHandlers().Last();

Tutaj napisałem pełny post o tej technice .

hemme
źródło
2
Czy to oznacza, że ​​mogę przypadkowo zrezygnować z subskrypcji innej instancji (tj. Nie mnie) od wydarzenia, jeśli subskrybują mnie?
dumbledad
@dumbledad oczywiście wyrejestruje to ostatnio zarejestrowane. Jeśli chcesz dynamicznie anulować subskrypcję określonego anonimowego uczestnika, musisz go jakoś zidentyfikować. Proponuję więc zachować referencję :)
LuckyLikey
To całkiem fajne, co robisz, ale nie mogę sobie wyobrazić jednego przypadku, w którym mogłoby to być przydatne. Ale tak naprawdę nie rozwiązuje pytania PO. -> +1. IMHO, nie należy po prostu używać anonimowych delegatów, jeśli zostaną później wyrejestrowani. Trzymanie ich jest głupie -> lepsza metoda użycia. Usunięcie tylko niektórych delegatów z listy wywołań jest dość losowe i bezużyteczne. Popraw mnie, jeśli się mylę. :)
LuckyLikey
6

Rodzaj kulawego podejścia:

public class SomeClass
{
  private readonly IList<Action> _eventList = new List<Action>();

  ...

  public event Action OnDoSomething
  {
    add {
      _eventList.Add(value);
    }
    remove {
      _eventList.Remove(value);
    }
  }
}
  1. Zastąp metody dodawania / usuwania zdarzeń.
  2. Zachowaj listę tych procedur obsługi zdarzeń.
  3. W razie potrzeby wyczyść je wszystkie i ponownie dodaj pozostałe.

Może to nie działać lub być najbardziej wydajną metodą, ale powinno zakończyć pracę.

casademora
źródło
14
Jeśli uważasz, że jest kiepski, nie publikuj tego.
Jerry Nixon
2

Jeśli chcesz kontrolować rezygnację z subskrypcji, musisz wybrać trasę wskazaną w zaakceptowanej odpowiedzi. Jeśli jednak obawiasz się o wyczyszczenie referencji, gdy klasa subskrybująca wykracza poza zakres, istnieje inne (nieco skomplikowane) rozwiązanie polegające na użyciu słabych referencji. Właśnie opublikowałem pytanie i odpowiedź na ten temat.

Benjol
źródło
2

Jedno proste rozwiązanie:

wystarczy przekazać zmienną uchwytu zdarzenia jako parametr do siebie. Zdarzenie, jeśli masz przypadek, że nie możesz uzyskać dostępu do oryginalnej utworzonej zmiennej z powodu wielowątkowości, możesz użyć tego:

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!");
}
Manuel Marhold
źródło
co jeśli MyEvent zostanie wywołany dwukrotnie, zanim MyEvent -= myEventHandlerInstance;zostanie uruchomiony? Jeśli to możliwe, wystąpiłby błąd. Ale nie jestem pewien, czy tak jest.
LuckyLikey
0

jeśli chcesz odwołać się do jakiegoś obiektu z tym delegatem, być może możesz użyć Delegate.CreateDelegate (typ, obiekt docelowy, MethodInfo methodInfo) .net uważa, że ​​delegat jest równy według celu i methodInfo

użytkownik3217549
źródło
0

Jeśli najlepszym sposobem jest zachowanie odwołania do subskrybowanego modułu obsługi zdarzenia, można to osiągnąć za pomocą słownika.

W tym przykładzie muszę użyć anonimowej metody, aby dołączyć parametr mergeColumn dla zestawu DataGridViews.

Użycie metody MergeColumn z parametrem enable ustawionym na wartość true włącza zdarzenie podczas korzystania z parametru false powoduje wyłączenie

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);
        }
    }
}
Larry
źródło