Czy twórcy oprogramowania Java świadomie porzucili RAII?

82

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 DbConnectionw usingbloku 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 IDisposablejako taką:

class Foo : IDisposable {
    DbConnection dbConn;

    public void Dispose()
    {       
        dbConn.Dispose();
    }
}

i co gorsza, każdy użytkownik Foomusiałby pamiętać o zamknięciu Foow usingbloku, 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?)

JoelFan
źródło
5
Nie jestem pewien, o czym mówisz, nie zamykając zasobów w C ++. Obiekt DBConnection prawdopodobnie obsługuje zamykanie wszystkich zasobów w swoim destruktorze.
wałek klonowy
16
@maple_shaft, dokładnie mój punkt! To jest zaleta C ++, o której mówię w tym pytaniu. W C # musisz ująć zasoby w „za pomocą” ... w C ++ nie.
JoelFan,
12
Rozumiem, że strategia RAII została zrozumiana dopiero wtedy, gdy kompilatory C ++ były na tyle dobre, że faktycznie używały zaawansowanego szablonowania, co jest znacznie późniejsze niż Java. C ++, że faktycznie nie był dostępny do użycia, gdy Java został stworzony był bardzo prymitywny, „C z klasami” stylu, ze może podstawowych szablonów, jeśli mieli szczęście.
Sean McMillan,
6
„Rozumiem, że strategia RAII została zrozumiana dopiero wtedy, gdy kompilatory C ++ były na tyle dobre, że faktycznie używały zaawansowanego szablonów, co jest bardzo dobre od Java”. - To nie do końca prawda. Konstruktorzy i niszczyciele są podstawowymi funkcjami C ++ od samego początku, na długo przed powszechnym użyciem szablonów i na długo przed Javą.
Jim In Texas
8
@JimInTexas: Myślę, że Sean ma gdzieś podstawowe ziarno prawdy (choć nie szablony, ale wyjątki to sedno). Konstruktory / Niszczyciele istniały od samego początku, ale znaczenie i koncepcja RAII nie była początkowo (jak tego słowa szukam) zrealizowana. Kompilacja kompilatorów zajęła kilka lat, zanim zdaliśmy sobie sprawę, jak ważna jest cała RAII.
Martin York,

Odpowiedzi:

38

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?)

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

Nemanja Trifunovic
źródło
11
@maple_shaft: Krótko mówiąc, nie jest to możliwe. Z wyjątkiem przypadku, gdy wymyśliłeś sposób na deterministyczne odśmiecanie pamięci (co wydaje się w ogóle niemożliwe, i w każdym razie unieważnia wszystkie optymalizacje GC z ostatnich dziesięcioleci), musiałbyś wprowadzić obiekty alokowane na stosie, ale to otwiera kilka puszek robaki: obiekty te wymagają semantyki, „problemu krojenia” z podtytowaniem (a zatem NIE polimorfizmu), zwisających wskaźników, chyba że nałożysz na niego znaczące ograniczenia lub wprowadzisz ogromne niezgodne zmiany w systemie typów. I to jest tuż nad moją głową.
13
@DeadMG: Sugerujesz, abyśmy wrócili do ręcznego zarządzania pamięcią. Jest to ogólnie właściwe podejście do programowania i oczywiście umożliwia deterministyczne zniszczenie. Ale to nie odpowiada na to pytanie, które dotyczy samego ustawienia GC, które chce zapewnić bezpieczeństwo pamięci i dobrze zdefiniowane zachowanie, nawet jeśli wszyscy zachowujemy się jak idioci. To wymaga GC do wszystkiego i nie ma możliwości ręcznego rozpoczęcia niszczenia obiektów (a cały istniejący kod Java opiera się przynajmniej na tym pierwszym), więc albo determinujesz GC, albo nie masz szczęścia.
26
@delan. Nie nazwałbym manualzarzą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ół.
Martin York,
10
@LokiAstari: Cóż, powiedziałbym, że są nieco mniej automatyczne niż pełna GC (musisz pomyśleć o tym, jakiego rodzaju inteligencji naprawdę chcesz) i wdrożenie ich, ponieważ biblioteka wymaga surowych wskaźników (a zatem i ręcznego zarządzania pamięcią), aby móc . Poza tym nie znam żadnego inteligentnego wskaźnika, który nie obsługuje automatycznie cyklicznych odwołań, co jest ścisłym wymogiem w przypadku śmieci w moich książkach. Inteligentne wskaźniki są z pewnością niesamowicie fajne i przydatne, ale musisz stawić czoła, że ​​nie mogą dać żadnych gwarancji (niezależnie od tego, czy uważasz je za przydatne, czy nie) w pełni i wyłącznie w języku GC.
11
@delan: Muszę się z tym nie zgodzić. Myślę, że są bardziej automatyczne niż GC, ponieważ są deterministyczne. DOBRZE. Aby być wydajnym, musisz upewnić się, że używasz właściwego (dam ci to). std :: poor_ptr doskonale radzi sobie z cyklami. Cykle są zawsze usuwane, ale w rzeczywistości prawie nigdy nie stanowi to problemu, ponieważ obiekt podstawowy jest zwykle oparty na stosie, a gdy to nastąpi, resztę uporządkuje. W rzadkich przypadkach może to stanowić problem std :: poor_ptr.
Martin York,
60

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 usingsłowo kluczowe, które pojawiło się dość późno w projektowaniu tego, co stało się C # 1.0. Cała IDisposablesprawa jest dość nieszczęsna i zastanawia się, czy bardziej elegancko byłoby usingpracować ze składnią C ++ destruktora ~oznaczającą te klasy, do których IDisposablewzorzec płyty kotłowej mógłby zostać automatycznie zastosowany?

Larry OBrien
źródło
2
Co z tym, co zrobił C ++ / CLI (.NET), gdzie obiekty na zarządzanej stercie mają również „uchwyt” oparty na stosie, który zapewnia RIAA?
JoelFan,
3
C ++ / CLI ma zupełnie inny zestaw decyzji projektowych i ograniczeń. Niektóre z tych decyzji oznaczają, że możesz wymagać więcej uwagi na temat alokacji pamięci i wpływu na wydajność od programistów: całe „dać im wystarczająco dużo liny, aby się zawiesić” kompromis. I wyobrażam sobie, że kompilator C ++ / CLI jest znacznie bardziej złożony niż kompilator C # (szczególnie we wczesnych generacjach).
Larry OBrien
5
+1 to jedyna jak dotąd poprawna odpowiedź - to dlatego, że Java celowo nie ma (nieprymitywnych) obiektów opartych na stosie.
BlueRaja - Danny Pflughoeft
8
@Peter Taylor - racja. Ale uważam, że niedeterministyczny destruktor C # jest bardzo mało wart, ponieważ nie można polegać na nim, aby zarządzać jakimkolwiek ograniczonym zasobem. Tak więc, moim zdaniem, lepiej byłoby użyć ~składni jako cukru syntaktycznegoIDisposable.Dispose()
Larry OBrien
3
@ Larry: Zgadzam się. C ++ / CLI nie używać ~jako cukier składniowej dla IDisposable.Dispose(), i to o wiele wygodniejsze niż składni C #.
dan04,
41

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.

  • Nie ma potrzeby syntaktycznego rozróżnienia między Fooi Foo*lub pomiędzy foo.bari foo->bar.
  • Nie ma potrzeby przeciążonego przypisania, gdy jedynym przypisaniem jest skopiowanie wskaźnika.
  • Nie ma potrzeby tworzenia konstruktorów kopii. ( Czasami istnieje potrzeba jawnej funkcji kopiowania, takiej jak clone(). Wiele obiektów po prostu nie musi być kopiowanych. Na przykład, niezmienne nie.)
  • Nie ma potrzeby deklarowania privatekonstruktorów kopiowania i operator=uczynienia klasy niemożliwą do kopiowania. Jeśli nie chcesz kopiować obiektów klasy, po prostu nie piszesz funkcji, aby je skopiować.
  • swapFunkcje nie są potrzebne . (Chyba że piszesz rutynę sortowania.)
  • Nie ma potrzeby odwoływania się do wartości w stylu C ++ 0x.
  • Nie ma potrzeby (N) RVO.
  • Nie ma problemu z krojeniem.
  • Kompilatorowi łatwiej jest ustalić układ obiektów, ponieważ odwołania mają stały rozmiar.

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 dlaDispose ( 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.

dan04
źródło
3
Również miłe linki (szczególnie ten pierwszy, który jest bardzo istotny w tej dyskusji) .
BlueRaja - Danny Pflughoeft
To usingoś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, a IDisposabletymi, które nie są; nadpisanie lub porzucenie miejsca przechowywania, do którego IDisposablenależy odnośnik, powinno zlikwidować cel w przypadku braku przeciwnej dyrektywy.
supercat
1
„Nie potrzeba konstruktorów kopii” brzmi nieźle, ale w praktyce źle się kończy. java.util.Date i Calendar to chyba najbardziej znane przykłady. Nic piękniejszego niż new Date(oldDate.getTime()).
kevin cline
2
iow RAII nie zostało „porzucone”, po prostu nie istniało porzucenie :) Jeśli chodzi o kopiowanie konstruktorów, nigdy ich nie lubiłem, zbyt łatwo się pomylić, są stałym źródłem bólu głowy, gdy gdzieś głęboko w kimś (else) zapomniałem zrobić głębokiej kopii, powodując, że zasoby są dzielone między kopie, które nie powinny być.
jwenting
20

Java7 wprowadziła coś podobnego do C # using: Instrukcja try-with-resources

tryoświadczenie, że deklaruje jeden lub więcej zasobów. Zasób jest jako obiekt, który musi zostać zamknięty po zakończeniu programu z nim. Instrukcja try-with-resources zapewnia, że ​​każdy zasób jest zamykany na końcu instrukcji. Każdy obiekt, który implementuje java.lang.AutoCloseable, w tym wszystkie obiekty, które implementują java.io.Closeable, może być używany jako zasób ...

Sądzę więc, że albo nie świadomie zdecydowali się nie wdrażać RAII, albo zmienili zdanie.

Patrick
źródło
Ciekawe, ale wygląda na to, że działa tylko z obiektami, które się implementują 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...
FrustratedWithFormsDesigner
9
@Patrick: Er, więc? usingto nie to samo, co RAII - w jednym przypadku dzwoniący martwi się o pozbycie się zasobów, w drugim przypadku odbiorca go obsługuje.
BlueRaja - Danny Pflughoeft
1
+1 Nie wiedziałem o próbach z zasobami; powinno być przydatne w zrzucaniu większej ilości płyty kotłowej.
jprete,
3
-1 dla using/ try-with-resources nie jest taki sam jak RAII.
Sean McMillan
4
@Sean: Uzgodniony. usingi to nie jest nigdzie w pobliżu RAII.
DeadMG
18

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?

return myObject;
  • Jeśli myObjectjest to lokalny obiekt oparty na stosie, wywoływany jest konstruktor kopii (jeśli wynik jest do czegoś przypisany).
  • Jeśli myObjectjest to lokalny obiekt oparty na stosie i zwracamy referencję, wynik jest niezdefiniowany.
  • Jeśli myObjectjest to element członkowski / globalny, wywoływany jest konstruktor kopii (jeśli wynik jest przypisany do czegoś).
  • Jeśli myObjectjest to element członkowski / globalny i zwracamy referencję, referencja jest zwracana.
  • Jeśli myObjectjest wskaźnikiem do lokalnego obiektu opartego na stosie, wynik jest niezdefiniowany.
  • Jeśli myObjectjest wskaźnikiem do elementu członkowskiego / globalnego, ten wskaźnik jest zwracany.
  • Jeśli myObjectjest wskaźnikiem do obiektu opartego na stercie, wskaźnik ten jest zwracany.

Co teraz robi ten sam kod w Javie?

return myObject;
  • Odwołanie myObjectjest 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.

BlueRaja - Danny Pflughoeft
źródło
6
Nie wiem, co masz na myśli przez „nie ma sensu w RAII” ... Myślę, że masz na myśli „nie ma możliwości zapewnienia RAII w Javie” ... RAII jest niezależna od jakiegokolwiek języka ... nie ma stają się „bezcelowe”, ponieważ jeden konkretny język tego nie zapewnia
JoelFan,
4
To nie jest prawidłowy powód. Obiekt nie musi tak naprawdę żyć na stosie, aby używać RAII opartego na stosie. Jeśli istnieje coś takiego jak „unikalne odniesienie”, destruktor może zostać wystrzelony, gdy wyłączy się z zakresu. Zobacz na przykład, jak to działa z językiem programowania D: d-programming-language.org/exception-safe.html
Nemanja Trifunovic
3
@Nemanja: Obiekt nie musi żyć na stosie, aby mieć semantykę opartą na stosie, i nigdy nie powiedziałem, że tak. Ale to nie jest problem; problemem, jak wspomniałem, jest sama semantyka oparta na stosie.
BlueRaja - Danny Pflughoeft
4
@Aaronaught: Diabeł jest w „prawie zawsze” i „przez większość czasu”. Jeśli nie zamkniesz połączenia db i nie pozostawisz go GC w celu wyzwolenia finalizatora, będzie on działał dobrze z twoimi testami jednostkowymi i ulegnie poważnemu zerwaniu po wdrożeniu w środowisku produkcyjnym. Deterministyczne porządki są ważne bez względu na język.
Nemanja Trifunovic
8
@NemanjaTrifunovic: Dlaczego przeprowadzasz testy jednostkowe połączenia z bazą danych na żywo? To nie jest tak naprawdę test jednostkowy. Nie, przepraszam, nie kupuję tego. I tak nie powinieneś tworzyć połączeń DB wszędzie, powinieneś przekazywać je przez konstruktory lub właściwości, a to oznacza, że nie chcesz semantyki autodestrukcji podobnej do stosu. Bardzo niewiele obiektów, które zależą od połączenia z bazą danych, powinno faktycznie być jego właścicielem. Jeśli niedeterministyczne porządkowanie gryzie cię tak często, tak mocno, to z powodu złego projektu aplikacji, a nie złego projektu języka.
Aaronaught
17

Twój opis otworów usingjest niepełny. Rozważ następujący problem:

interface Bar {
    ...
}
class Foo : Bar, IDisposable {
    ...
}

Bar b = new Foo();

// Where's the Dispose?

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()i free()tam.

DeadMG
źródło
2
Zgadzam się, że RAII to kolana pszczół. Ale usingklauzula 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).
Martin York,
8
„Jeśli chodzi o zamykanie plików w Javie, jest tam malloc () i free ()”. - Oczywiście.
Konrad Rudolph,
9
@KonradRudolph: Jest gorszy niż Malloc i darmowy. Przynajmniej w C nie masz wyjątków.
Nemanja Trifunovic
1
@Nemanja: Bądźmy uczciwi, możesz free()w finally.
DeadMG,
4
@Loki: Problem z klasą podstawową jest znacznie ważniejszy jako problem. Na przykład oryginał IEnumerablenie odziedziczył po nim IDisposable, a było kilka specjalnych iteratorów, które nigdy nie mogły zostać zaimplementowane.
DeadMG,
14

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ą.

gbjbaanb
źródło
IMHO, takie jak „używanie” bloków jest właściwym podejściem do deterministycznego porządkowania, ale potrzeba jeszcze kilku innych rzeczy: (1) sposób zapewnienia, że ​​obiekty zostaną unieszkodliwione, jeśli ich destruktory zgłoszą wyjątek; (2) sposób automatycznego generowania rutynowej metody wywoływania Disposena wszystkich polach oznaczonych usingdyrektywą i określający, czy ma ona IDisposable.Disposeautomatycznie wywoływać; (3) dyrektywa podobna do using, ale która wymagałaby jedynie Disposew przypadku wyjątku; (4) której wariant IDisposablewymagałby Exceptionparametru, i ...
supercat
... które w razie potrzeby byłyby wykorzystywane automatycznie using; parametr byłby taki, nullgdyby usingblok 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.
supercat
11

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.

... Aby zrozumieć, w jaki sposób język może być zarówno nieprzyjemny, jak i skomplikowany, a jednocześnie dobrze zaprojektowany, musisz pamiętać o podstawowej decyzji projektowej, na której opierało się wszystko w C ++: zgodność z C. Stroustrup - i poprawnie tak , wygląda na to - że sposobem na zmuszenia mas programistów C do przejścia do obiektów było uczynienie tego ruchu przezroczystym: umożliwienie im skompilowania kodu C bez zmian w C ++. To było ogromne ograniczenie i zawsze była największą siłą C ++ ... i jego zmorą. To właśnie sprawiło, że C ++ był tak samo udany, jak i tak złożony.

Oszukało także projektantów Java, którzy nie rozumieli wystarczająco dobrze C ++. Na przykład uznali, że przeciążenie operatora jest zbyt trudne, aby programiści mogli z niego prawidłowo korzystać. Co jest w zasadzie prawdą w C ++, ponieważ C ++ ma zarówno alokację stosu, jak i alokację sterty i musisz przeciążać operatorów, aby obsłużyć wszystkie sytuacje i nie powodować przecieków pamięci. Rzeczywiście trudne. Jednak Java ma pojedynczy mechanizm alokacji pamięci i moduł wyrzucania elementów bezużytecznych, co sprawia, że ​​przeciążanie operatora jest banalne - jak pokazano w C # (ale już pokazano w Pythonie, który był wcześniejszy niż Java). Ale przez wiele lat częściowo napisano w zespole Java: „Przeciążenie operatora jest zbyt skomplikowane”. Ta i wiele innych decyzji, w których ktoś wyraźnie nie zrobił

Istnieje wiele innych przykładów. Prymitywy „musiały zostać uwzględnione ze względu na wydajność”. Właściwą odpowiedzią jest pozostanie wiernym „wszystko jest przedmiotem” i zapewnienie drzwi pułapkowych do wykonywania działań na niższym poziomie, gdy wymagana jest wydajność (pozwoliłoby to również technologiom hotspot na przejrzyste zwiększenie wydajności, ponieważ ostatecznie mieć, posiadać). Aha i fakt, że nie można bezpośrednio używać procesora zmiennoprzecinkowego do obliczania funkcji transcendentalnych (zamiast tego odbywa się to w oprogramowaniu). Pisałem o takich sprawach jak tylko mogę, a odpowiedź, którą słyszę, zawsze była jakąś tautologiczną odpowiedzią na to, że „to jest Java”.

Kiedy pisałem o tym, jak źle zaprojektowano generyczne, otrzymałem tę samą odpowiedź, wraz z „musimy być kompatybilni wstecz z poprzednimi (złymi) decyzjami podjętymi w Javie”. Ostatnio coraz więcej osób ma wystarczające doświadczenie z Generics, aby przekonać się, że naprawdę są bardzo trudne w użyciu - w rzeczywistości szablony C ++ są znacznie bardziej wydajne i spójne (i znacznie łatwiejsze w użyciu teraz, gdy komunikaty o błędach kompilatora są tolerowane). Ludzie nawet poważnie podchodzą do refikacji - coś, co byłoby pomocne, ale nie zaszkodzi aż tak bardzo projektowi, który jest okaleczony przez narzucone ograniczenia.

Lista przechodzi do punktu, w którym jest po prostu nudna ...

Gryźć
źródło
5
To brzmi jak odpowiedź Java kontra C ++, zamiast koncentrować się na RAII. Myślę, że C ++ i Java są różnymi językami, z których każdy ma swoje mocne i słabe strony. Również projektanci C ++ nie odrobili pracy domowej w wielu obszarach (nie zastosowano zasady KISS, brak prostego mechanizmu importu dla klas itp.). Ale w centrum uwagi było RAII: brakuje jej w Javie i trzeba ją zaprogramować ręcznie.
Giorgio
4
@Giorgio: Istotą tego artykułu jest to, że Java zdaje się przegapić łódź w wielu kwestiach, z których niektóre odnoszą się bezpośrednio do RAII. Jeśli chodzi o C ++ i jego wpływ na Javę, Eckels zauważa: „Musisz pamiętać o podstawowej decyzji projektowej, na której opierało się wszystko w C ++: kompatybilność z C. To było ogromne ograniczenie i zawsze była największą siłą C ++… i jego zmora. Oszukało także projektantów Java, którzy nie rozumieli wystarczająco dobrze C ++. ” Projekt C ++ miał bezpośredni wpływ na Javę, podczas gdy C # miał okazję uczyć się od obu. (Czy to zrobiło, to kolejne pytanie.)
Gnawme,
2
@Giorgio Badanie istniejących języków w konkretnym paradygmacie i rodzinie językowej jest rzeczywiście częścią pracy domowej wymaganej do opracowania nowego języka. To jest jeden przykład, w którym po prostu sfingowali go z Javą. Mieli C ++ i Smalltalk do obejrzenia. C ++ nie miał Java do obejrzenia, kiedy został opracowany.
Jeremy,
1
@Gnawme: „Wygląda na to, że Java przeoczyła łódkę w wielu kwestiach, z których niektóre odnoszą się bezpośrednio do RAII”: czy możesz wspomnieć o tych problemach? W opublikowanym artykule nie ma wzmianki o RAII.
Giorgio
2
@Giorgio Pewnie, od czasu rozwoju C ++ wprowadzono innowacje, które odpowiadają za wiele funkcji, których tam brakuje. Czy są jakieś funkcje, które powinni znaleźć, patrząc na języki utworzone przed opracowaniem C ++? Jest to rodzaj pracy domowej, o której mówimy w Javie - nie ma powodu, aby nie brać pod uwagę każdej funkcji C ++ w rozwoju Javy. Niektórzy lubią wielokrotne dziedzictwo, które celowo pominęli - inni, jak RAII, wydają się przeoczyć.
Jeremy,
10

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.

Tim Williscroft
źródło
1
Java zaprojektowana dla wielu wątków wyjaśnia również, dlaczego GC nie opiera się na zliczaniu referencji.
dan04
4
@NemanjaTrifunovic: Nie można porównywać C ++ / CLI z Javą lub C #, został zaprojektowany prawie wyłącznie w celu wyraźnej współpracy z niezarządzanym kodem C / C ++; jest bardziej jak niezarządzany język, który daje dostęp do frameworku .NET niż odwrotnie.
Aaronaught
3
@NemanjaTrifunovic: Tak, C ++ / CLI jest jednym z przykładów tego, jak można to zrobić w sposób całkowicie nieodpowiedni dla normalnych aplikacji . To tylko użyteczne dla C / C ++ współdziałanie. Nie tylko zwykli programiści nie muszą być obarczeni całkowicie nieistotną decyzją „stosu lub sterty”, ale jeśli kiedykolwiek spróbujesz to zmienić, to banalnie łatwo jest przypadkowo stworzyć zerowy błąd wskaźnika / odniesienia i / lub wyciek pamięci. Przepraszam, ale muszę się zastanawiać, czy kiedykolwiek programowałeś w Javie lub C #, ponieważ nie sądzę, aby ktokolwiek chciał, aby semantyka była używana w C ++ / CLI.
Aaronaught,
2
@Aaronaught: Programowałem zarówno z Javą (trochę), jak i C # (dużo), a mój obecny projekt jest prawie w całości C #. Uwierz mi, wiem o czym mówię i nie ma to nic wspólnego z „stosem kontra stosem” - ma wszystko wspólnego z upewnieniem się, że wszystkie twoje zasoby zostaną uwolnione, gdy tylko ich nie będziesz potrzebować. Automatycznie. Jeśli nie są one - ty będziesz miał kłopoty.
Nemanja Trifunovic
4
@NemanjaTrifunovic: To wspaniale, naprawdę wspaniale, ale zarówno C #, jak i C ++ / CLI wymagają, abyś wyraźnie określił, kiedy to się dzieje, po prostu używają innej składni. Nikt nie kwestionuje zasadniczego punktu, o którym się obecnie bawi (że „zasoby są uwalniane, gdy tylko ich nie potrzebujesz”), ale robisz gigantyczny logiczny skok do „wszystkich zarządzanych języków powinno się automatycznie, ale tylko -sort-of deterministyczne usuwanie oparte na stosie wywołań ". Po prostu nie zatrzymuje wody.
Aaronaught,
5

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.

imre
źródło
7
„Daj-programiści-szansę-rzucić-swoją-własną filozofię” działa dobrze, dopóki nie musisz łączyć bibliotek napisanych przez programistów, z których każdy opracował własne klasy ciągów znaków i inteligentne wskaźniki.
dan04
@ dan04, więc zarządzane języki, które dają ci wstępnie zdefiniowane klasy ciągów, a następnie pozwalają małpować ich łatanie, co jest receptą na katastrofę, jeśli jesteś typem faceta, który nie poradzi sobie z innym własnym ciągiem klasa.
gbjbaanb
-1

DisposeMetodą tę wywołałeś już przybliżony odpowiednik tego w języku C # . Java też ma finalize. UWAGA: Zdaję sobie sprawę, że finalizacja Javy jest niedeterministyczna i różni się od Dispose, 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.

wałek klonowy
źródło
12
Przede wszystkim Java „sfinalizować” nie jest deterministyczny ... to nie odpowiednik C # „s«wyrzucać»lub C ++” destruktory s ... również, C ++ ma również śmieciarza jeśli używasz .NET
JoelFan
2
@DeadMG: Problem polega na tym, że możesz nie być idiotą, ale mógł to być inny facet, który właśnie opuścił firmę (i który napisał kod, który teraz utrzymujesz).
Kevin,
7
Ten facet napisze gówniany kod, cokolwiek zrobisz. Nie możesz wziąć złego programisty i zmusić go do napisania dobrego kodu. Zwisające wskazówki są najmniejsze z moich zmartwień w kontaktach z idiotami. Dobre standardy kodowania wykorzystują inteligentne wskaźniki pamięci, która musi być śledzona, dlatego inteligentne zarządzanie powinno wyraźnie uświadomić, jak bezpiecznie przydzielić pamięć i uzyskać do niej dostęp.
DeadMG,
3
Co powiedział DeadMG. W C ++ jest wiele złych rzeczy. Ale RAII nie jest jednym z nich na dłuższą metę. W rzeczywistości brak Java i .NET do prawidłowego uwzględnienia zarządzania zasobami (ponieważ pamięć jest jedynym zasobem, prawda?) Jest jednym z ich największych problemów.
Konrad Rudolph,
8
Moim zdaniem finalizator jest zaprojektowany pod kątem katastrofy. Gdy wymuszasz prawidłowe użycie obiektu od projektanta do użytkownika obiektu (nie w zakresie zarządzania pamięcią, ale zarządzania zasobami). W C ++ zadaniem projektanta klasy jest poprawne zarządzanie zasobami (wykonywane tylko raz). W Javie obowiązkiem użytkownika klasy jest prawidłowe zarządzanie zasobami, dlatego należy to robić za każdym razem, gdy klasa, z której korzystaliśmy. stackoverflow.com/questions/161177/…
Martin York