Perché il paradigma del distruttore di oggetti nelle immondizie raccolte pervasivamente è assente?


27

Alla ricerca di informazioni dettagliate sulle decisioni relative al design del linguaggio raccolto dall'immondizia. Forse un esperto di lingue potrebbe illuminarmi? Vengo da uno sfondo C ++, quindi questa area è sconcertante per me.

Sembra che quasi tutti i moderni linguaggi di immondizia raccolti con il supporto di oggetti OOPy come Ruby, Javascript / ES6 / ES7, Actionscript, Lua, ecc. Omettono completamente il paradigma distruttore / finalista. Python sembra essere l'unico con il suo class __del__()metodo. Perchè è questo? Esistono limiti funzionali / teorici all'interno delle lingue con la garbage collection automatica che impediscono implementazioni efficaci di un metodo di distruzione / finalizzazione sugli oggetti?

Trovo estremamente carente che queste lingue considerino la memoria come l' unica risorsa che valga la pena di essere gestita. Che dire di socket, handle di file, stati delle applicazioni? Senza la possibilità di implementare la logica personalizzata per ripulire le risorse non di memoria e gli stati sulla finalizzazione degli oggetti, sono tenuto a sporcare la mia applicazione con myObject.destroy()chiamate di stile personalizzate , posizionando la logica di pulizia fuori dalla mia "classe", rompendo i tentativi di incapsulamento e relegando il mio applicazione a perdite di risorse dovute a errori umani piuttosto che essere gestite automaticamente da gc.

Quali sono le decisioni di progettazione del linguaggio che portano a queste lingue non avere alcun modo di eseguire la logica personalizzata sullo smaltimento degli oggetti? Devo immaginare che ci sia una buona ragione. Mi piacerebbe capire meglio le decisioni tecniche e teoriche che hanno portato questi linguaggi a non avere supporto per la distruzione / finalizzazione degli oggetti.

Aggiornare:

Forse un modo migliore per formulare la mia domanda:

Perché un linguaggio dovrebbe avere il concetto integrato di istanze di oggetto con strutture di classe o di classe insieme a un'istanza personalizzata (costruttori), ma omettere completamente la funzionalità di distruzione / finalizzazione? Le lingue che offrono la raccolta automatica dei rifiuti sembrano essere i primi candidati a supportare la distruzione / finalizzazione degli oggetti come sanno con certezza al 100% quando un oggetto non è più in uso. Eppure la maggior parte di quelle lingue non lo supporta.

Non penso che sia un caso in cui il distruttore non possa mai essere chiamato, in quanto si tratterebbe di una perdita di memoria principale, che gcs è progettata per evitare. Ho potuto vedere una possibile argomentazione sul fatto che il distruttore / finalizzatore potrebbe non essere chiamato fino a qualche tempo indeterminato in futuro, ma ciò non ha impedito a Java o Python di supportare la funzionalità.

Quali sono i motivi principali della progettazione del linguaggio per non supportare alcuna forma di finalizzazione degli oggetti?


9
Forse perché finalize/ destroyè una bugia? Non vi è alcuna garanzia che verrà mai eseguito. E, anche se, non sai quando (data la garbage collection automatica), e se il contesto necessario è ancora lì (potrebbe essere già stato raccolto). Quindi è più sicuro garantire uno stato coerente in altri modi e si potrebbe voler forzare il programmatore a farlo.
Raffaello

1
Penso che questa domanda sia offtopica borderline. È una domanda di progettazione del linguaggio di programmazione del tipo che vogliamo intrattenere o è una domanda per un sito più orientato alla programmazione? Voti della comunità, per favore.
Raffaello

14
È una bella domanda nel design di PL, facciamolo.
Andrej Bauer,

3
Questa non è in realtà una distinzione statica / dinamica. Molti linguaggi statici non hanno finalizzatori. In effetti, le lingue con i finalizzatori non sono in minoranza?
Andrej Bauer,

1
penso che ci sia qualche domanda qui ... sarebbe meglio se tu definissi i termini un po 'di più. java ha un blocco finally che non è legato alla distruzione degli oggetti ma all'uscita dal metodo. ci sono anche altri modi per gestire le risorse. ad es. in Java, un pool di connessioni può gestire connessioni che non vengono utilizzate [x] di tempo e richiederle. non elegante ma funziona. parte della risposta alla tua domanda è che la garbage collection è approssimativamente un processo non deterministico, non istantaneo e non è guidata da oggetti che non vengono più utilizzati ma da vincoli / massimali di memoria innescati.
vzn

Risposte:


10

Il modello di cui stai parlando, in cui gli oggetti sanno come ripulire le proprie risorse, rientra in tre categorie rilevanti. Non confondiamo i distruttori con i finalizzatori : solo uno è legato alla garbage collection:

  • Il modello del finalizzatore : metodo di pulizia dichiarato automaticamente, definito dal programmatore, chiamato automaticamente.

    I finalizzatori vengono chiamati automaticamente prima della deallocazione da un garbage collector. Il termine si applica se l'algoritmo di garbage collection utilizzato può determinare i cicli di vita degli oggetti.

  • Il modello distruttore : metodo di pulizia dichiarato automaticamente, definito dal programmatore, chiamato automaticamente solo a volte.

    I distruttori possono essere chiamati automaticamente per oggetti allocati in pila (poiché la durata dell'oggetto è deterministica), ma devono essere esplicitamente chiamati su tutti i possibili percorsi di esecuzione per oggetti allocati in pila (poiché la durata dell'oggetto non è deterministica).

  • Il modello di eliminazione: metodo di pulizia dichiarato, definito e chiamato dal programmatore.

    I programmatori creano un metodo di smaltimento e lo chiamano da soli: è qui myObject.destroy()che cade il tuo metodo personalizzato . Se lo smaltimento è assolutamente necessario, è necessario chiamare gli eliminatori su tutti i possibili percorsi di esecuzione.

I finalizzatori sono i droidi che stai cercando.

Il modello di finalizzatore (il modello di cui si sta ponendo la domanda) è il meccanismo per associare gli oggetti alle risorse di sistema (socket, descrittori di file, ecc.) Per il recupero reciproco da parte di un garbage collector. Ma i finalizzatori sono fondamentalmente in balia dell'algoritmo di garbage collection in uso.

Considera questo tuo assunto:

Le lingue che offrono la raccolta automatica dei rifiuti ... sanno con certezza al 100% quando un oggetto non è più in uso.

Tecnicamente falso (grazie, @babou). La garbage collection riguarda fondamentalmente la memoria, non gli oggetti. Se o quando un algoritmo di raccolta si rende conto che la memoria di un oggetto non è più in uso dipende dall'algoritmo e (possibilmente) da come gli oggetti si riferiscono l'un l'altro. Parliamo di due tipi di netturbini di runtime. Esistono molti modi per modificarli e aumentarli in tecniche di base:

  1. Traccia GC. Questi tracciano la memoria, non gli oggetti. A meno che non vengano aumentati, non mantengono indietro i riferimenti agli oggetti dalla memoria. A meno che non siano aumentati, questi GC non sapranno quando un oggetto può essere finalizzato, anche se sanno quando la sua memoria è irraggiungibile. Pertanto, le chiamate del finalizzatore non sono garantite.

  2. GC di conteggio di riferimento . Questi usano oggetti per tracciare la memoria. Modellano la raggiungibilità degli oggetti con un grafico diretto di riferimenti. Se c'è un ciclo nel grafico di riferimento degli oggetti, tutti gli oggetti nel ciclo non avranno mai il loro finalizzatore chiamato (fino alla conclusione del programma, ovviamente). Ancora una volta, le chiamate del finalizzatore non sono garantite.

TLDR

La raccolta dei rifiuti è difficile e diversificata. Una chiamata del finalizzatore non può essere garantita prima della conclusione del programma.


Hai ragione che questo non è statico v. Dinamico. È un problema con le lingue raccolte con immondizia. La garbage collection è un problema complesso ed è probabilmente il motivo principale in quanto vi sono molti casi limite da considerare (ad es. Cosa succede se la logica finalize()fa sì che l'oggetto venga ripulito di nuovo a cui fare riferimento?). Tuttavia, non essere in grado di garantire che il finalizzatore venga chiamato prima della chiusura del programma non ha impedito a Java di supportarlo. Non dire che la tua risposta è errata, forse forse incompleta. Ancora un ottimo post. Grazie.
dbcb,

Grazie per il feedback. Ecco un tentativo di completare la mia risposta: omettendo esplicitamente i finalizzatori, un linguaggio costringe i suoi utenti a gestire le proprie risorse. Per molti tipi di problemi, questo è probabilmente uno svantaggio. Personalmente, preferisco la scelta di Java, perché ho il potere dei finalizzatori e non c'è niente che mi impedisce di scrivere e usare il mio dispositivo. Java sta dicendo: "Ehi, programmatore. Non sei un idiota, quindi ecco un finalizzatore. Stai solo attento."
kdbanman,

1
Ho aggiornato la mia domanda originale per riflettere che si tratta di lingue raccolte con immondizia. Accettare la tua risposta Grazie per il tempo dedicato a rispondere.
dbcb,

Felice di aiutare. Il chiarimento del mio commento ha reso la mia risposta più chiara?
kdbanman,

2
Va bene. Per me, la vera risposta qui è che le lingue scelgono di non implementarlo perché il valore percepito non supera i problemi di implementazione della funzionalità. Non è impossibile (come dimostrano Java e Python), ma c'è un compromesso che molte lingue scelgono di non fare.
dbcb,

5

In poche parole

La finalizzazione non è una questione semplice da gestire per i garbage collector. È facile da usare con il conteggio dei riferimenti GC, ma questa famiglia di GC è spesso incompleta e richiede che le perdite di memoria siano compensate dall'innesco esplicito di distruzione e finalizzazione di alcuni oggetti e strutture. La tracciabilità dei raccoglitori di rifiuti è molto più efficace, ma rende molto più difficile identificare l'oggetto da finalizzare e distruggere, al contrario di identificare semplicemente la memoria inutilizzata, richiedendo quindi una gestione più complessa, con un costo in termini di tempo e spazio e complessità l'implemento.

introduzione

Suppongo che ciò che stai chiedendo sia il motivo per cui le lingue della garbage collection non gestiscono automaticamente la distruzione / finalizzazione all'interno del processo di garbage collection, come indicato dall'osservazione:

Trovo estremamente carente che queste lingue considerino la memoria come l'unica risorsa che valga la pena di essere gestita. Che dire di socket, handle di file, stati delle applicazioni?

Non sono d'accordo con la risposta accettata data da kdbanman . Mentre i fatti dichiarati sono per lo più corretti, sebbene fortemente orientati al conteggio dei riferimenti, non credo che spieghino correttamente la situazione lamentata nella domanda.

Non credo che la terminologia sviluppata in quella risposta costituisca un grosso problema, ed è più probabile che confonda le cose. In effetti, come presentato, la terminologia è principalmente determinata dal modo in cui le procedure sono attivate piuttosto che da ciò che fanno. Il punto è che in tutti i casi è necessario finalizzare un oggetto non più necessario con un processo di pulizia e liberare qualsiasi risorsa abbia utilizzato, la memoria è solo una di queste. Idealmente, tutto dovrebbe essere fatto automaticamente quando l'oggetto non deve più essere utilizzato, tramite un garbage collector. In pratica, GC potrebbe mancare o presentare carenze, e ciò è compensato dall'attivazione esplicita del programma di finalizzazione e bonifica.

Il trigerring esplicito da parte del programma è un problema poiché può consentire errori di programmazione difficili da analizzare, quando un oggetto ancora in uso viene esplicitamente terminato.

Quindi è molto meglio fare affidamento sulla raccolta automatica dei rifiuti per recuperare le risorse. Ma ci sono due problemi:

  • alcune tecniche di garbage collection consentiranno perdite di memoria che impediscono il pieno recupero delle risorse. Questo è ben noto per il conteggio dei riferimenti GC, ma può apparire per altre tecniche GC quando si utilizzano alcune organizzazioni di dati senza cura (punto non discusso qui).

  • mentre la tecnica GC può essere efficace nell'identificare le risorse di memoria non più utilizzate, finalizzare gli oggetti in esse contenuti potrebbe non essere semplice e ciò complica il problema del recupero di altre risorse utilizzate da questi oggetti, che è spesso lo scopo della finalizzazione.

Infine, un punto importante spesso dimenticato è che i cicli GC possono essere innescati da qualsiasi cosa, non solo dalla carenza di memoria, se vengono forniti gli hook adeguati e se il costo di un ciclo GC ne vale la pena. Quindi è perfettamente OK avviare un GC quando manca qualsiasi tipo di risorsa, nella speranza di liberarne un po '.

Riferimenti che contano i netturbini

Il conteggio dei riferimenti è una tecnica di raccolta dei rifiuti debole , che non gestirà correttamente i cicli. Sarebbe davvero debole nel distruggere le strutture obsolete e nel recuperare altre risorse semplicemente perché è debole nel recuperare la memoria. Ma i finalizzatori possono essere usati più facilmente con un garbage collector (GC) di conteggio dei riferimenti, poiché un GC di conteggio dei ref reclama una struttura quando il suo conteggio dei ref scende fino a 0, a quel punto il suo indirizzo è noto insieme al suo tipo, sia staticamente o dinamicamente. Quindi è possibile recuperare la memoria proprio dopo aver applicato il finalizzatore appropriato e aver chiamato ricorsivamente il processo su tutti gli oggetti appuntiti (possibilmente tramite la procedura di finalizzazione).

In breve, la finalizzazione è facile da implementare con Ref Counting GC, ma soffre della "incompletezza" di quel GC, in effetti a causa di strutture circolari, esattamente nella stessa misura in cui soffre il recupero della memoria. In altre parole, con il conteggio dei riferimenti, la memoria è gestita in modo inadeguato esattamente come altre risorse come socket, handle di file, ecc.

In effetti, l' impossibilità di Ref Count GC di recuperare le strutture di loop (in generale) può essere vista come una perdita di memoria . Non ci si può aspettare che tutti i GC evitino perdite di memoria. Dipende dall'algoritmo GC e dalle informazioni sulla struttura del tipo disponibili dinamicamente (ad esempio nel GC conservativo ).

Tracciamento dei netturbini

La famiglia più potente di GC, senza tali perdite, è la famiglia di tracciamento che esplora le parti vive della memoria, a partire da puntatori radice ben identificati. Tutte le parti della memoria che non sono state visitate in questo processo di tracciamento (che può essere effettivamente decomposto in vari modi, ma che devo semplificare) sono parti inutilizzate della memoria che possono essere così recuperate 1 . Questi collezionisti recupereranno tutte le parti di memoria a cui il programma non può più accedere, indipendentemente da ciò che fa. Recupera strutture circolari e i GC più avanzati si basano su alcune variazioni di questo paradigma, a volte altamente sofisticate. Può essere combinato con il conteggio dei riferimenti in alcuni casi e compensare i suoi punti deboli.

Un problema è che la tua affermazione (alla fine della domanda):

Le lingue che offrono la raccolta automatica dei rifiuti sembrano essere i primi candidati a supportare la distruzione / finalizzazione degli oggetti come sanno con certezza al 100% quando un oggetto non è più in uso.

è tecnicamente errato per la tracciabilità dei collezionisti.

Ciò che è noto con certezza al 100% è quali parti della memoria non sono più in uso . (Più precisamente, si dovrebbe dire che non sono più accessibili , perché alcune parti, che non possono più essere utilizzate secondo la logica del programma, sono ancora considerate in uso se nel programma è ancora presente un puntatore inutile dati.) Ma sono necessarie ulteriori elaborazioni e strutture appropriate per sapere quali oggetti inutilizzati potrebbero essere stati archiviati in queste parti della memoria ormai inutilizzate . Ciò non può essere determinato da ciò che è noto del programma, poiché il programma non è più collegato a queste parti della memoria.

Quindi dopo un passaggio di Garbage Collection, ti rimangono frammenti di memoria che contengono oggetti che non sono più in uso, ma a priori non c'è modo di sapere quali siano questi oggetti in modo da applicare la corretta finalizzazione. Inoltre, se il raccoglitore di tracciamento è di tipo mark-and-sweep, è possibile che alcuni frammenti possano contenere oggetti che sono già stati finalizzati in un precedente passaggio GC, ma che non sono stati utilizzati da allora per motivi di frammentazione. Tuttavia, ciò può essere risolto utilizzando la digitazione esplicita estesa.

Mentre un semplice raccoglitore dovrebbe semplicemente recuperare questi frammenti di memoria, senza ulteriori indugi, la finalizzazione richiede un passaggio specifico per esplorare quella memoria inutilizzata, identificare gli oggetti in essa contenuti e applicare le procedure di finalizzazione. Ma una tale esplorazione richiede la determinazione del tipo di oggetti che sono stati memorizzati lì, e la determinazione del tipo è anche necessaria per applicare l'eventuale finalizzazione corretta.

Ciò implica costi aggiuntivi in ​​termini di tempo GC (il passaggio aggiuntivo) e, eventualmente, costi di memoria aggiuntivi per rendere disponibili le informazioni sul tipo corretto durante tale passaggio mediante diverse tecniche. Questi costi possono essere significativi in ​​quanto si vorrà spesso finalizzare solo pochi oggetti, mentre il tempo e lo spazio in alto potrebbero riguardare tutti gli oggetti.

Un altro punto è che il sovraccarico di tempo e spazio può riguardare l'esecuzione del codice del programma e non solo l'esecuzione del GC.

Non posso dare una risposta più precisa, indicando questioni specifiche, perché non conosco i dettagli di molte delle lingue che elenchi. Nel caso di C, la digitazione è un problema molto difficile che porta allo sviluppo di collezionisti conservatori. La mia ipotesi sarebbe che questo influisca anche sul C ++, ma non sono un esperto di C ++. Ciò sembra essere confermato da Hans Boehm che ha svolto gran parte delle ricerche sul GC conservatore. Conservative GC non può recuperare sistematicamente tutta la memoria inutilizzata proprio perché potrebbe non disporre di informazioni precise sul tipo di dati. Per lo stesso motivo, non sarebbe in grado di applicare sistematicamente le procedure di finalizzazione.

Quindi, è possibile fare quello che stai chiedendo, come sai da alcune lingue. Ma non viene gratis. A seconda della lingua e della sua implementazione, può comportare un costo anche quando non si utilizza la funzione. Varie tecniche e compromessi possono essere considerati per affrontare questi problemi, ma questo va oltre lo scopo di una risposta di dimensioni ragionevoli.

1 - questa è una presentazione astratta della raccolta di tracce (che comprende sia GC copia e contrassegna e trascina), le cose variano in base al tipo di raccoglitore di tracce, ed esplorare la parte inutilizzata della memoria è diverso, a seconda se copia o contrassegna e viene utilizzato lo sweep.


Fornisci molti dettagli sulla raccolta dei rifiuti. Tuttavia, la tua risposta in realtà non è in disaccordo con la mia: il tuo abstract e il mio TLDR stanno essenzialmente dicendo la stessa cosa. E per quello che vale, la mia risposta usa come esempio il conteggio dei riferimenti GC, non un "forte pregiudizio".
kdbanman,

Dopo aver letto più a fondo, vedo il disaccordo. Modificherò di conseguenza. Inoltre, la mia terminologia doveva essere inequivocabile. La domanda stava combinando finalizzatori e distruttori, e menzionava persino i disposers nello stesso respiro. Vale la pena spargere le parole giuste.
kdbanman,

@kdbanman La difficoltà era che mi rivolgevo a entrambi, poiché la vostra risposta era di riferimento. Non puoi usare il conteggio dei riferimenti come esempio paradigmatico perché è un GC debole, usato raramente nelle lingue (controlla le lingue citate dall'OP), per cui l'aggiunta di finalizzatori sarebbe effettivamente facile (ma con un uso limitato). I collettori di tracciamento sono quasi sempre utilizzati. Ma i finalizzatori sono difficili da agganciarli, perché gli oggetti morenti non sono noti (contrariamente all'affermazione che ritieni corretta). La distinzione tra tipizzazione statica e dinamica è irrilevante, poiché la tipizzazione dinamica dell'archivio dati è essenziale.
babou,

@kdbanman Per quanto riguarda la terminologia, è utile in generale, poiché corrisponde a diverse situazioni. Ma qui non aiuta, poiché la domanda riguarda il trasferimento della finalizzazione al GC. Il GC di base dovrebbe fare solo la distruzione. Ciò che è necessario è una terminologia che distingua getting memory recycled, che io chiamo reclamation, e che faccia qualche ripulitura prima di questo, come il recupero di altre risorse o l'aggiornamento di alcune tabelle di oggetti, che io chiamo finalization. Questi mi sembravano i problemi rilevanti, ma potrei aver perso un punto nella tua terminologia, che era nuova per me.
babou,

1
Grazie @kdbanman, babou. Buona discussione. Penso che entrambi i tuoi post coprano punti simili. Come entrambi sottolineate, il problema principale sembra essere la categoria di Garbage Collector impiegata nel runtime della lingua. Ho trovato questo articolo , che chiarisce alcune idee sbagliate per me. Sembra che i gcs più robusti gestiscano solo memoria raw di basso livello, il che rende i tipi di oggetti di livello superiore opachi per il gc. Senza la conoscenza degli interni della memoria, il GC non può distruggere gli oggetti. Quale sembra essere la tua conclusione.
dbcb,

4

Il modello di distruttore di oggetti è fondamentale per la gestione degli errori nella programmazione dei sistemi, ma non ha nulla a che fare con la garbage collection. Piuttosto, ha a che fare con l'adattamento della durata degli oggetti a un ambito e può essere implementato / utilizzato in qualsiasi linguaggio che abbia funzioni di prima classe.

Esempio (pseudocodice). Supponiamo di avere un tipo di "file non elaborato", come il tipo di descrittore di file Posix. Ci sono quattro operazioni fondamentali, open(), close(), read(), write(). Si desidera implementare un tipo di file "sicuro" che ripulisce sempre dopo se stesso. (Cioè, che ha un costruttore e un distruttore automatici.)

Io assumere la nostra lingua ha la gestione delle eccezioni con throw, trye finally(in lingue senza la manipolazione è possibile impostare una disciplina in cui l'utente del tipo restituisce un valore speciale per indicare un errore di eccezione.)

Si imposta una funzione che accetta una funzione che fa il lavoro. La funzione worker accetta un argomento (un handle per il file "sicuro").

with_file_opened_for_read (string:   filename,
                           function: worker_function(safe_file f)):
  raw_file rf = open(filename, O_RDONLY)
  if rf == error:
    throw File_Open_Error

  try:
    worker_function(rf)
  finally:
    close(rf)

Fornisci anche implementazioni di read()e write()per safe_file(che chiamano semplicemente raw_file read()e write()). Ora l'utente utilizza il safe_filetipo in questo modo:

...
with_file_opened_for_read ("myfile.txt",
                           anonymous_function(safe_file f):
                             mytext = read(f)
                             ... (including perhaps throwing an error)
                          )

Un distruttore C ++ è davvero solo zucchero sintattico per un try-finallyblocco. Praticamente tutto ciò che ho fatto qui è convertire in ciò che una safe_fileclasse C ++ con un costruttore e un distruttore comporterebbe. Si noti che il C ++ non ha finallyper le sue eccezioni, in particolare perché Stroustrup ha ritenuto che l'uso di un distruttore esplicito fosse meglio sintatticamente (e lo ha introdotto nella lingua prima che la lingua avesse funzioni anonime).

(Questa è una semplificazione di uno dei modi in cui le persone hanno fatto errori nella gestione di linguaggi simili a Lisp per molti anni. Penso di essermi imbattuto per la prima volta alla fine degli anni '80 o all'inizio degli anni '90, ma non ricordo dove.)


Descrive gli interni del modello di distruttore basato su stack in C ++, ma non spiega perché un linguaggio garbage collection non implementerebbe tale funzionalità. Potresti avere ragione sul fatto che ciò non ha nulla a che fare con la garbage collection, ma è correlata alla distruzione / finalizzazione generale degli oggetti, che sembra essere difficile o inefficiente nei linguaggi di garbage collection. Quindi, se la distruzione generale non è supportata, anche la distruzione basata sullo stack sembra essere omessa.
dbcb,

Come ho detto all'inizio: qualsiasi linguaggio garbage collection che abbia funzioni di prima classe (o qualche approssimazione di funzioni di prima classe) ti dà la possibilità di fornire interfacce "a prova di proiettile" come safe_filee with_file_opened_for_read(un oggetto che si chiude quando esce dal campo di applicazione ). Questa è la cosa importante, che non ha la stessa sintassi dei costruttori è irrilevante. Lisp, Scheme, Java, Scala, Go, Haskell, Rust, Javascript, Clojure supportano tutte sufficienti funzioni di prima classe sufficienti, quindi non hanno bisogno di distruttori per fornire la stessa utile funzionalità.
Wandering Logic,

Penso di vedere quello che stai dicendo. Dal momento che le lingue forniscono i mattoni di base (try / catch / infine, funzioni di prima classe, ecc.) Per implementare manualmente funzionalità simili a quelle dei distruttori, non hanno bisogno di distruttori? Ho potuto vedere alcune lingue prendere quella strada per motivi di semplicità. Anche se, sembra improbabile che sia la ragione principale per tutte le lingue elencate, ma forse è quello che è. Forse sono solo nella vasta minoranza che ama i distruttori del C ++ e a nessun altro importa davvero, il che potrebbe benissimo essere il motivo per cui la maggior parte delle lingue non implementano i distruttori. A loro non importa.
dbcb,

2

Questa non è una risposta completa alla domanda, ma volevo aggiungere un paio di osservazioni che non sono state trattate nelle altre risposte o commenti.

  1. La domanda presuppone implicitamente che stiamo parlando di un linguaggio orientato agli oggetti in stile Simula, che è esso stesso limitante. Nella maggior parte delle lingue, anche quelle con oggetti, non tutto è un oggetto. Le macchine per implementare i distruttori importerebbero un costo che non tutti gli implementatori di lingue sono disposti a pagare.

  2. Il C ++ ha alcune garanzie implicite sull'ordine di distruzione. Se hai una struttura di dati ad albero, ad esempio, i figli verranno distrutti prima del genitore. Questo non è il caso dei linguaggi GC, quindi le risorse gerarchiche possono essere rilasciate in un ordine imprevedibile. Per le risorse non di memoria, questo può avere importanza.


2

Quando sono stati progettati i due framework GC più popolari (Java e .NET), penso che gli autori si aspettassero che la finalizzazione avrebbe funzionato abbastanza bene da evitare la necessità di altre forme di gestione delle risorse. Molti aspetti della progettazione del linguaggio e del framework possono essere notevolmente semplificati se non sono necessarie tutte le funzionalità necessarie per gestire la gestione delle risorse affidabile e deterministica al 100%. In C ++, è necessario distinguere tra i concetti di:

  1. Puntatore / riferimento che identifica un oggetto di proprietà esclusiva del titolare del riferimento e che non è identificato da alcun puntatore / riferimento di cui il proprietario non è a conoscenza.

  2. Puntatore / riferimento che identifica un oggetto condivisibile che non è di proprietà esclusiva di nessuno.

  3. Puntatore / riferimento che identifica un oggetto di proprietà esclusiva del titolare del riferimento, ma al quale può essere accessibile tramite "visualizzazioni" il proprietario non ha alcun modo di tracciare.

  4. Puntatore / riferimento che identifica un oggetto che fornisce una vista di un oggetto di proprietà di qualcun altro.

Se un linguaggio / framework GC non deve preoccuparsi della gestione delle risorse, tutto quanto sopra può essere sostituito da un solo tipo di riferimento.

Troverei ingenua l'idea che la finalizzazione eliminerebbe la necessità di altre forme di gestione delle risorse, ma se tale aspettativa fosse o meno ragionevole al momento, la storia da allora ha dimostrato che ci sono molti casi che richiedono una gestione delle risorse più precisa di quella fornita dalla finalizzazione . Mi capita di pensare che i vantaggi del riconoscimento della proprietà a livello di lingua / framework siano sufficienti per giustificare il costo (la complessità deve esistere da qualche parte e spostarla nella lingua / framework semplificherebbe il codice utente), ma riconosco che ci sono significativi la progettazione beneficia di avere un unico "tipo" di riferimento - qualcosa che funziona solo se il linguaggio / il quadro sono agnostici ai problemi di pulizia delle risorse.


2

Perché il paradigma del distruttore di oggetti nelle immondizie raccolte pervasivamente è assente?

Vengo da uno sfondo C ++, quindi questa area è sconcertante per me.

Il distruttore in C ++ in realtà fa due cose combinate. Libera la RAM e libera gli ID delle risorse.

Altre lingue separano queste preoccupazioni avendo il GC responsabile della liberazione della RAM mentre un'altra funzione linguistica si occupa della liberazione degli ID delle risorse.

Trovo estremamente carente che queste lingue considerino la memoria come l'unica risorsa che valga la pena di essere gestita.

Questo è tutto ciò che riguarda i GC. Danno solo una cosa ed è assicurarsi che non si esaurisca la memoria. Se la RAM è infinita, tutti i GC verrebbero ritirati in quanto non esiste più alcun motivo reale per esistere.

Che dire di socket, handle di file, stati delle applicazioni?

Le lingue possono fornire diversi modi per liberare gli ID delle risorse:

  • manuale .CloseOrDispose()sparso attraverso il codice

  • manuale .CloseOrDispose()sparso nel " finallyblocco" manuale

  • manuale "blocchi di Identificazione di risorse" (cioè using, with, try-con-risorse , ecc) che automatizza .CloseOrDispose()dopo il blocco viene fatto

  • garantiti "blocchi di Identificazione di risorse" che automatizza.CloseOrDispose() dopo il blocco viene fatto

Molte lingue usano meccanismi manuali (anziché garantiti) che creano un'opportunità per la cattiva gestione delle risorse. Prendi questo semplice codice NodeJS:

require('fs').openSync('file1.txt', 'w');
// forget to .closeSync the opened file

..che il programmatore ha dimenticato di chiudere il file aperto.

Fino a quando il programma continua a funzionare, il file aperto rimarrà bloccato nel limbo. Questo è facile da verificare provando ad aprire il file usando HxD e verificando che non sia possibile:

inserisci qui la descrizione dell'immagine

Anche la liberazione di ID risorse all'interno dei distruttori C ++ non è garantita. Si potrebbe pensare che RAII funzioni come "blocchi ID risorse" garantiti, ma diversamente dai "blocchi ID risorse", il linguaggio C ++ non impedisce all'oggetto che fornisce il blocco RAII di fuoriuscire , quindi il blocco RAII potrebbe non essere mai eseguito .


Sembra che quasi tutti i moderni linguaggi di immondizia raccolti con il supporto di oggetti OOPy come Ruby, Javascript / ES6 / ES7, Actionscript, Lua, ecc. Omettono completamente il paradigma distruttore / finalizzato. Python sembra essere l'unico con il suo __del__()metodo di classe . Perchè è questo?

Perché gestiscono gli ID delle risorse usando altri modi, come menzionato sopra.

Quali sono le decisioni di progettazione del linguaggio che portano a queste lingue non avere alcun modo di eseguire la logica personalizzata sullo smaltimento degli oggetti?

Perché gestiscono gli ID delle risorse usando altri modi, come menzionato sopra.

Perché un linguaggio dovrebbe avere il concetto integrato di istanze di oggetto con strutture di classe o di classe insieme a un'istanza personalizzata (costruttori), ma omettere completamente la funzionalità di distruzione / finalizzazione?

Perché gestiscono gli ID delle risorse usando altri modi, come menzionato sopra.

Ho potuto vedere una possibile argomentazione sul fatto che il distruttore / finalizzatore potrebbe non essere chiamato fino a qualche tempo indeterminato in futuro, ma ciò non ha impedito a Java o Python di supportare la funzionalità.

Java non ha distruttori.

I documenti Java menzionano :

lo scopo abituale di finalizzare, tuttavia, è eseguire azioni di pulizia prima che l'oggetto venga irrevocabilmente scartato. Ad esempio, il metodo finalize per un oggetto che rappresenta una connessione input / output potrebbe eseguire transazioni I / O esplicite per interrompere la connessione prima che l'oggetto venga scartato in modo permanente.

.. ma inserire il codice di gestione dell'ID risorsa Object.finalizerè in gran parte considerato un anti-modello ( cfr .). Tale codice dovrebbe invece essere scritto nel sito di chiamata.

Per le persone che usano l'anti-pattern, la loro giustificazione è che potrebbero aver dimenticato di rilasciare gli ID delle risorse sul sito della chiamata. Quindi, lo fanno di nuovo nel finalizzatore, per ogni evenienza.

Quali sono i motivi principali della progettazione del linguaggio per non supportare alcuna forma di finalizzazione degli oggetti?

Non ci sono molti casi d'uso per i finalizzatori in quanto sono per l'esecuzione di un pezzo di codice tra il momento in cui non ci sono più riferimenti forti all'oggetto e il momento in cui la sua memoria viene recuperata dal GC.

Un possibile caso d'uso è quando si desidera conservare un registro del tempo tra l'oggetto viene raccolto dal GC e il momento in cui non vi sono più riferimenti forti all'oggetto, in quanto tale:

finalize() {
    Log(TimeNow() + ". Obj " + toString() + " is going to be memory-collected soon!"); // "soon"
}

-1

trovato un riferimento a questo in Dr Dobbs Wrt c ++ che ha idee più generali che sostengono che i distruttori sono problematici in un linguaggio in cui sono implementati. un'idea approssimativa qui sembra essere che uno scopo principale dei distruttori sia gestire la deallocazione della memoria, e che è difficile da realizzare correttamente. la memoria viene allocata a tratti ma oggetti diversi vengono collegati e quindi la responsabilità / i confini della deallocazione non sono così chiari.

così la soluzione a questo di un garbage collector si è evoluta anni fa, ma la garbage collection non si basa su oggetti che scompaiono dall'ambito alle uscite del metodo (che è un'idea concettuale che è difficile da implementare), ma su un collettore che gira periodicamente, in qualche modo non deterministicamente, quando l'app subisce "pressione della memoria" (ovvero esaurisce la memoria).

in altre parole, il semplice concetto umano di un "oggetto appena inutilizzato" è in qualche modo un'astrazione fuorviante, nel senso che nessun oggetto può "istantaneamente" diventare inutilizzato. gli oggetti non utilizzati possono essere "scoperti" solo eseguendo un algoritmo di garbage collection che attraversa il grafico di riferimento dell'oggetto e gli algoritmi con le migliori prestazioni vengono eseguiti in modo intermittente.

è possibile che esista un algoritmo di raccolta dei rifiuti migliore in attesa di essere scoperto in grado di identificare quasi istantaneamente oggetti inutilizzati, il che potrebbe portare a un codice di chiamata distruttore coerente, ma non è stato trovato dopo molti anni di ricerca nell'area.

la soluzione alle aree di gestione delle risorse come file o connessioni sembra essere quella di avere "gestori" di oggetti che tentano di gestirne l'utilizzo.


2
Trova interessante. Grazie. L'argomento dell'autore si basa sul fatto che il distruttore venga chiamato nel momento sbagliato a causa del passaggio di istanze di classe in base al valore in cui la classe non ha un costruttore di copia adeguato (che è un vero problema). Tuttavia, questo scenario non esiste realmente nella maggior parte (se non in tutti) i linguaggi dinamici moderni perché tutto viene passato per riferimento, il che evita la situazione dell'autore. Sebbene questa sia una prospettiva interessante, non penso che spieghi perché la maggior parte dei linguaggi raccolti dai rifiuti abbia scelto di omettere la funzionalità di distruttore / finalizzazione.
dbcb,

2
Questa risposta travisa l'articolo del Dr. Dobb: l'articolo non sostiene che i distruttori siano problematici in generale. L'articolo in realtà sostiene questo: le primitive di gestione della memoria sono come le dichiarazioni goto, perché sono entrambe semplici ma troppo potenti. Allo stesso modo in cui le istruzioni goto sono meglio incapsulate in "strutture di controllo appropriatamente limitate" (Vedi: Dijktsra), le primitive di gestione della memoria sono meglio incapsulate in "strutture di dati appropriatamente limitate". I distruttori sono un passo in questa direzione, ma non abbastanza lontano. Decidi tu stesso se è vero o no.
kdbanman,
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.