Gli sviluppatori di Java hanno abbandonato coscientemente RAII?

80

In qualità di programmatore C # di lunga data, recentemente sono venuto a sapere di più sui vantaggi di L'acquisizione delle risorse è inizializzata (RAII). In particolare, ho scoperto che l'idioma C #:

using (var dbConn = new DbConnection(connStr)) {
    // do stuff with dbConn
}

ha l'equivalente C ++:

{
    DbConnection dbConn(connStr);
    // do stuff with dbConn
}

significa che ricordare di racchiudere l'uso di risorse come DbConnection in un blocco using non è necessario in C ++! Questo sembra un vantaggio importante del C ++. Ciò è ancora più convincente se si considera una classe che ha un membro di istanza di tipo DbConnection , ad esempio

class Foo {
    DbConnection dbConn;

    // ...
}

In C # avrei bisogno di avere Foo implementare IDisposable come tale:

class Foo : IDisposable {
    DbConnection dbConn;

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

e quel che è peggio, ogni utente di Foo dovrebbe ricordare di racchiudere Foo in un blocco using , come:

   using (var foo = new Foo()) {
       // do stuff with "foo"
   }

Ora guardando C # e le sue radici Java mi chiedo ... gli sviluppatori di Java hanno pienamente apprezzato ciò che stavano abbandonando quando hanno abbandonato lo stack a favore dell'heap, abbandonando così RAII?

(Allo stesso modo, Stroustrup ha pienamente apprezzato il significato di RAII?)

    
posta JoelFan 07.11.2011 - 15:45
fonte

11 risposte

38

Now looking at C# and its Java roots I am wondering... did the developers of Java fully appreciate what they were giving up when they abandoned the stack in favor of the heap, thus abandoning RAII?

(Similarly, did Stroustrup fully appreciate the significance of RAII?)

Sono abbastanza sicuro che Gosling non abbia capito il significato di RAII al momento in cui ha progettato Java. Nelle sue interviste ha parlato spesso delle ragioni per cui ha lasciato fuori i generici e il sovraccarico dell'operatore, ma non ha mai menzionato i distruttori deterministici e RAII.

Abbastanza divertente, anche Stroustrup non era consapevole dell'importanza dei distruttori deterministici nel momento in cui li progettò. Non riesco a trovare la citazione, ma se sei veramente interessato, puoi trovarlo tra le sue interviste qui: link

    
risposta data 07.11.2011 - 16:41
fonte
59

Sì, i progettisti di C # (e, sono sicuro, Java) hanno deciso specificamente contro la finalizzazione deterministica. Ho chiesto ad Anders Hejlsberg di parlare di questo più volte nel periodo 1999-2002.

In primo luogo, l'idea di semantica diversa per un oggetto in base al fatto che il suo stack o heap basato è certamente in contrasto con l'obiettivo di progettazione unificante di entrambe le lingue, che era quello di alleviare i programmatori esattamente di tali problemi.

In secondo luogo, anche se riconosci che ci sono dei vantaggi, ci sono significative complessità di implementazione e inefficienze coinvolte nella contabilità. Non puoi veramente mettere nello stack oggetti di tipo stack in una lingua gestita. Si rimane con il dire "semantica pila-like" e si impegnano in un lavoro significativo (i tipi di valore sono già abbastanza difficili, si pensi a un oggetto che è un'istanza di una classe complessa, con riferimenti che entrano e ritornano nella memoria gestita).

Per questo motivo, non vuoi la finalizzazione deterministica su ogni oggetto in un sistema di programmazione in cui "(quasi) tutto è un oggetto." Quindi do deve introdurre una sorta di sintassi controllata dal programmatore per separare un oggetto normalmente tracciato da uno che ha una finalizzazione deterministica.

In C #, hai la parola chiave using , che è arrivata abbastanza tardi nella progettazione di ciò che è diventato C # 1.0. L'intera IDisposable thing è piuttosto miserabile, e ci si chiede se sarebbe più elegante avere using a lavorare con la sintassi del distruttore di C ++ ~ che contrassegna quelle classi a cui il pattern boiler-plate IDisposable potrebbe essere applicato automaticamente ?

    
risposta data 07.11.2011 - 19:43
fonte
40

Ricorda che Java è stato sviluppato nel 1991-1995 quando il linguaggio C ++ era molto diverso. Le eccezioni (che rendevano RAII necessario ) e i modelli (che rendevano più semplice l'implementazione di puntatori intelligenti) erano funzionalità "nuove-fangled". La maggior parte dei programmatori C ++ provenivano da C e venivano utilizzati per la gestione manuale della memoria.

Quindi dubito che gli sviluppatori di Java abbiano deliberatamente deciso di abbandonare RAII. Era, tuttavia, una decisione deliberata per Java di preferire la semantica di riferimento invece della semantica del valore. La distruzione deterministica è difficile da attuare in un linguaggio semantico di riferimento.

Allora perché usare la semantica di riferimento invece della semantica del valore?

Perché rende la lingua molto più semplice.

  • Non è necessaria una distinzione sintattica tra Foo e Foo* o tra foo.bar e foo->bar .
  • Non è necessario per l'assegnazione sovraccaricata, quando tutto il compito è copiare un puntatore.
  • Non sono necessari costruttori di copie. (C'è occasionalmente una necessità per una funzione di copia esplicita come clone() . Molti oggetti non devono essere copiati, ad esempio non immutabili.
  • Non è necessario dichiarare private copy constructors e operator= per rendere una classe noncopyable. Se non vuoi che gli oggetti di una classe copiati, semplicemente non scrivi una funzione per copiarla.
  • Non sono necessarie le funzioni swap . (A meno che tu non stia scrivendo una sorta di routine.)
  • Non sono necessari riferimenti razionali in stile C ++ 0x.
  • Non è necessario (N) RVO.
  • Non c'è alcun problema di affettatura.
  • È più semplice per il compilatore determinare i layout degli oggetti, perché i riferimenti hanno una dimensione fissa.

Lo svantaggio principale della semantica di riferimento è che quando ogni oggetto ha potenzialmente riferimenti multipli a esso, diventa difficile sapere quando eliminarlo. hai hai praticamente la gestione automatica della memoria.

Java ha scelto di utilizzare un garbage collector non deterministico.

GC non può essere deterministico?

Sì, può. Ad esempio, l'implementazione C di Python utilizza il conteggio dei riferimenti. E in seguito ha aggiunto la traccia GC per gestire la spazzatura ciclica in caso di fallimento degli account.

Ma il conteggio è orribilmente inefficiente. Un sacco di cicli della CPU ha speso l'aggiornamento dei conteggi. Ancora peggio in un ambiente multi-thread (come il tipo per cui è stato progettato Java) in cui è necessario sincronizzare tali aggiornamenti. Molto meglio usare il garbage collector null finché non è necessario passare a un altro.

Potresti dire che Java ha scelto di ottimizzare il caso comune (memoria) a scapito di risorse non fungibili come file e socket. Oggi, alla luce dell'adozione di RAII in C ++, questa potrebbe sembrare la scelta sbagliata. Ma ricorda che gran parte del pubblico di riferimento per Java erano programmatori C (o "C con classi") che erano abituati a chiudere esplicitamente queste cose.

Ma per quanto riguarda gli "oggetti stack" C ++ / CLI?

Sono solo sintattici zucchero per Dispose ( link originale ), proprio come C # using . Tuttavia, non risolve il problema generale della distruzione deterministica, perché puoi creare un gcnew FileStream("filename.ext") anonimo e C ++ / CLI non lo eliminerà automaticamente.

    
risposta data 08.11.2011 - 06:51
fonte
20

Java7 ha introdotto qualcosa di simile a C # using : Dichiarazione try-with-resources

a try statement that declares one or more resources. A resource is as an object that must be closed after the program is finished with it. The try-with-resources statement ensures that each resource is closed at the end of the statement. Any object that implements java.lang.AutoCloseable, which includes all objects which implement java.io.Closeable, can be used as a resource...

Quindi immagino che non abbiano scelto consapevolmente di non implementare RAII o che nel frattempo abbiano cambiato idea.

    
risposta data 07.11.2011 - 16:52
fonte
18

Java intenzionalmente non ha oggetti basati su stack (cioè oggetti valore). Questi sono necessari per far sì che l'oggetto venga automaticamente distrutto alla fine del metodo in questo modo.

Per questo motivo e per il fatto che Java è un sistema di raccolta dei dati, la finalizzazione deterministica è più o meno impossibile (ad esempio, se il mio oggetto "locale" viene referenziato da qualche altra parte?). non voglio che venga distrutto) .

Tuttavia, questo va bene con la maggior parte di noi, perché non c'è quasi mai un bisogno per la finalizzazione deterministica, eccetto quando si interagisce con risorse native (C ++)!

Perché Java non ha oggetti basati su stack?

(Oltre ai primitivi ..)

Poiché gli oggetti basati su stack hanno una semantica diversa rispetto ai riferimenti basati su heap. Immagina il seguente codice in C ++; che cosa fa?

return myObject;
  • Se myObject è un oggetto locale basato su stack, viene chiamato il costruttore di copie (se il risultato è assegnato a qualcosa).
  • Se myObject è un oggetto locale basato su stack e stiamo restituendo un riferimento, il risultato non è definito.
  • Se myObject è un membro / oggetto globale, viene chiamato il costruttore della copia (se il risultato è assegnato a qualcosa).
  • Se myObject è un membro / oggetto globale e restituiamo un riferimento, viene restituito il riferimento.
  • Se myObject è un puntatore a un oggetto basato su stack locale, il risultato non è definito.
  • Se myObject è un puntatore a un membro / oggetto globale, viene restituito quel puntatore.
  • Se myObject è un puntatore a un oggetto basato sull'heap, quel puntatore viene restituito.

Ora cosa fa lo stesso codice in Java?

return myObject;
  • Viene restituito il riferimento a myObject . Non importa se la variabile è locale, membro o globale; e non ci sono oggetti basati su stack o casi di puntatori di cui preoccuparsi.

Quanto sopra mostra perché gli oggetti basati su stack sono una causa molto comune degli errori di programmazione in C ++. Per questo motivo, i designer Java li hanno tolti; e senza di essi, non ha senso usare RAII in Java.

    
risposta data 07.11.2011 - 21:50
fonte
17

La tua descrizione dei buchi di using è incompleta. Considera il seguente problema:

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

Bar b = new Foo();

// Where's the Dispose?

Secondo me, non avere RAII e GC era una cattiva idea. Quando si tratta di chiudere i file in Java, è malloc() e free() laggiù.

    
risposta data 07.11.2011 - 16:43
fonte
14

Sono piuttosto vecchio. Sono stato lì e l'ho visto e mi ha sbattuto la testa molte volte.

Ero ad una conferenza a Hursley Park, dove i ragazzi dell'IBM ci dicevano quanto fosse meraviglioso questo nuovo linguaggio Java, solo qualcuno chiedeva ... perché non c'è un distruttore per questi oggetti. Non intendeva la cosa che conosciamo come un distruttore in C ++, ma c'era anche nessun finalizzatore (o aveva finalizzatori ma in pratica non funzionavano). Questo è molto tempo fa e abbiamo deciso che Java era un po 'un linguaggio giocattolo a quel punto.

ora hanno aggiunto Finalisers alle specifiche del linguaggio e Java ne ha visto l'adozione.

Naturalmente, più tardi è stato detto a tutti di non mettere i finalizzatori sui loro oggetti perché rallentava enormemente il GC. (poiché non doveva solo bloccare l'heap ma spostare gli oggetti finalizzati su un'area temporanea, poiché questi metodi non potevano essere chiamati in quanto il GC ha messo in pausa l'esecuzione dell'applicazione, ma sarebbero stati chiamati immediatamente prima del prossimo Ciclo GC) (e peggio, a volte il finalizzatore non verrebbe mai chiamato quando l'app si chiudeva. Immagina di non avere il tuo handle di handle chiuso, mai)

Poi abbiamo avuto C #, e ricordo il forum di discussione su MSDN dove ci è stato detto quanto fosse meraviglioso questo nuovo linguaggio in C #. Qualcuno ha chiesto perché non ci fosse una finalizzazione deterministica ei ragazzi della SM ci hanno detto che non avevamo bisogno di queste cose, poi ci hanno detto che dovevamo cambiare il nostro modo di progettare le app, poi ci hanno detto quanto GC fosse incredibile e come tutte le nostre vecchie app fossero spazzatura e mai lavorato a causa di tutti i riferimenti circolari. Poi cedettero alla pressione e ci dissero che avevano aggiunto questo schema IDispose alle specifiche che potevamo usare. A quel punto pensavo che fosse più tardi la gestione manuale della memoria per noi nelle app C #.

Ovviamente, i ragazzi della MS hanno scoperto in seguito che tutto quello che ci avevano detto era ... beh, hanno reso IDispose un po 'più di una semplice interfaccia standard, e in seguito hanno aggiunto l'istruzione using. W00t! Si sono resi conto che la finalizzazione deterministica era, in fondo, una cosa che mancava alla lingua. Certo, devi ancora ricordarti di metterlo ovunque, quindi è ancora un po 'manuale, ma è meglio.

Quindi perché lo hanno fatto quando avrebbero potuto usare semantica dello stile di utilizzo posizionata automaticamente su ogni blocco di scope dall'inizio? Probabilmente l'efficienza, ma mi piace pensare che semplicemente non se ne sono resi conto. Proprio come alla fine hanno capito che hai ancora bisogno di puntatori intelligenti in .NET (google SafeHandle) hanno pensato che il GC avrebbe davvero risolto tutti i problemi. Hanno dimenticato che un oggetto è più di una semplice memoria e che GC è stato progettato principalmente per gestire la gestione della memoria. sono stati presi dall'idea che il GC avrebbe gestito questo, e si sono dimenticati di mettere altre cose lì dentro, un oggetto non è solo un blob di memoria che non importa se non lo elimini per un po '.

Ma penso anche che la mancanza di un metodo finalize nell'originale Java avesse un po 'di più in esso - che gli oggetti che hai creato riguardassero interamente la memoria, e se volessi cancellare qualcos'altro (come un handle di DB o un socket o qualsiasi altra cosa) ci si aspettava che lo facessero manualmente .

Ricordate che Java è stato progettato per ambienti embedded in cui le persone erano abituate a scrivere codice C con molte allocazioni manuali, quindi non avere automaticamente il free non era un grosso problema - non l'avevano mai fatto prima, quindi perché ne avreste bisogno Giava? Il problema non era niente a che fare con i thread, o stack / heap, probabilmente era solo lì per rendere l'allocazione della memoria (e quindi de-allocare) un po 'più facile. In generale, l'istruzione try / finally è probabilmente un posto migliore per gestire le risorse non di memoria.

Quindi IMHO, il modo in cui .NET ha semplicemente copiato il più grande difetto di Java è la sua più grande debolezza. .NET avrebbe dovuto essere un C ++ migliore, non un Java migliore.

    
risposta data 20.03.2012 - 17:36
fonte
11

Bruce Eckel, autore di "Thinking in Java" e "Thinking in C ++" e membro del C ++ Standards Committee, è dell'opinione che, in molte aree (non solo RAII), Gosling e il team Java non ha fatto i compiti.

...To understand how the language can be both unpleasant and complicated, and well designed at the same time, you must keep in mind the primary design decision upon which everything in C++ hung: compatibility with C. Stroustrup decided -- and correctly so, it would appear -- that the way to get the masses of C programmers to move to objects was to make the move transparent: to allow them to compile their C code unchanged under C++. This was a huge constraint, and has always been C++'s greatest strength ... and its bane. It's what made C++ as successful as it was, and as complex as it is.

It also fooled the Java designers who didn't understand C++ well enough. For example, they thought operator overloading was too hard for programmers to use properly. Which is basically true in C++, because C++ has both stack allocation and heap allocation and you must overload your operators to handle all situations and not cause memory leaks. Difficult indeed. Java, however, has a single storage allocation mechanism and a garbage collector, which makes operator overloading trivial -- as was shown in C# (but had already been shown in Python, which predated Java). But for many years, the partly line from the Java team was "Operator overloading is too complicated." This and many other decisions where someone clearly didn't do their homework is why I have a reputation for disdaining many of the choices made by Gosling and the Java team.

There are plenty of other examples. Primitives "had to be included for efficiency." The right answer is to stay true to "everything is an object" and provide a trap door to do lower-level activities when efficiency was required (this would also have allowed for the hotspot technologies to transparently make things more efficient, as they eventually would have). Oh, and the fact that you can't use the floating point processor directly to calculate transcendental functions (it's done in software instead). I've written about issues like this as much as I can stand, and the answer I hear has always been some tautological reply to the effect that "this is the Java way."

When I wrote about how badly generics were designed, I got the same response, along with "we must be backwards compatible with previous (bad) decisions made in Java." Lately more and more people have gained enough experience with Generics to see that they really are very hard to use -- indeed, C++ templates are much more powerful and consistent (and much easier to use now that compiler error messages are tolerable). People have even been taking reification seriously -- something that would be helpful but won't put that much of a dent in a design that is crippled by self-imposed constraints.

The list goes on to the point where it's just tedious...

    
risposta data 07.11.2011 - 20:00
fonte
10

La migliore ragione è molto più semplice della maggior parte delle risposte qui.

Non puoi passare gli oggetti allocati allo stack ad altri thread.

Fermati e pensaci. Continua a pensare .... Ora il C ++ non aveva thread quando tutti erano così entusiasti di RAII. Persino Erlang (heap separati per thread) diventa icky quando passi in giro troppi oggetti. C ++ ha ottenuto solo un modello di memoria in C ++ 2011; ora puoi quasi ragionare sulla concorrenza in C ++ senza dover fare riferimento alla "documentazione" del tuo compilatore.

Java è stato progettato dal (quasi) giorno uno per più thread.

Ho ancora la mia vecchia copia di "Il linguaggio di programmazione C ++" in cui Stroustrup mi assicura che non avrò bisogno di thread.

Il secondo motivo doloroso è evitare l'affettatura.

    
risposta data 07.11.2011 - 22:25
fonte
5

In C ++, usi più funzioni di linguaggio di livello generale e di livello inferiore (i distruttori invocano automaticamente oggetti basati su stack) per implementare uno di livello superiore (RAII), e questo approccio è qualcosa che la gente di C # / Java sembra non essere troppo affezionato. Preferiscono invece progettare specifici strumenti di alto livello per esigenze specifiche e fornirli ai programmatori già pronti, integrati nella lingua. Il problema con strumenti così specifici è che spesso sono impossibili da personalizzare (in parte questo è ciò che li rende così facili da imparare). Quando si costruiscono da blocchi più piccoli, una soluzione migliore può andare in giro con il tempo, mentre se solo hanno costrutti incorporati di alto livello, questo è meno probabile.

Quindi sì, penso (non ero in realtà lì ...) è stata una decisione consapevole, con l'obiettivo di rendere più facili le lingue, ma secondo me è stata una decisione sbagliata. Poi di nuovo, generalmente preferisco la filosofia del c ++ dare-i-programmatori-una-possibilità-di-roll-loro, quindi sono un po 'di parte.

    
risposta data 07.11.2011 - 17:25
fonte
-1

Hai già definito l'equivalente approssimativo di questo in C # con il metodo Dispose . Java ha anche finalize . NOTA: Mi rendo conto che la finalizzazione di Java non è deterministica e diversa da Dispose , sto solo facendo notare che entrambi hanno un metodo per pulire le risorse accanto al GC.

Se qualcosa C ++ diventa più un dolore, perché un oggetto deve essere fisicamente distrutto. Nei linguaggi di livello superiore come C # e Java dipendiamo da un garbage collector per ripulirlo quando non ci sono più riferimenti ad esso. Non vi è alcuna garanzia che l'oggetto DBConnection in C ++ non abbia riferimenti o puntatori non autorizzati.

Sì, il codice C ++ può essere più intuitivo da leggere ma può essere un incubo per il debug perché i limiti e le limitazioni che linguaggi come Java mettono in atto escludono alcuni dei bug più aggravanti e difficili e proteggono anche altri sviluppatori dal comune errori da principiante.

Forse si tratta di preferenze, alcune come la potenza di basso livello, il controllo e la purezza del C ++, dove altri come me preferiscono un linguaggio più sandbox che è molto più esplicito.

    
risposta data 07.11.2011 - 16:06
fonte

Leggi altre domande sui tag