W jednym z projektów, w których biorę udział, jest szerokie zastosowanie WeakAction
. Jest to klasa, która pozwala zachować referencję do instancji akcji, nie powodując, że jej cel nie zostanie zebrany. Sposób działania jest prosty, działa na konstruktorze i utrzymuje słabe odniesienie do celu akcji i metody, ale odrzuca odniesienie do samego działania. Kiedy nadejdzie czas wykonania akcji, sprawdza, czy cel wciąż żyje, a jeśli tak, wywołuje metodę na celu.Bug in WeakAction w przypadku zamknięcia działania
Wszystko działa dobrze, z wyjątkiem jednego przypadku - gdy akcja jest tworzona w zamknięciu. Rozważmy następujący przykład:
public class A
{
WeakAction action = null;
private void _register(string msg)
{
action = new WeakAction(() =>
{
MessageBox.Show(msg);
}
}
}
Ponieważ wyrażenie lambda używa zmiennej lokalnej msg
, auto C# kompilator generuje zagnieżdżone klasy, aby pomieścić wszystkie zmienne zamknięcia. Celem akcji jest instancja klasy zagnieżdżonej zamiast instancji A. Akcja przekazana do konstruktora WeakAction
nie jest odwoływana po zakończeniu konstruktora, więc garbage collector może go natychmiast zlikwidować. Później, jeśli zostanie wykonany WeakAction
, nie zadziała, ponieważ cel nie jest już aktywny, mimo że pierwotna instancja A
jest żywa.
Nie mogę zmienić sposobu wywołania WeakAction
(ponieważ jest szeroko stosowany), ale mogę zmienić jego implementację. Zastanawiałem się nad próbą znalezienia sposobu na uzyskanie dostępu do instancji A i zmuszenia instancji klasy zagnieżdżonej do pozostania przy życiu, podczas gdy instancja A jest wciąż żywa, ale nie wiem, jak ją uzyskać.
Istnieje wiele pytań o to, co A
ma do czynienia z niczego, a sugestie, aby zmienić sposób A
tworzy słabe działanie (których nie możemy zrobić), więc tutaj jest wyjaśnienie:
Instancja klasy A
chce, aby instancja klasy B
powiadomiła go, gdy coś się stanie, więc zapewnia wywołanie zwrotne za pomocą obiektu Action
. A
nie jest świadomy, że B
używa słabych akcji, po prostu zapewnia Action
, aby służyć jako callback. Fakt, że B
używa WeakAction
jest szczegółem implementacji, który nie jest ujawniony. B
musi zapisać tę akcję i użyć jej w razie potrzeby. Ale B
może żyć znacznie dłużej niż A
, a posiadanie silnego odniesienia do normalnej akcji (która sama w sobie posiada silną referencję instancji A
, która ją wygenerowała) powoduje, że A
nigdy nie zostanie zbuforowane. Jeśli A
jest częścią listy elementów, które nie są już żywe, oczekujemy, że A
zostanie zbuforowane, a z powodu odniesienia do wstrzymań Akcji, która sama w sobie wskazuje na A
, mamy wyciek pamięci.
Więc zamiast B
posiadający działanie powodujące A
warunkiem, B
zawija je w WeakAction
i przechowuje tylko słabe działanie. Kiedy nadejdzie czas, aby to nazwać, B
robi to tylko wtedy, gdy WeakAction
jest wciąż żywy, który powinien być tak długi, jak A
jest wciąż żywy.
A
tworzy tę akcję wewnątrz metody i nie zachowuje odniesienia do niej na własną rękę - to jest dane. Ponieważ Action
został skonstruowany w kontekście konkretnej instancji A
, to wystąpienie jest celem A
, a gdy zginie A
, wszystkie słabe odniesienia do niego stają się null
, aby nie wywoływać go i unieszkodliwiać z obiektu .
Ale czasami metoda generująca Action
wykorzystuje zmienne zdefiniowane lokalnie w tej funkcji. W takim przypadku kontekst, w którym działa akcja, obejmuje nie tylko instancję A
, ale także stan zmiennych lokalnych w metodzie (nazywany "zamknięciem"). Kompilator C# robi to, tworząc ukrytą klasę zagnieżdżoną do przechowywania tych zmiennych (pozwala to nazwać ją A__closure
), a wystąpienie, które staje się celem Action
, jest instancją A__closure
, a nie A
. Jest to coś, o czym użytkownik nie powinien wiedzieć. Tyle tylko, że to wystąpienie A__closure
odwołuje się tylko do obiektu Action
. A ponieważ tworzymy słabe odniesienie do celu i nie mamy odniesienia do akcji, nie ma odniesienia do instancji A__closure
, a garbage collector może (i zwykle robi) natychmiast ją pozbyć. Tak więc A
umiera, A__closure
umiera, i pomimo faktu, że A
nadal oczekuje wywołania zwrotnego, nie można go wykonać podając B
.
To błąd.
Moje pytanie było, czy ktoś zna sposób, że konstruktor WeakAction
, jedyny kawałek kodu, który faktycznie posiada oryginalny przedmiot działania, tymczasowo, może w jakiś magiczny sposób wyodrębnić oryginalne wystąpienie A
od instancji, że to A__closure
znajduje się w Target
z Action
. Jeśli tak, mógłbym przedłużyć cykl życia A__Closure
, aby dopasować go do stanu A
.
Czy zostało to naprawione w późniejszych wersjach C#? –