Gli sviluppatori di Java hanno abbandonato consapevolmente RAII?


82

Come programmatore C # di lunga data, di recente ho imparato di più sui vantaggi di Resource Acquisition Is Initialization (RAII). In particolare, ho scoperto che il linguaggio C #:

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

ha l'equivalente in C ++:

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

nel senso che ricordare di racchiudere l'uso di risorse come DbConnectionin un usingblocco non è necessario in C ++! Questo sembra un grande vantaggio di 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 implementare Foo IDisposablecome tale:

class Foo : IDisposable {
    DbConnection dbConn;

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

e quel che è peggio, ogni utente Foodovrebbe ricordarsi di racchiuderlo Fooin un usingblocco, 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 apprezzato appieno ciò che stavano abbandonando quando hanno abbandonato lo stack in favore dell'heap, abbandonando così RAII?

(Allo stesso modo, ha Stroustrup apprezzare appieno il significato di Raii?)


5
Non sono sicuro di cosa tu stia parlando non racchiudendo risorse in C ++. L'oggetto DBConnection probabilmente gestisce la chiusura di tutte le risorse nel suo distruttore.
maple_shaft

16
@maple_shaft, esattamente il mio punto! Questo è il vantaggio di C ++ che sto affrontando in questa domanda. In C # è necessario racchiudere le risorse in "utilizzo" ... in C ++ no.
JoelFan

12
La mia comprensione è che RAII, come strategia, è stato compreso solo una volta che i compilatori C ++ erano abbastanza buoni da utilizzare effettivamente il modello avanzato, che è ben dopo Java. il C ++ che era effettivamente disponibile per l'uso durante la creazione di Java era uno stile "C con classi" molto primitivo, con modelli forse di base, se si fosse fortunati.
Sean McMillan,

6
"La mia comprensione è che RAII, come strategia, è stato compreso solo una volta che i compilatori C ++ erano abbastanza buoni da utilizzare effettivamente il modello avanzato, che è ben dopo Java." - Non è proprio corretto. Costruttori e distruttori sono state le caratteristiche principali di C ++ sin dal primo giorno, ben prima dell'uso diffuso di modelli e ben prima di Java.
Jim In Texas,

8
@JimInTexas: Penso che Sean abbia un seme fondamentale di verità da qualche parte (anche se non i modelli ma le eccezioni è il punto cruciale). Costruttori / distruttori erano presenti sin dall'inizio, ma lì l'importanza e il concetto di RAII non erano inizialmente (qual è la parola che sto cercando) realizzati. Ci sono voluti alcuni anni e un po 'di tempo perché i compilatori diventassero buoni prima di renderci conto di quanto sia cruciale l'intera RAII.
Martin York,

Risposte:


38

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

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

Sono abbastanza sicuro che Gosling non abbia avuto il significato di RAII al momento in cui ha progettato Java. Nelle sue interviste ha spesso parlato delle ragioni per tralasciare i generici e il sovraccarico degli operatori, ma non ha mai menzionato distruttori deterministici e RAII.

Abbastanza divertente, anche Stroustrup non era consapevole dell'importanza dei distruttori deterministici al momento in cui li progettò. Non riesco a trovare la citazione, ma se ti piace davvero, puoi trovarla tra le sue interviste qui: http://www.stroustrup.com/interviews.html


11
@maple_shaft: in breve, non è possibile. Tranne se hai inventato un modo per avere una garbage collection deterministica (che sembra impossibile in generale, e in ogni caso annulla tutte le ottimizzazioni GC degli ultimi decenni), dovresti introdurre oggetti allocati nello stack, ma che apre diverse lattine di worm: questi oggetti necessitano di semantica, "problema di suddivisione" con sottotipizzazione (e quindi NESSUN polimorfismo), puntatori penzolanti, a meno che forse se non si impongono restrizioni significative su di esso o si apportano enormi modifiche al sistema di tipi incompatibili. E questo è appena fuori dalla mia testa.

13
@DeadMG: Quindi suggerisci di tornare alla gestione manuale della memoria. Questo è un approccio valido alla programmazione in generale e ovviamente consente la distruzione deterministica. Ma questo non risponde a questa domanda, che si occupa di un'impostazione solo per GC che vuole fornire sicurezza della memoria e comportamenti ben definiti anche se tutti agiamo come idioti. Ciò richiede GC per tutto e nessun modo per avviare manualmente la distruzione degli oggetti (e tutto il codice Java esistente si basa almeno sul primo), quindi o fai GC deterministico o sei sfortunato.

26
@delan. Non chiamerei manualgestione della memoria dei puntatori intelligenti C ++ . Sono più simili a un bidone della spazzatura controllabile deterministico a grana fine. Se usati correttamente, i puntatori intelligenti sono le ginocchia delle api.
Martin York,

10
@LokiAstari: Beh, direi che sono leggermente meno automatici del GC completo (devi pensare a quale tipo di intelligenza vuoi effettivamente) e implementarli come libreria richiede puntatori grezzi (e quindi gestione manuale della memoria) su cui basare . Inoltre, non sono a conoscenza di alcun puntatore intelligente che gestisca automaticamente i riferimenti ciclici, che è un requisito rigoroso per la raccolta dei rifiuti nei miei libri. I puntatori intelligenti sono certamente incredibilmente interessanti e utili, ma devi affrontare che non possono fornire alcune garanzie (che tu li consideri utili o meno) di un linguaggio GC completamente ed esclusivamente.

11
@delan: devo essere in disaccordo lì. Penso che siano più automatici di GC in quanto deterministici. OK. Per essere efficiente devi assicurarti di usare quello corretto (te lo darò io). std :: weak_ptr gestisce i cicli perfettamente bene. I cicli sono sempre tracciati, ma in realtà non è quasi mai un problema perché l'oggetto base è di solito basato su stack e quando ciò andrà a sistemare il resto. Per i rari casi in cui può essere un problema std :: weak_ptr.
Martin York,

60

Sì, i progettisti di C # (e, ne sono certo, Java) hanno deciso espressamente contro la finalizzazione deterministica. Ho chiesto ad Anders Hejlsberg di parlarne più volte tra il 1999 e il 2002.

In primo luogo, l'idea di una semantica diversa per un oggetto basata sul fatto che sia basata su stack o heap è certamente in contrasto con l'obiettivo di progettazione unificante di entrambi i linguaggi, che era quello di alleviare esattamente i programmatori di tali problemi.

In secondo luogo, anche se si riconosce che ci sono vantaggi, vi sono significative complessità di attuazione e inefficienze nella contabilità. Non puoi davvero mettere oggetti simili a stack nello stack in una lingua gestita. Ti resta da dire "semantica simile a uno stack" e impegnarti in un lavoro significativo (i tipi di valore sono già abbastanza difficili, pensa a un oggetto che è un'istanza di una classe complessa, con riferimenti che entrano e ritornano nella memoria gestita).

Per questo motivo, non si desidera la finalizzazione deterministica su ogni oggetto in un sistema di programmazione in cui "(quasi) tutto è un oggetto". Così si fa necessario introdurre una sorta di sintassi programmatore controllato per separare un oggetto normalmente monitorati da uno che ha finalizzazione deterministica.

In C #, hai la usingparola chiave, che è arrivata abbastanza tardi nella progettazione di quello che è diventato C # 1.0. Il tutto IDisposableè piuttosto disgraziato, e ci si chiede se sarebbe più elegante usinglavorare con la sintassi del distruttore C ++ che ~segna quelle classi a cui il IDisposablemodello della piastra della caldaia potrebbe essere applicato automaticamente?


2
Che dire di ciò che ha fatto C ++ / CLI (.NET), in cui gli oggetti sull'heap gestito hanno anche un "handle" basato su stack, che fornisce RIAA?
JoelFan

3
C ++ / CLI ha un insieme molto diverso di decisioni e vincoli di progettazione. Alcune di queste decisioni significano che dai programmatori puoi chiedere più attenzione all'allocazione della memoria e alle implicazioni sulle prestazioni: l'intero "dare loro abbastanza corda per impiccarsi" è un compromesso. E immagino che il compilatore C ++ / CLI sia considerevolmente più complesso di quello di C # (specialmente nelle sue prime generazioni).
Larry OBrien,

5
+1 questa è l'unica risposta corretta finora - è perché Java intenzionalmente non ha oggetti (non primitivi) basati su stack.
BlueRaja - Danny Pflughoeft

8
@Peter Taylor - giusto. Ma ritengo che il distruttore non deterministico di C # valga pochissimo, dal momento che non si può fare affidamento su di esso per gestire qualsiasi tipo di risorsa vincolata. Quindi, secondo me, sarebbe stato meglio usare la ~sintassi per essere zucchero sintattico perIDisposable.Dispose()
Larry OBrien,

3
@Larry: sono d'accordo. C ++ / CLI non usa ~come zucchero sintattico per IDisposable.Dispose(), ed è molto più conveniente che la sintassi C #.
dan04,

41

Tieni presente che Java è stato sviluppato nel 1991-1995 quando il C ++ era un linguaggio molto diverso. Eccezioni (che rendevano necessaria la RAII ) e modelli (che rendevano più semplice l'implementazione di puntatori intelligenti) erano caratteristiche "nuove". La maggior parte dei programmatori C ++ proveniva da C ed era abituata a gestire manualmente la memoria.

Quindi dubito che gli sviluppatori di Java abbiano deliberatamente deciso di abbandonare RAII. Tuttavia, è stata una decisione deliberata per Java preferire la semantica di riferimento anziché la semantica di valore. La distruzione deterministica è difficile da implementare in un linguaggio di semantica di riferimento.

Quindi perché usare la semantica di riferimento invece della semantica di valore?

Perché rende la lingua molto più semplice.

  • Non è necessaria una distinzione sintattica tra Fooe Foo*o tra foo.bare foo->bar.
  • Non è necessario un compito sovraccarico, quando tutto il compito è copiare un puntatore.
  • Non sono necessari costruttori di copie. ( Occasionalmente è necessaria una funzione di copia esplicita come clone(). Molti oggetti non devono essere copiati. Ad esempio, gli immutabili no.)
  • Non è necessario dichiarare i privatecostruttori di copie e operator=rendere una classe non copiabile. Se non vuoi copiare oggetti di una classe, non devi semplicemente scrivere una funzione per copiarla.
  • Non sono necessarie swapfunzioni. (A meno che tu non stia scrivendo una sorta di routine.)
  • Non sono necessari riferimenti al valore rx in stile C ++ 0x.
  • Non è necessario per (N) RVO.
  • Non ci sono problemi di taglio.
  • È più facile per il compilatore determinare i layout degli oggetti, poiché i riferimenti hanno una dimensione fissa.

Il principale svantaggio della semantica di riferimento è che quando ogni oggetto ha potenzialmente più riferimenti ad esso, diventa difficile sapere quando eliminarlo. È più o meno avere avere 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 successivamente aggiunto GC di tracciamento per gestire la spazzatura ciclica in cui i conteggi falliscono.

Ma il conteggio è orribilmente inefficiente. Molti cicli di CPU spesi per l'aggiornamento dei conteggi. Ancora peggio in un ambiente multi-thread (come il tipo per cui Java è stato progettato) in cui tali aggiornamenti devono essere sincronizzati. Molto meglio usare il Garbage Collector null fino a quando non è necessario passare a un altro.

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

Ma che dire degli "oggetti stack" C ++ / CLI?

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


3
Inoltre, bei collegamenti (in particolare il primo, che è molto rilevante per questa discussione) .
BlueRaja - Danny Pflughoeft

La usingdichiarazione gestisce bene molti problemi relativi alla pulizia, ma molti altri rimangono. Suggerirei che il giusto approccio per una lingua e un framework sarebbe di distinguere in modo dichiarativo tra posizioni di archiviazione che "possiedono" un riferimento IDisposablea quelle che non lo fanno; sovrascrivere o abbandonare una posizione di archiviazione che possiede un riferimento IDisposabledovrebbe eliminare l'obiettivo in assenza di una direttiva contraria.
supercat

1
"Non c'è bisogno di costruttori di copie" suona bene, ma in pratica fallisce male. java.util.Date e Calendar sono forse gli esempi più noti. Niente di più bello di new Date(oldDate.getTime()).
Kevin Cline,

2
iow RAII non è stato "abbandonato", semplicemente non esisteva per essere abbandonato :) Per quanto riguarda i costruttori di copie, non mi sono mai piaciuti, troppo facili da sbagliare, sono una fonte costante di mal di testa quando da qualche parte in fondo qualcuno (altro) ha dimenticato di fare una copia profonda, causando la condivisione di risorse tra le copie che non dovrebbero essere.
jwenting

20

Java7 ha introdotto qualcosa di simile a C # using: L'istruzione try-with-resources

una trydichiarazione che dichiara una o più risorse. Una risorsa è come un oggetto che deve essere chiuso al termine del programma. L' tryistruzione -with-resources assicura che ogni risorsa sia chiusa alla fine dell'istruzione. Qualsiasi oggetto che implementa java.lang.AutoCloseable, che include tutti gli oggetti che implementano java.io.Closeable, può essere utilizzato come risorsa ...

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


Interessante, ma sembra che funzioni solo con oggetti implementati java.lang.AutoCloseable. Probabilmente non è un grosso problema, ma non mi piace come questo sembra in qualche modo limitato. Forse ho qualche altro oggetto che dovrebbe essere rilasciato automaticamente, ma è semanticamente strano renderlo implementare AutoCloseable...
FrustratedWithFormsDesigner

9
@Patrick: Ehm, quindi? usingnon è lo stesso di RAII: in un caso il chiamante si preoccupa di disporre le risorse, nell'altro caso la chiamata lo gestisce.
BlueRaja - Danny Pflughoeft

1
+1 Non sapevo di provare con le risorse; dovrebbe essere utile per scaricare più boilerplate.
giovedì

3
-1 per using/ try-with-resources non uguale a RAII.
Sean McMillan,

4
@Sean: concordato. usinge non è da nessuna parte vicino a RAII.
DeadMG

18

Java non ha intenzionalmente oggetti basati su stack (aka oggetti valore). Questi sono necessari per la distruzione automatica dell'oggetto alla fine del metodo.

A causa di ciò e del fatto che Java viene raccolto in modo inutile, la finalizzazione deterministica è più o meno impossibile (es. E se il mio oggetto "locale" venisse referenziato da qualche altra parte? Quindi quando il metodo termina, non vogliamo che venga distrutto ) .

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


Perché Java non ha oggetti basati su stack?

(Altro che primitivi ..)

Perché 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 basato su stack locale, viene chiamato il costruttore di copia (se il risultato è assegnato a qualcosa).
  • Se myObjectè un oggetto basato su stack locale e stiamo restituendo un riferimento, il risultato non è definito.
  • Se myObjectè un membro / oggetto globale, viene chiamato il costruttore di copia (se il risultato è assegnato a qualcosa).
  • Se myObjectè un membro / oggetto globale e stiamo restituendo 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, quel puntatore viene restituito.
  • Se myObjectè un puntatore a un oggetto basato su heap, viene restituito quel puntatore.

Cosa fa lo stesso codice in Java?

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

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


6
Non so che cosa intendi con "non ha senso in RAII" ... Penso che intendi "non c'è possibilità di fornire RAII in Java" ... RAII è indipendente da qualsiasi lingua ... non lo è diventa "inutile" perché 1 lingua particolare non lo fornisce
JoelFan

4
Questo non è un motivo valido. Un oggetto non deve effettivamente vivere nello stack per utilizzare RAII basato su stack. Se esiste una cosa come un "riferimento unico", il distruttore può essere sparato una volta uscito dal campo di applicazione. Vedi ad esempio, come funziona con il linguaggio di programmazione D: d-programming-language.org/exception-safe.html
Nemanja Trifunovic

3
@Nemanja: un oggetto non deve vivere nello stack per avere una semantica basata sullo stack, e non l'ho mai detto. Ma non è questo il problema; il problema, come ho già detto, è la stessa semantica basata su stack.
BlueRaja - Danny Pflughoeft

4
@Aaronaught: Il diavolo è in "quasi sempre" e "il più delle volte". Se non chiudi la tua connessione db e la lasci al GC per attivare il finalizzatore, funzionerà perfettamente con i test unitari e si interromperà gravemente quando distribuito in produzione. La pulizia deterministica è importante indipendentemente dalla lingua.
Nemanja Trifunovic,

8
@NemanjaTrifunovic: Perché test di unità su una connessione al database live? Non è proprio un test unitario. No, scusa, non lo sto comprando. Non dovresti comunque creare connessioni DB ovunque, dovresti passarle attraverso costruttori o proprietà, e ciò significa che non vuoi semantiche di autodistruzione di tipo stack. Pochissimi oggetti che dipendono da una connessione al database dovrebbero effettivamente possederlo. Se la pulizia non deterministica ti sta mordendo così spesso, così difficile, allora è a causa della cattiva progettazione delle applicazioni, non della cattiva progettazione del linguaggio.
Aaronaught,

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 sia RAII che GC era una cattiva idea. Quando si tratta di chiudere i file in Java, è malloc()e free()lì.


2
Concordo sul fatto che RAII è il ginocchio delle api. Ma la usingclausola è un grande passo avanti per C # su Java. Permette la distruzione deterministica e quindi la corretta gestione delle risorse (non è tanto buono quanto RAII quanto devi ricordare di farlo, ma è sicuramente una buona idea).
Martin York,

8
"Quando si tratta di chiudere i file in Java, è malloc () e free () laggiù." - Assolutamente.
Konrad Rudolph,

9
@KonradRudolph: è peggio di malloc e gratuito. Almeno in C non hai eccezioni.
Nemanja Trifunovic,

1
@Nemanja: Cerchiamo di essere onesti, è possibile free()in finally.
DeadMG

4
@Loki: il problema della classe base è molto più importante come problema. Ad esempio, l'originale IEnumerablenon ha ereditato IDisposablee c'erano un sacco di iteratori speciali che non potevano mai essere implementati di conseguenza.
DeadMG

14

Sono piuttosto vecchio. Ci sono stato, l'ho visto e ci ho sbattuto la testa molte volte.

Ero a una conferenza a Hursley Park in cui i ragazzi IBM ci dicevano quanto fosse meraviglioso questo nuovissimo linguaggio Java, solo qualcuno ha chiesto ... perché non c'è un distruttore per questi oggetti. Non intendeva la cosa che conosciamo come distruttore in C ++, ma non c'era neanche un finalizzatore (o aveva finalizzatori ma sostanzialmente non funzionavano). Questo è molto indietro, e abbiamo deciso che Java era un po 'un linguaggio giocattolo a quel punto.

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

Naturalmente, in seguito a tutti è stato detto di non mettere i finalizzatori sui propri oggetti perché rallentava enormemente il GC. (poiché doveva non solo bloccare l'heap, ma spostare gli oggetti da finalizzare in un'area temporanea, poiché questi metodi non potevano essere chiamati poiché il GC ha messo in pausa l'app in esecuzione. Invece sarebbero stati chiamati immediatamente prima del successivo Ciclo GC) (e peggio, a volte il finalizzatore non veniva mai chiamato affatto quando l'app si chiudeva. Immagina di non avere mai il tuo file handle chiuso, mai)

Poi abbiamo avuto C # e ricordo il forum di discussione su MSDN in cui ci è stato detto quanto fosse meraviglioso questo nuovo linguaggio C #. Qualcuno ha chiesto perché non ci fosse una finalizzazione deterministica e i ragazzi della SM ci hanno detto come 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 fosse fantastico GC e come erano tutte le nostre vecchie app spazzatura e mai lavorato a causa di tutti i riferimenti circolari. Quindi hanno ceduto alla pressione e ci hanno detto che avevano aggiunto questo schema IDispose alle specifiche che potevamo usare. A quel punto ho pensato che fosse praticamente tornato alla gestione manuale della memoria nelle app C #.

Naturalmente, i ragazzi della SM in seguito hanno scoperto che tutto quello che ci avevano detto era ... beh, hanno fatto di 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 mancava dopo tutto alla lingua. Certo, devi ancora ricordarti di metterlo ovunque, quindi è ancora un po 'manuale, ma è meglio.

Quindi perché l'hanno fatto quando avrebbero potuto disporre automaticamente della semantica in stile utilizzo su ogni blocco dell'ambito dall'inizio? Probabilmente efficienza, ma mi piace pensare che non se ne siano resi conto. Proprio come alla fine si sono resi conto 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 è molto più di una semplice memoria e che GC è progettato principalmente per gestire la gestione della memoria. sono rimasti intrappolati nell'idea che il GC avrebbe gestito questo, e hanno dimenticato che hai messo 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 nella Java originale avesse un po 'di più: che gli oggetti che hai creato riguardassero solo la memoria e se volessi eliminare qualcos'altro (come un handle DB o un socket o altro ), quindi ti aspettavi di farlo manualmente .

Ricorda che Java è stato progettato per ambienti embedded in cui le persone erano abituate a scrivere codice C con molte allocazioni manuali, quindi non avere il free automatico non era un problema: non l'avevano mai fatto prima, quindi perché dovresti averne bisogno in Java? Il problema non aveva nulla a che fare con i thread, o stack / heap, probabilmente era proprio lì per rendere un po 'più semplice l'allocazione della memoria (e quindi la de-allocazione). Nel complesso, l'istruzione try / finally è probabilmente un posto migliore per gestire 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.


IMHO, cose come "usare" i blocchi sono l'approccio giusto per la pulizia deterministica, ma sono necessarie anche alcune altre cose: (1) un mezzo per garantire che gli oggetti vengano eliminati se i loro distruttori generano un'eccezione; (2) un mezzo per generare automaticamente un metodo di routine per richiamare Disposetutti i campi contrassegnati con una usingdirettiva e specificare se IDisposable.Disposedeve chiamarlo automaticamente; (3) una direttiva simile using, ma che richiederebbe solo Disposein caso di eccezione; (4) una variante del IDisposablequale richiederebbe un Exceptionparametro e ...
supercat

... che verrebbero utilizzati automaticamente usingse necessario; il parametro sarebbe nullse il usingblocco uscisse normalmente, altrimenti indicherebbe quale eccezione era in sospeso se fosse uscita tramite eccezione. Se esistessero cose del genere, sarebbe molto più semplice gestire le risorse in modo efficace ed evitare perdite.
supercat

11

Bruce Eckel, autore di "Thinking in Java" e "Thinking in C ++" e membro del C ++ Standards Committee, è del parere che, in molte aree (non solo RAII), Gosling e il team Java non abbiano fatto il loro compiti a casa.

... Per capire in che modo il linguaggio può essere sia spiacevole e complicato, sia ben progettato allo stesso tempo, è necessario tenere presente la decisione progettuale principale su cui pende tutto in C ++: la compatibilità con C. Stroustrup ha deciso - e correttamente così , sembrerebbe - che il modo per far spostare le masse di programmatori C su oggetti fosse rendere trasparente lo spostamento: per consentire loro di compilare il loro codice C invariato in C ++. Questo è stato un enorme vincolo, ed è sempre stato il più grande punto di forza del C ++ ... e la sua rovina. È ciò che ha reso C ++ tanto efficace quanto complesso e complesso.

Ha anche ingannato i designer Java che non capivano abbastanza bene il C ++. Ad esempio, hanno pensato che il sovraccarico dell'operatore fosse troppo difficile da usare per i programmatori. Ciò è fondamentalmente vero in C ++, poiché C ++ ha sia allocazione di stack che allocazione di heap e devi sovraccaricare i tuoi operatori per gestire tutte le situazioni e non causare perdite di memoria. Difficile davvero. Java, tuttavia, ha un unico meccanismo di allocazione della memoria e un garbage collector, che rende banale il sovraccarico dell'operatore - come è stato mostrato in C # (ma era già stato mostrato in Python, che ha preceduto Java). Ma per molti anni, la linea in parte del team Java è stata "Il sovraccarico dell'operatore è troppo complicato". Questa e molte altre decisioni in cui qualcuno chiaramente non ha

Ci sono molti altri esempi. I primitivi "dovevano essere inclusi per efficienza". La risposta giusta è rimanere fedeli a "tutto è un oggetto" e fornire una botola per fare attività di livello inferiore quando era richiesta efficienza (ciò avrebbe anche permesso alle tecnologie di hotspot di rendere trasparentemente le cose più efficienti, come avrebbero fatto avere). Oh, e il fatto che non è possibile utilizzare direttamente il processore a virgola mobile per calcolare le funzioni trascendentali (invece è fatto nel software). Ho scritto su questioni come questa per quanto posso sopportare, e la risposta che sento è sempre stata una risposta tautologica all'effetto che "questo è il modo di Java".

Quando ho scritto di come sono stati progettati i generici male, ho ottenuto la stessa risposta, insieme a "dobbiamo essere retrocompatibili con le precedenti (cattive) decisioni prese in Java". Ultimamente sempre più persone hanno acquisito sufficiente esperienza con Generics per vedere che sono davvero molto difficili da usare - in effetti, i modelli C ++ sono molto più potenti e coerenti (e molto più facili da usare ora che i messaggi di errore del compilatore sono tollerabili). Le persone hanno persino preso sul serio la reificazione - qualcosa che sarebbe utile ma che non metterà molto a dura prova in un progetto che è paralizzato da vincoli autoimposti.

L'elenco prosegue fino al punto in cui è solo noioso ...


5
Sembra una risposta Java contro C ++, piuttosto che concentrarsi su RAII. Penso che C ++ e Java siano linguaggi diversi, ognuno con i suoi punti di forza e di debolezza. Inoltre, i progettisti del C ++ non hanno svolto i compiti in molte aree (principio KISS non applicato, meccanismo di importazione semplice per le classi mancanti, ecc.). Ma il focus della domanda era RAII: questo manca in Java e bisogna programmarlo manualmente.
Giorgio,

4
@Giorgio: Il punto dell'articolo è che Java sembra aver perso la barca su una serie di problemi, alcuni dei quali riguardano direttamente RAII. Per quanto riguarda il C ++ e il suo impatto su Java, Eckels osserva: "Devi tenere presente la decisione di progettazione primaria su cui tutto in C ++ pesa: la compatibilità con C. Questo è stato un enorme vincolo, ed è sempre stato il più grande punto di forza del C ++ ... e il suo bane. Ha anche ingannato i designer Java che non capivano abbastanza bene il C ++ ". Il design del C ++ ha influenzato direttamente Java, mentre C # ha avuto l'opportunità di imparare da entrambi. (Se lo ha fatto è un'altra domanda.)
Gnawme

2
@Giorgio Studiare le lingue esistenti in un particolare paradigma e famiglia linguistica è davvero una parte dei compiti necessari per lo sviluppo di nuove lingue. Questo è un esempio in cui l'hanno semplicemente soffiato con Java. Avevano C ++ e Smalltalk da guardare. C ++ non aveva Java da guardare quando è stato sviluppato.
Jeremy,

1
@Gnawme: "Java sembra aver perso la barca su una serie di problemi, alcuni dei quali direttamente correlati a RAII": puoi menzionarli? L'articolo che hai pubblicato non menziona RAII.
Giorgio,

2
@Giorgio Certo, ci sono state innovazioni dallo sviluppo del C ++ che spiegano molte delle caratteristiche che ti mancano. C'è una di quelle caratteristiche che avrebbero dovuto trovare guardando i linguaggi stabiliti prima dello sviluppo del C ++? Questo è il tipo di compiti di cui stiamo parlando con Java - non c'è motivo per loro di non prendere in considerazione tutte le funzionalità C ++ nello sviluppo di Java. Ad alcuni piace l'eredità multipla che sono stati intenzionalmente esclusi - altri come RAII sembrano aver trascurato.
Jeremy,

10

Il miglior motivo è molto più semplice della maggior parte delle risposte qui.

Non è possibile passare oggetti allocati in pila ad altri thread.

Fermati e pensaci. Continuate a pensare .... Ora C ++ non aveva discussioni quando tutti erano così entusiasti di RAII. Anche Erlang (cumuli separati per thread) diventa icky quando passi troppi oggetti in giro. 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) primo giorno 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 è quello di evitare l'affettatura.


1
Java progettato per più thread spiega anche perché il GC non si basa sul conteggio dei riferimenti.
dan04,

4
@NemanjaTrifunovic: non puoi confrontare C ++ / CLI con Java o C #, è stato progettato quasi allo scopo esplicito di interagire con codice C / C ++ non gestito; è più simile a un linguaggio non gestito che dà accesso al framework .NET che viceversa.
Aaronaught l'

3
@NemanjaTrifunovic: Sì, C ++ / CLI è un esempio di come può essere fatto in modo totalmente inappropriato per le normali applicazioni . È utile solo per l'interoperabilità C / C ++. Non solo gli sviluppatori normali non dovrebbero dover essere sellati con una decisione "stack o heap" del tutto irrilevante, ma se si tenta di effettuare il refactoring, è banalmente facile creare accidentalmente un errore nullo di puntatore / riferimento e / o una perdita di memoria. Ci dispiace, ma devo chiedermi se tu abbia mai programmato in realtà in Java o C #, perché non credo che chiunque abbia effettivamente desiderato la semantica utilizzata in C ++ / CLI.
Aaronaught,

2
@Aaronaught: ho programmato sia con Java (un po ') che con C # (molto) e il mio progetto attuale è praticamente tutto in C #. Credetemi, so di cosa sto parlando, e non ha nulla a che fare con "stack vs. heap" - ha tutto a che fare con la garanzia che tutte le vostre risorse vengano rilasciate non appena non ne avete bisogno. Automaticamente. Se non lo sono - si sarà nei guai.
Nemanja Trifunovic,

4
@NemanjaTrifunovic: È fantastico, davvero fantastico, ma sia C # che C ++ / CLI richiedono che tu dichiari esplicitamente quando vuoi che ciò accada, usano semplicemente una sintassi diversa. Nessuno sta contestando il punto essenziale su cui stai vagando (che "le risorse vengono rilasciate non appena non ne hai bisogno") ma stai facendo un balzo logico gigantesco a "tutte le lingue gestite dovrebbero avere solo automatico -sort di eliminazione deterministica basata su stack di chiamate ". Semplicemente non trattiene l'acqua.
Aaronaught,

5

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

Quindi sì, penso (in realtà non ero lì ...) è stata una decisione consapevole, con l'obiettivo di rendere le lingue più facili da imparare, ma secondo me è stata una decisione sbagliata. Poi di nuovo, in genere preferisco la filosofia C ++ dare ai programmatori una possibilità di lanciarsi, quindi sono un po 'di parte.


7
La "filosofia-dà-ai-programmatori-una-possibilità-di-rotolare-loro" funziona benissimo FINO a quando è necessario combinare librerie scritte da programmatori che hanno ciascuno arrotolato le proprie classi di stringhe e puntatori intelligenti.
dan04,

@ dan04 quindi le lingue gestite che ti danno classi di stringhe predefinite, quindi ti permettono di correggerle con le scimmie, che è una ricetta per il disastro se sei il tipo di ragazzo che non riesce a far fronte a una stringa diversa classe.
gbjbaanb

-1

Hai già chiamato l'equivalente approssimativo di questo in C # con il Disposemetodo. Anche Java ha finalize. NOTA: mi rendo conto che la finalizzazione di Java non è deterministica e diversa da quella Dispose, sto solo sottolineando che entrambi hanno un metodo di pulizia delle risorse insieme al GC.

Semmai il C ++ diventa più un dolore perché un oggetto deve essere distrutto fisicamente. In linguaggi di livello superiore come C # e Java dipendiamo da un garbage collector per ripulirlo quando non ci sono più riferimenti ad esso. Non esiste tale 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 da debug perché i confini e le limitazioni messi in atto da linguaggi come Java escludono alcuni dei bug più aggravanti e difficili, oltre a proteggere altri sviluppatori da comuni errori da principiante.

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


12
Prima di tutto Java di "finalizzare" è non-deterministico ... è non è l'equivalente di C # 's 'smaltire' o di C ++' distruttori s ... anche, C ++ ha anche un garbage collector se si utilizza .NET
JoelFan

2
@DeadMG: Il problema è che potresti non essere un idiota, ma potrebbe essere stato quell'altro ragazzo che ha appena lasciato la società (e che ha scritto il codice che ora gestisci).
Kevin,

7
Quel ragazzo scriverà codice di merda qualunque cosa tu faccia. Non puoi prendere un programmatore cattivo e fargli scrivere un buon codice. I suggerimenti ciondolanti sono l'ultima delle mie preoccupazioni quando si tratta di idioti. I buoni standard di codifica utilizzano i puntatori intelligenti per la memoria che deve essere tracciata, quindi la gestione intelligente dovrebbe rendere ovvio come disallocare in modo sicuro e accedere alla memoria.
DeadMG

3
Cosa ha detto DeadMG. Ci sono molte cose cattive su C ++. Ma RAII non è uno di questi da un lungo tratto. In effetti, la mancanza di Java e .NET per tenere correttamente conto della gestione delle risorse (perché la memoria è l'unica risorsa, giusto?) È uno dei loro maggiori problemi.
Konrad Rudolph,

8
Il finalizzatore secondo me è un progetto di disastro saggio. Mentre stai forzando il corretto utilizzo di un oggetto dal progettista all'utente (non in termini di gestione della memoria ma gestione delle risorse). In C ++ è responsabilità del progettista della classe ottenere una corretta gestione delle risorse (eseguita una sola volta). In Java è responsabilità dell'utente della classe ottenere una corretta gestione delle risorse e quindi deve essere eseguita ogni volta che la classe utilizzata. stackoverflow.com/questions/161177/…
Martin York,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.