Potknąłeś się tutaj i wyciągasz bardzo błędne wnioski, ponieważ używasz debuggera. Będziesz musiał uruchomić swój kod tak, jak działa na komputerze użytkownika. Przełącz się najpierw do kompilacji wydania za pomocą menedżera Build + Configuration, zmień opcję „Konfiguracja aktywnego rozwiązania” w lewym górnym rogu na „Wersja”. Następnie przejdź do Narzędzia + Opcje, Debugowanie, Ogólne i odznacz opcję „Wstrzymaj optymalizację JIT”.
Teraz ponownie uruchom program i majsterkuj przy kodzie źródłowym. Zwróć uwagę, że dodatkowe szelki nie mają żadnego efektu. Zwróć też uwagę, że ustawienie zmiennej na null nie robi żadnej różnicy. Zawsze wypisze "1". Teraz działa tak, jak masz nadzieję i spodziewasz się, że zadziała.
Co pozostawia z zadaniem wyjaśnienia, dlaczego działa tak inaczej po uruchomieniu kompilacji debugowania. Wymaga to wyjaśnienia, w jaki sposób moduł odśmiecania pamięci wykrywa zmienne lokalne i jaki ma na to wpływ obecność debugera.
Po pierwsze, jitter spełnia dwa ważne zadania podczas kompilowania IL dla metody do kodu maszynowego. Pierwsza z nich jest bardzo widoczna w debugerze, można zobaczyć kod maszynowy za pomocą okna Debug + Windows + Disassembly. Drugi obowiązek jest jednak całkowicie niewidoczny. Generuje również tabelę opisującą, w jaki sposób używane są zmienne lokalne w treści metody. Ta tabela zawiera wpis dla każdego argumentu metody i zmiennej lokalnej z dwoma adresami. Adres, pod którym zmienna będzie najpierw przechowywać odniesienie do obiektu. I adres instrukcji kodu maszynowego, w której ta zmienna nie jest już używana. Również czy ta zmienna jest przechowywana w ramce stosu, czy w rejestrze procesora.
Ta tabela jest niezbędna dla modułu odśmiecania pamięci, musi wiedzieć, gdzie szukać odwołań do obiektów podczas wykonywania kolekcji. Całkiem łatwo to zrobić, gdy odniesienie jest częścią obiektu na stercie GC. Zdecydowanie nie jest to łatwe, gdy odniesienie do obiektu jest przechowywane w rejestrze procesora. Tabela mówi, gdzie szukać.
Adres „nieużywany” w tabeli jest bardzo ważny. To sprawia, że śmieciarz jest bardzo wydajny . Może zbierać odniesienie do obiektu, nawet jeśli jest używane wewnątrz metody, a ta metoda jeszcze się nie zakończyła. Co jest bardzo powszechne, na przykład twoja metoda Main () przestanie działać tylko tuż przed zakończeniem programu. Oczywiście nie chciałbyś, aby odniesienia do obiektów używane wewnątrz tej metody Main () istniały przez czas trwania programu, co oznaczałoby przeciek. Jitter może użyć tabeli, aby odkryć, że taka zmienna lokalna nie jest już użyteczna, w zależności od tego, jak daleko program posunął się wewnątrz tej metody Main (), zanim wykonał wywołanie.
Niemal magiczną metodą związaną z tą tabelą jest GC.KeepAlive (). Jest to bardzo szczególna metoda, w ogóle nie generuje żadnego kodu. Jego jedynym obowiązkiem jest modyfikacja tej tabeli. to rozszerzaokres istnienia zmiennej lokalnej, co zapobiega pobieraniu elementów bezużytecznych przez przechowywane przez nią odwołanie. Jedynym momentem, w którym musisz go użyć, jest powstrzymanie GC przed nadmiernym gromadzeniem odwołania, co może się zdarzyć w scenariuszach międzyoperacyjnych, w których odwołanie jest przekazywane do niezarządzanego kodu. Moduł odśmiecania pamięci nie widzi takich odwołań używanych przez taki kod, ponieważ nie został skompilowany przez jitter, więc nie ma tabeli, która mówi, gdzie szukać odwołania. Przekazanie obiektu delegata do niezarządzanej funkcji, takiej jak EnumWindows (), jest standardowym przykładem, kiedy trzeba użyć GC.KeepAlive ().
Tak więc, jak widać z przykładowego fragmentu kodu po uruchomieniu go w kompilacji wydania, zmienne lokalne mogą zostać zebrane wcześnie, zanim metoda zakończy wykonywanie. Co więcej, obiekt może zostać zebrany, gdy jedna z jego metod jest uruchomiona, jeśli ta metoda już nie odnosi się do tego . Jest z tym problem, debugowanie takiej metody jest bardzo niewygodne. Ponieważ możesz dobrze umieścić zmienną w oknie Watch lub sprawdzić ją. I zniknie podczas debugowania, jeśli wystąpi GC. Byłoby to bardzo nieprzyjemne, więc jitter jest świadomy obecności dołączonego debuggera. Następnie modyfikujetabeli i zmienia „ostatnio używany” adres. I zmienia go z normalnej wartości na adres ostatniej instrukcji w metodzie. Co utrzymuje zmienną przy życiu, dopóki metoda nie zwróciła. Dzięki temu możesz go obserwować do momentu powrotu metody.
To teraz wyjaśnia również, co widziałeś wcześniej i dlaczego zadałeś pytanie. Wyświetla „0”, ponieważ wywołanie GC.Collect nie może zebrać odwołania. Tabela mówi, że zmienna jest używana po wywołaniu GC.Collect (), aż do końca metody. Zmuszony do powiedzenia tego przez dołączenie debugera i uruchomienie kompilacji debugowania.
Ustawienie zmiennej na null ma teraz wpływ, ponieważ GC sprawdzi zmienną i nie będzie już widzieć odwołania. Ale upewnij się, że nie wpadniesz w pułapkę, w którą wpadło wielu programistów C #, pisanie tego kodu było bezcelowe. Nie ma znaczenia, czy ta instrukcja jest obecna, czy nie, podczas uruchamiania kodu w kompilacji wydania. W rzeczywistości optymalizator jittera usunie tę instrukcję, ponieważ nie ma ona żadnego wpływu. Więc pamiętaj, aby nie pisać takiego kodu, nawet jeśli wydawało się, że ma to wpływ.
Ostatnia uwaga na ten temat: to właśnie sprawia, że programiści piszą małe programy, aby zrobić coś z aplikacją pakietu Office. Debugger zwykle umieszcza je na niewłaściwej ścieżce, chcą, aby program pakietu Office zakończył działanie na żądanie. Odpowiednim sposobem jest wywołanie GC.Collect (). Ale odkryją, że to nie działa, gdy debugują swoją aplikację, prowadząc ich do krainy nigdy-nigdy, wywołując Marshal.ReleaseComObject (). Ręczne zarządzanie pamięcią, rzadko działa poprawnie, ponieważ łatwo przeoczą niewidoczne odniesienie do interfejsu. GC.Collect () faktycznie działa, ale nie podczas debugowania aplikacji.