Pytanie: Dlaczego Java / C # nie może implementować RAII?
Wyjaśnienie: Wiem, że śmieciarz nie jest deterministyczny. Dlatego przy obecnych funkcjach językowych nie jest możliwe automatyczne wywoływanie metody Dispose () obiektu przy wyjściu z zakresu. Ale czy można dodać taką cechę deterministyczną?
Moje zrozumienie:
Wydaje mi się, że implementacja RAII musi spełniać dwa wymagania: 1. Żywotność zasobu musi być związana z zakresem.
2. Implikowany. Zwolnienie zasobu musi nastąpić bez wyraźnego oświadczenia programisty. Analogicznie do śmieciarza zwalniającego pamięć bez wyraźnego oświadczenia. „Ujawnienie” musi wystąpić tylko w momencie użycia klasy. Twórca biblioteki klas musi oczywiście jawnie zaimplementować metodę destructor lub Dispose ().
Java / C # spełnia punkt 1. W C # zasób implementujący IDisposable może być powiązany z zakresem „using”:
void test()
{
using(Resource r = new Resource())
{
r.foo();
}//resource released on scope exit
}
Nie spełnia to punktu 2. Programista musi jawnie powiązać obiekt ze specjalnym zakresem „używającym”. Programiści mogą (i robią) zapomnieć o jawnym powiązaniu zasobu z zakresem, tworząc wyciek.
W rzeczywistości bloki „using” są konwertowane przez kompilator na kod try-ultimate-dispose (). Ma tę samą wyraźną naturę wzorca try-last-dispose (). Bez domniemanego zwolnienia hakiem do zakresu jest cukier składniowy.
void test()
{
//Programmer forgot (or was not aware of the need) to explicitly
//bind Resource to a scope.
Resource r = new Resource();
r.foo();
}//resource leaked!!!
Myślę, że warto utworzyć funkcję języka w Javie / C #, która pozwala na specjalne obiekty, które są zaczepiane do stosu za pomocą inteligentnego wskaźnika. Ta funkcja umożliwia oznaczenie klasy jako związanej z zakresem, dzięki czemu zawsze jest tworzona z zaczepem do stosu. Mogą istnieć opcje dla różnych rodzajów inteligentnych wskaźników.
class Resource - ScopeBound
{
/* class details */
void Dispose()
{
//free resource
}
}
void test()
{
//class Resource was flagged as ScopeBound so the tie to the stack is implicit.
Resource r = new Resource(); //r is a smart-pointer
r.foo();
}//resource released on scope exit.
Myślę, że domniemanie jest „tego warte”. Podobnie jak niejawność zbierania śmieci jest „tego warta”. Jawne używanie bloków odświeża oczy, ale nie zapewnia przewagi semantycznej nad try-last-dispose ().
Czy zaimplementowanie takiej funkcji w językach Java / C # jest niepraktyczne? Czy można to wprowadzić bez zerwania starego kodu?
Dispose
s są zawsze uruchamiane, niezależnie od tego, jak są one wyzwalane. Dodanie ukrytego zniszczenia na końcu zakresu nie pomoże.using
wykonaniaDispose
jest gwarantowane (cóż, pomijanie procesu nagle umiera bez wyjątku, w którym to momencie przypuszczalnie wszystkie porządki stają się dyskusyjne).struct
), ale są one zazwyczaj unika z wyjątkiem bardzo szczególnych przypadkach. Zobacz także .Odpowiedzi:
Takie rozszerzenie języka byłoby znacznie bardziej skomplikowane i inwazyjne, niż się wydaje. Nie możesz po prostu dodać
do odpowiedniej sekcji specyfikacji języka i gotowe. Zignoruję problem wartości tymczasowych (
new Resource().doSomething()
), który można rozwiązać nieco bardziej ogólnym sformułowaniem, nie jest to najpoważniejszy problem. Na przykład ten kod zostałby zepsuty (i tego rodzaju rzeczy prawdopodobnie stają się ogólnie niemożliwe):Teraz potrzebujesz zdefiniowanych przez użytkownika konstruktorów kopiowania (lub przenieś konstruktorów) i zacznij je wywoływać wszędzie. Nie tylko ma to wpływ na wydajność, ale także sprawia, że te rzeczy skutecznie wyceniają typy, podczas gdy prawie wszystkie inne obiekty są typami referencyjnymi. W przypadku Javy jest to radykalne odchylenie od sposobu działania obiektów. W C # mniej tak (już ma
struct
s, ale nie ma dla nich AFAIK zdefiniowanych przez użytkownika konstruktorów kopiowania), ale nadal czyni te obiekty RAII bardziej specjalnymi. Alternatywnie, ograniczona wersja typów liniowych (por. Rust) może również rozwiązać problem, kosztem zakazu aliasingu, w tym przekazywania parametrów (chyba że chcesz wprowadzić jeszcze większą złożoność poprzez przyjęcie pożyczonych odniesień podobnych do rdzy i sprawdzania pożyczek).Można to zrobić technicznie, ale kończy się na kategorii rzeczy, które bardzo różnią się od wszystkiego innego w języku. Jest to prawie zawsze zły pomysł, który ma konsekwencje dla implementatorów (więcej przypadków, więcej czasu / kosztów w każdym dziale) i użytkowników (więcej pomysłów do nauczenia się, więcej możliwości błędów). To nie jest warte dodatkowej wygody.
źródło
File
ten sposób, nic się nie zmienia iDispose
nigdy nie jest wywoływane. Jeśli zawsze dzwoniszDispose
, nie możesz nic zrobić z przedmiotami jednorazowymi. A może proponujesz jakiś plan, jak go pozbyć, a czasem nie? Jeśli tak, opisz to szczegółowo, a powiem ci, w jakich sytuacjach się nie udaje.Dispose
jeśli referencja ucieka? Analiza ucieczki jest starym i trudnym problemem, nie zawsze będzie działać bez dalszych zmian w języku. Kiedy odniesienie jest przekazywane do innej (wirtualnej) metody (something.EatFile(f);
), należyf.Dispose
wywołać na końcu zakresu? Jeśli tak, przerywasz dzwoniące, które przechowująf
do późniejszego wykorzystania. Jeśli nie, wycieksz z zasobu, jeśli dzwoniący nie zapiszef
. Jedynym dość prostym sposobem na usunięcie tego jest system liniowy, który (jak już omówiłem w dalszej części mojej odpowiedzi) wprowadza wiele innych komplikacji.Największą trudnością we wdrożeniu czegoś takiego w Javie lub C # byłoby zdefiniowanie, jak działa transfer zasobów. Potrzebujesz sposobu, aby przedłużyć żywotność zasobu poza zakres. Rozważać:
Co gorsza, może to nie być oczywiste dla osoby wdrażającej
IWrapAResource
:Coś w rodzaju
using
oświadczenia C # jest prawdopodobnie tak blisko, jak masz zamiar mieć semantykę RAII bez uciekania się do zasobów liczących odniesienia lub wymuszania semantyki wartości wszędzie, takich jak C lub C ++. Ponieważ Java i C # mają niejawne współużytkowanie zasobów zarządzanych przez moduł wyrzucający elementy bezużyteczne, programista musi być w stanie zrobić to wybrać zakres, do którego zasób jest związany, co jest dokładnie tym, cousing
już robi.źródło
using
oświadczenie.IWrapSomething
go pozbywaćT
. Ktokolwiek stworzył,T
musi się o to martwić, niezależnie od tego, czy korzystausing
, jestIDisposable
sobą, czy ma jakiś schemat cyklu życia zasobów ad-hoc.Powodem, dla którego RAII nie może działać w języku takim jak C #, ale działa w C ++, jest to, że w C ++ możesz zdecydować, czy obiekt jest naprawdę tymczasowy (przydzielając go na stosie), czy też jest długowieczny (przez przydzielanie go na stercie za
new
pomocą wskaźników).Tak więc w C ++ możesz zrobić coś takiego:
W języku C # nie można rozróżnić dwóch przypadków, więc kompilator nie miałby pojęcia, czy sfinalizować obiekt, czy nie.
Możesz wprowadzić specjalny rodzaj zmiennej lokalnej, którego nie możesz wstawiać do pól itp. *, A który automatycznie zostanie usunięty, gdy wykracza poza zakres. To właśnie robi C ++ / CLI. W C ++ / CLI piszesz taki kod:
To kompiluje się w zasadzie do tej samej IL, co następujący C #:
Podsumowując, jeśli zgadnę, dlaczego projektanci C # nie dodali RAII, to dlatego, że uważali, że posiadanie dwóch różnych typów zmiennych lokalnych nie jest tego warte, głównie dlatego, że w języku z GC finalizacja deterministyczna nie jest przydatna, często.
* Nie bez odpowiednika
&
operatora, którym jest C ++ / CLI%
. Chociaż jest to „niebezpieczne” w tym sensie, że po zakończeniu metody pole będzie odnosić się do rozmieszczonego obiektu.źródło
struct
typów takich jak D.Jeśli
using
blokowanie przeszkadza ci w ich jawności, być może możemy zrobić mały krok w stronę mniejszej jawności, zamiast zmieniać samą specyfikację C #. Rozważ ten kod:Widzisz
local
dodane słowo kluczowe? Wszystko robi to dodać nieco więcej cukru składniowej, podobnie jakusing
, informując kompilator zadzwonićDispose
wfinally
bloku na końcu zakresu zmiennej. To wszystko. Jest to całkowicie równoważne z:ale z zakresem domniemanym, a nie jawnym. Jest to prostsze niż inne sugestie, ponieważ nie muszę definiować klasy jako ograniczonej zasięgiem. Po prostu czystszy, bardziej domyślny cukier składniowy.
Mogą występować problemy z trudnymi do rozwiązania zakresami, chociaż nie widzę ich teraz i byłbym wdzięczny każdemu, kto może to znaleźć.
źródło
using
użyjemy słowa kluczowego, możemy zachować istniejące zachowanie i użyć go również w przypadkach, gdy nie potrzebujemy określonego zakresu. Miećusing
domyślny zakres bez nawiasów .Na przykład, jak RAII działa w języku śmieci, sprawdź
with
słowo kluczowe w Pythonie . Zamiast polegać na deterministycznie zniszczonych obiektach, pozwala ci powiązać__enter__()
i__exit__()
metody z danym zakresem leksykalnym. Typowym przykładem jest:Podobnie jak w stylu RAII w C ++, plik byłby zamykany przy wychodzeniu z tego bloku, bez względu na to, czy jest to „normalne” wyjście, a
break
, natychmiastreturn
czy wyjątek.Pamiętaj, że
open()
wywołanie jest zwykłą funkcją otwierania plików. aby to zadziałało, zwrócony obiekt pliku zawiera dwie metody:Jest to powszechny idiom w Pythonie: obiekty powiązane z zasobem zazwyczaj zawierają te dwie metody.
Zauważ, że obiekt pliku może nadal pozostać przydzielony po
__exit__()
wywołaniu, ważne jest to, że jest zamknięty.źródło
with
w Pythonie jest prawie dokładnie tak jakusing
w C # i jako takie nie jest RAII, jeśli chodzi o to pytanie.defer
w języku Go).