Jako długoletni programista C #, ostatnio przyjechałem, aby dowiedzieć się więcej o zaletach pozyskiwania zasobów to inicjalizacja (RAII). W szczególności odkryłem, że idiom C #:
using (var dbConn = new DbConnection(connStr)) {
// do stuff with dbConn
}
ma odpowiednik C ++:
{
DbConnection dbConn(connStr);
// do stuff with dbConn
}
co oznacza, że zapamiętywanie wykorzystania zasobów jak DbConnection
w using
bloku nie jest konieczne w C ++! Wydaje się to dużą zaletą C ++. Jest to tym bardziej przekonująca, jeśli wziąć pod uwagę klasę, która ma element instancji typu DbConnection
, na przykład
class Foo {
DbConnection dbConn;
// ...
}
W C # musiałbym mieć implementację Foo IDisposable
jako taką:
class Foo : IDisposable {
DbConnection dbConn;
public void Dispose()
{
dbConn.Dispose();
}
}
i co gorsza, każdy użytkownik Foo
musiałby pamiętać o zamknięciu Foo
w using
bloku, na przykład:
using (var foo = new Foo()) {
// do stuff with "foo"
}
Teraz patrząc na C # i jego korzenie w Javie Zastanawiam się ... czy programiści Java w pełni docenili to, co zrezygnowali, gdy porzucili stos na rzecz stosu, a tym samym porzucili RAII?
(Podobnie, czy Stroustrup w pełni docenił znaczenie RAII?)
źródło
Odpowiedzi:
Jestem pewien, że Gosling nie zrozumiał znaczenia RAII w czasie, gdy projektował Javę. W swoich wywiadach często mówił o przyczynach pominięcia leków generycznych i przeciążania operatora, ale nigdy nie wspominał o deterministycznych niszczycielach i RAII.
Co zabawne, nawet Stroustrup nie zdawał sobie sprawy ze znaczenia deterministycznych niszczycieli w momencie ich projektowania. Nie mogę znaleźć cytatu, ale jeśli naprawdę go lubisz, możesz go znaleźć wśród jego wywiadów tutaj: http://www.stroustrup.com/interviews.html
źródło
manual
zarządzania pamięcią inteligentnych wskaźników C ++ . Są bardziej jak deterministyczny, kontrolowany kolektor śmieci. Przy prawidłowym stosowaniu inteligentne wskaźniki to kolana pszczół.Tak, projektanci C # (i, jestem pewien, Java) specjalnie postanowili przeciw deterministycznej finalizacji. Pytałem o to Andersa Hejlsberga wiele razy około 1999-2002.
Po pierwsze, idea innej semantyki dla obiektu w zależności od tego, czy jest on oparty na stosie, czy na stosie, jest z pewnością sprzeczny z ujednoliconym celem projektowania obu języków, którym było uwolnienie programistów od dokładnie takich problemów.
Po drugie, nawet jeśli uznajesz, że istnieją zalety, istnieją znaczne złożoności wdrażania i nieefektywności związane z prowadzeniem ksiąg rachunkowych. Tak naprawdę nie można umieszczać na stosie obiektów podobnych do stosu w zarządzanym języku. Pozostaje Ci powiedzieć „semantyka podobna do stosu” i zobowiązać się do znacznej pracy (typy wartości są już wystarczająco trudne, pomyśl o obiekcie, który jest instancją klasy złożonej, z referencjami przychodzącymi i wracającymi do pamięci zarządzanej).
Z tego powodu nie chcesz deterministycznej finalizacji każdego obiektu w systemie programowania, w którym „(prawie) wszystko jest przedmiotem”. Więc nie trzeba wprowadzać jakieś składni programator sterowany w celu oddzielenia normalnie śledzonego obiektu z jednego, który ma deterministycznego finalizację.
W języku C # masz
using
słowo kluczowe, które pojawiło się dość późno w projektowaniu tego, co stało się C # 1.0. CałaIDisposable
sprawa jest dość nieszczęsna i zastanawia się, czy bardziej elegancko byłobyusing
pracować ze składnią C ++ destruktora~
oznaczającą te klasy, do którychIDisposable
wzorzec płyty kotłowej mógłby zostać automatycznie zastosowany?źródło
~
składni jako cukru syntaktycznegoIDisposable.Dispose()
~
jako cukier składniowej dlaIDisposable.Dispose()
, i to o wiele wygodniejsze niż składni C #.Należy pamiętać, że Java została opracowana w latach 1991-1995, gdy C ++ był znacznie innym językiem. Wyjątki (które wymagały RAII ) i szablony (które ułatwiły implementację inteligentnych wskaźników) były funkcjami „nowymi”. Większość programistów C ++ pochodziła z C i była przyzwyczajona do ręcznego zarządzania pamięcią.
Wątpię więc, aby deweloperzy Javy świadomie postanowili zrezygnować z RAII. Jednak dla Javy była to celowa decyzja o preferowaniu semantyki odniesienia zamiast semantyki wartości. Deterministyczne zniszczenie jest trudne do wdrożenia w języku semantyki odniesienia.
Dlaczego więc używać semantyki odniesienia zamiast semantyki wartości?
Ponieważ dzięki temu język jest znacznie prostszy.
Foo
iFoo*
lub pomiędzyfoo.bar
ifoo->bar
.clone()
. Wiele obiektów po prostu nie musi być kopiowanych. Na przykład, niezmienne nie.)private
konstruktorów kopiowania ioperator=
uczynienia klasy niemożliwą do kopiowania. Jeśli nie chcesz kopiować obiektów klasy, po prostu nie piszesz funkcji, aby je skopiować.swap
Funkcje nie są potrzebne . (Chyba że piszesz rutynę sortowania.)Główną wadą semantyki odwołań jest to, że gdy każdy obiekt potencjalnie ma wiele odwołań do niego, trudno jest określić, kiedy należy go usunąć. Prawie musisz mieć automatyczne zarządzanie pamięcią.
Java zdecydowała się na niedeterministyczny moduł wyrzucania elementów bezużytecznych.
Czy GC nie może być deterministyczna?
Tak, może. Na przykład implementacja języka C w Pythonie korzysta z liczenia referencji. Później dodano śledzenie GC do obsługi cyklicznych śmieci, w których nie powiodły się rachunki.
Ale liczenie jest okropnie nieefektywne. Wiele cykli procesora spędziłem na aktualizacji liczników. Co gorsza, w środowisku wielowątkowym (takim jak ten, dla którego zaprojektowano Javę), w którym te aktualizacje muszą być synchronizowane. Znacznie lepiej jest używać zerowego modułu wyrzucania elementów bezużytecznych, dopóki nie trzeba przełączyć się na inny.
Można powiedzieć, że Java zdecydowała się zoptymalizować typowy przypadek (pamięć) kosztem nie zamiennych zasobów, takich jak pliki i gniazda. Dzisiaj, w świetle przyjęcia RAII w C ++, może się to wydawać złym wyborem. Pamiętaj jednak, że znaczną część docelowych odbiorców Javy stanowili programiści C (lub „C z klasami”), którzy byli przyzwyczajeni do jawnego zamykania tych rzeczy.
A co z „stosami obiektów” w C ++ / CLI?
Są tylko składniowym cukrem dla
Dispose
( oryginalny link ), podobnie jak C #using
. Nie rozwiązuje to jednak ogólnego problemu deterministycznego zniszczenia, ponieważ można utworzyć anonimowy,gcnew FileStream("filename.ext")
a C ++ / CLI nie usunie go automatycznie.źródło
using
oświadczenie radzi sobie dobrze z wieloma problemami związanymi z czyszczeniem, ale wiele innych pozostaje. Sugerowałbym, że właściwym podejściem do języka i frameworka byłoby deklaratywne rozróżnienie między miejscami przechowywania, które „posiadają” odnośniki, aIDisposable
tymi, które nie są; nadpisanie lub porzucenie miejsca przechowywania, do któregoIDisposable
należy odnośnik, powinno zlikwidować cel w przypadku braku przeciwnej dyrektywy.new Date(oldDate.getTime())
.Java7 wprowadziła coś podobnego do C #
using
: Instrukcja try-with-resourcesSądzę więc, że albo nie świadomie zdecydowali się nie wdrażać RAII, albo zmienili zdanie.
źródło
java.lang.AutoCloseable
. Prawdopodobnie nie jest to wielka sprawa, ale nie podoba mi się, jak to jest nieco ograniczone. Może mam jakiś inny obiekt, który powinien zostać automatycznie wydany, ale bardzo dziwnie semantycznie jest sprawić, by zaimplementowałAutoCloseable
...using
to nie to samo, co RAII - w jednym przypadku dzwoniący martwi się o pozbycie się zasobów, w drugim przypadku odbiorca go obsługuje.using
/ try-with-resources nie jest taki sam jak RAII.using
i to nie jest nigdzie w pobliżu RAII.Java celowo nie ma obiektów opartych na stosie (zwanych także obiektami wartości). Są one konieczne, aby obiekt był automatycznie niszczony na końcu takiej metody.
Z tego powodu i faktu, że Java jest gromadzona w pamięci, deterministyczne finalizowanie jest mniej więcej niemożliwe (np. Co się stanie, jeśli mój „lokalny” obiekt zostanie przywołany gdzieś indziej?) Gdy więc metoda się skończy, nie chcemy, aby została zniszczona ) .
Jest to jednak w porządku dla większości z nas, ponieważ prawie nigdy nie ma potrzeby deterministycznej finalizacji, z wyjątkiem interakcji z rodzimymi zasobami (C ++)!
Dlaczego Java nie ma obiektów opartych na stosie?
(Inne niż prymitywne ..)
Ponieważ obiekty oparte na stosie mają inną semantykę niż odwołania na podstawie stosu. Wyobraź sobie następujący kod w C ++; co to robi?
myObject
jest to lokalny obiekt oparty na stosie, wywoływany jest konstruktor kopii (jeśli wynik jest do czegoś przypisany).myObject
jest to lokalny obiekt oparty na stosie i zwracamy referencję, wynik jest niezdefiniowany.myObject
jest to element członkowski / globalny, wywoływany jest konstruktor kopii (jeśli wynik jest przypisany do czegoś).myObject
jest to element członkowski / globalny i zwracamy referencję, referencja jest zwracana.myObject
jest wskaźnikiem do lokalnego obiektu opartego na stosie, wynik jest niezdefiniowany.myObject
jest wskaźnikiem do elementu członkowskiego / globalnego, ten wskaźnik jest zwracany.myObject
jest wskaźnikiem do obiektu opartego na stercie, wskaźnik ten jest zwracany.Co teraz robi ten sam kod w Javie?
myObject
jest zwracane. Nie ma znaczenia, czy zmienna jest lokalna, członkowska czy globalna; i nie ma się czym martwić o obiekty oparte na stosie lub przypadki wskaźników.Powyższe pokazuje, dlaczego obiekty oparte na stosie są bardzo częstą przyczyną błędów programowania w C ++. Z tego powodu projektanci Java wyjęli je; i bez nich nie ma sensu używać RAII w Javie.
źródło
Twój opis otworów
using
jest niepełny. Rozważ następujący problem:Moim zdaniem brak zarówno RAII, jak i GC był złym pomysłem. Jeśli chodzi o zamykanie plików w Javie, jest
malloc()
ifree()
tam.źródło
using
klauzula jest wielkim krokiem naprzód dla języka C # w Javie. Umożliwia deterministyczne niszczenie, a tym samym prawidłowe zarządzanie zasobami (nie jest tak dobre jak RAII, jak trzeba pamiętać, ale zdecydowanie jest dobrym pomysłem).free()
wfinally
.IEnumerable
nie odziedziczył po nimIDisposable
, a było kilka specjalnych iteratorów, które nigdy nie mogły zostać zaimplementowane.Jestem całkiem stara Byłem tam, widziałem to i wiele razy waliłem w nie głową.
Byłem na konferencji w Hursley Park, gdzie chłopcy IBM opowiadali nam, jak cudowny jest ten nowy język Java, tylko ktoś zapytał ... dlaczego nie ma destruktora dla tych obiektów. Nie miał na myśli tego, co znamy jako destruktor w C ++, ale nie było też finalizatora (lub miał finalizatory, ale w zasadzie nie działały). To jest bardzo dawno temu i doszliśmy do wniosku, że Java była trochę zabawkowym językiem.
teraz dodali Finalizatory do specyfikacji języka, a Java zyskała trochę akceptacji.
Oczywiście później wszystkim powiedziano, aby nie nakładali finalizatorów na swoje obiekty, ponieważ to znacznie spowolniło GC. (ponieważ musiał nie tylko zablokować stertę, ale także przenieść obiekty, które mają zostać sfinalizowane, do obszaru tymczasowego, ponieważ metody te nie mogły zostać wywołane, ponieważ GC wstrzymała działanie aplikacji. Zamiast tego zostałyby wywołane bezpośrednio przed następnym Cykl GC) (i co gorsza, czasami finalizator nigdy nie zostanie wywołany, gdy aplikacja zostanie zamknięta. Wyobraź sobie, że nie masz zamkniętego uchwytu pliku)
Potem mieliśmy C # i pamiętam forum dyskusyjne na MSDN, gdzie powiedziano nam, jak wspaniały był ten nowy język C #. Ktoś zapytał, dlaczego nie było deterministycznej finalizacji, a chłopcy ze stwardnienia rozsianego powiedzieli nam, że nie potrzebujemy takich rzeczy, a następnie powiedzieli nam, że musimy zmienić nasz sposób projektowania aplikacji, a następnie powiedzieli nam, jak niesamowity był GC i jak wszystkie nasze stare aplikacje były śmieci i nigdy nie działały z powodu wszystkich okólników. Potem poddali się presji i powiedzieli nam, że dodali ten wzór IDispose do specyfikacji, której moglibyśmy użyć. Myślałem, że w tym momencie wróciłem do ręcznego zarządzania pamięcią w aplikacjach C #.
Oczywiście, chłopcy ze stwardnienia rozsianego później odkryli, że wszystko, co nam powiedzieli, to ... cóż, uczynili IDispose nieco więcej niż zwykłym interfejsem, a później dodali instrukcję using. W00t! Zrozumieli, że mimo wszystko w języku brakowało deterministycznej finalizacji. Oczywiście nadal musisz pamiętać o umieszczeniu go wszędzie, więc jest trochę ręczny, ale jest lepszy.
Dlaczego więc zrobili to, skoro od samego początku mogli automatycznie umieszczać semantykę w stylu przy użyciu każdego bloku zakresu? Prawdopodobnie wydajność, ale lubię myśleć, że po prostu nie zdawali sobie sprawy. Tak jak w końcu zdali sobie sprawę, że nadal potrzebujesz inteligentnych wskaźników w .NET (Google SafeHandle), pomyśleli, że GC naprawdę rozwiąże wszystkie problemy. Zapomnieli, że obiekt to coś więcej niż pamięć i że GC jest przede wszystkim zaprojektowany do zarządzania pamięcią. złapali się na pomysł, że GC sobie z tym poradzi i zapomnieli, że umieściłeś tam inne rzeczy, obiekt nie jest tylko kroplą pamięci, która nie ma znaczenia, jeśli nie usuniesz go przez jakiś czas.
Ale myślę też, że brak metody finalizacji w oryginalnej Javie miał coś więcej - że stworzone obiekty dotyczyły pamięci i jeśli chcesz usunąć coś innego (np. Uchwyt DB, gniazdo lub cokolwiek innego ), oczekiwano, że zrobisz to ręcznie .
Pamiętaj, że Java została zaprojektowana dla środowisk osadzonych, w których ludzie byli przyzwyczajeni do pisania kodu C z dużą ilością ręcznych alokacji, więc brak automatycznego bezpłatnego nie stanowił większego problemu - nigdy wcześniej tego nie robili, więc po co miałbyś go używać w Javie? Problem nie miał nic wspólnego z wątkami lub stosem / stertą, prawdopodobnie był po to, aby ułatwić przydzielanie pamięci (a tym samym cofanie alokacji). Podsumowując, instrukcja try / wreszcie jest prawdopodobnie lepszym miejscem do obsługi zasobów innych niż pamięć.
Więc IMHO, sposób, w jaki .NET po prostu skopiował największą wadę Javy, jest jej największą słabością. .NET powinien być lepszym C ++, a nie lepszą Javą.
źródło
Dispose
na wszystkich polach oznaczonychusing
dyrektywą i określający, czy ma onaIDisposable.Dispose
automatycznie wywoływać; (3) dyrektywa podobna dousing
, ale która wymagałaby jedynieDispose
w przypadku wyjątku; (4) której wariantIDisposable
wymagałbyException
parametru, i ...using
; parametr byłby taki,null
gdybyusing
blok wychodził normalnie, lub wskazywałby, jaki wyjątek czekał, gdyby wychodził przez wyjątek. Gdyby takie rzeczy istniały, znacznie łatwiej byłoby skutecznie zarządzać zasobami i unikać wycieków.Bruce Eckel, autor „Thinking in Java” i „Thinking in C ++” oraz członek C ++ Standards Committee jest zdania, że w wielu obszarach (nie tylko RAII) Gosling i zespół Java nie zrobili tego zadanie domowe.
źródło
Najlepszy powód jest znacznie prostszy niż większość odpowiedzi tutaj.
Nie możesz przekazywać obiektów przydzielonych stosowi do innych wątków.
Zatrzymaj się i pomyśl o tym. Myśl dalej ... Teraz C ++ nie miał wątków, kiedy wszyscy tak bardzo interesowali się RAII. Nawet Erlang (osobne sterty na wątek) staje się trudny, gdy mijasz zbyt wiele przedmiotów. C ++ ma tylko model pamięci w C ++ 2011; teraz możesz prawie argumentować o współbieżności w C ++ bez konieczności odwoływania się do „dokumentacji” kompilatora.
Java została zaprojektowana od (prawie) pierwszego dnia dla wielu wątków.
Nadal mam swoją starą kopię „The C ++ Programming language”, w której Stroustrup zapewnia mnie, że nie będę potrzebował nici.
Drugim bolesnym powodem jest unikanie krojenia.
źródło
W C ++ używasz bardziej ogólnych funkcji języka niższego poziomu (destruktory wywoływane automatycznie na obiektach opartych na stosie) do implementacji języka wyższego poziomu (RAII), a takie podejście wydaje się być czymś, czego ludzie C # / Java nie wydają się być zbyt lubi. Wolą projektować specjalne narzędzia wysokiego poziomu dla określonych potrzeb i udostępniać je programistom w postaci gotowej, wbudowanej w język. Problem z takimi konkretnymi narzędziami polega na tym, że często nie można ich dostosować (po części dlatego są tak łatwe do nauczenia się). Podczas budowania z mniejszych klocków z czasem może pojawić się lepsze rozwiązanie, ale jeśli masz tylko wbudowane konstrukcje wysokiego poziomu, jest to mniej prawdopodobne.
Więc tak, myślę (tak naprawdę mnie tam nie było ...) była to świadoma decyzja, której celem było ułatwienie wyboru języków, ale moim zdaniem była to zła decyzja. Z drugiej strony, generalnie wolę C ++ dać programistom szansę na wdrożenie własnej filozofii, więc jestem nieco stronniczy.
źródło
Dispose
Metodą tę wywołałeś już przybliżony odpowiednik tego w języku C # . Java też mafinalize
. UWAGA: Zdaję sobie sprawę, że finalizacja Javy jest niedeterministyczna i różni się odDispose
, po prostu wskazuję, że oboje mają metodę czyszczenia zasobów obok GC.Jeśli jednak C ++ stanie się bardziej uciążliwy, ponieważ obiekt musi zostać fizycznie zniszczony. W językach wyższego poziomu, takich jak C # i Java, polegamy na śmieciarzu, który wyczyści go, gdy nie będzie już żadnych odwołań do niego. Nie ma takiej gwarancji, że obiekt DBConnection w C ++ nie ma nieuczciwych odniesień ani wskaźników do niego.
Tak, kod C ++ może być bardziej intuicyjny w czytaniu, ale może być koszmarem do debugowania, ponieważ granice i ograniczenia, które wprowadzają języki takie jak Java, wykluczają niektóre bardziej irytujące i trudne błędy, a także chronią innych programistów przed typowymi błędami początkującymi.
Być może sprowadza się to do preferencji, niektóre z nich to na przykład niskopoziomowa moc, kontrola i czystość C ++, podczas gdy inni tacy jak ja wolą język piaskownicy, który jest o wiele bardziej wyraźny.
źródło