Pourquoi le paradigme destructeur d'objets dans les langues de collecte des ordures est-il omniprésent?

27

Vous cherchez un aperçu des décisions concernant la conception de la langue de collecte des ordures. Peut-être qu'un expert linguistique pourrait m'éclairer? Je viens d'un milieu C ++, donc ce domaine est déroutant pour moi.

Il semble que presque tous les langages modernes récupérés avec la prise en charge des objets OOPy comme Ruby, Javascript / ES6 / ES7, Actionscript, Lua, etc. omettent complètement le paradigme destructeur / finaliser. Python semble être le seul avec sa class __del__()méthode. Pourquoi est-ce? Existe-t-il des limitations fonctionnelles / théoriques dans les langages avec ramasse-miettes automatique qui empêchent les implémentations efficaces d'une méthode destructrice / finaliser sur les objets?

Je trouve extrêmement insuffisant que ces langages considèrent la mémoire comme la seule ressource à gérer. Qu'en est-il des sockets, des descripteurs de fichiers, des états d'application? Sans la possibilité d'implémenter une logique personnalisée pour nettoyer les ressources non mémoire et les états lors de la finalisation des objets, je suis obligé de jeter mon application avec des myObject.destroy()appels de style personnalisé , de placer la logique de nettoyage en dehors de ma "classe", de rompre les tentatives d'encapsulation et de reléguer mon application aux fuites de ressources dues à une erreur humaine plutôt que d'être gérées automatiquement par le GC.

Quelles sont les décisions de conception de langage qui font que ces langages n'ont aucun moyen d'exécuter une logique personnalisée lors de l'élimination des objets? Je dois imaginer qu'il y a une bonne raison. J'aimerais mieux comprendre les décisions techniques et théoriques qui ont eu pour conséquence que ces langages ne prennent pas en charge la destruction / finalisation d'objets.

Mise à jour:

Peut-être une meilleure façon de formuler ma question:

Pourquoi un langage aurait-il le concept intégré d'instances d'objets avec des structures de classe ou de classe avec une instanciation personnalisée (constructeurs), tout en omettant complètement la fonctionnalité de destruction / finalisation? Les langages qui offrent la collecte automatique des ordures semblent être des candidats privilégiés pour prendre en charge la destruction / finalisation d'objets, car ils savent avec une certitude à 100% qu'un objet n'est plus utilisé. Pourtant, la plupart de ces langues ne le prennent pas en charge.

Je ne pense pas que ce soit un cas où le destructeur ne sera jamais appelé, car ce serait une fuite de mémoire centrale, que les gcs sont conçus pour éviter. Je pouvais voir un argument possible étant que le destructeur / finaliseur pourrait ne pas être appelé avant un moment indéterminé à l'avenir, mais cela n'a pas empêché Java ou Python de prendre en charge la fonctionnalité.

Quelles sont les principales raisons de conception du langage pour ne prendre en charge aucune forme de finalisation d'objet?

dbcb
la source
9
Peut-être parce que finalize/ destroyest un mensonge? Il n'y a aucune garantie qu'il sera jamais exécuté. Et, même si, vous ne savez pas quand (en raison du ramassage automatique des ordures), et si le contexte nécessaire est toujours là (il a peut-être déjà été collecté). Il est donc plus sûr de garantir un état cohérent par d'autres moyens, et on peut vouloir forcer le programmeur à le faire.
Raphael
1
Je pense que cette question est limite hors sujet. Est-ce une question de conception de langage de programmation du type que nous voulons aborder, ou est-ce une question pour un site plus orienté vers la programmation? Votes de la communauté, s'il vous plaît.
Raphael
14
C'est une bonne question dans la conception PL, nous allons l'avoir.
Andrej Bauer
3
Ce n'est pas vraiment une distinction statique / dynamique. De nombreux langages statiques n'ont pas de finaliseurs. En fait, les langues avec finaliseurs ne sont-elles pas minoritaires?
Andrej Bauer
1
pense qu'il y a une question ici ... il serait préférable que vous définissiez un peu plus les termes. java a un bloc finalement qui n'est pas lié à la destruction d'objet mais à la sortie de méthode. il existe également d'autres moyens de gérer les ressources. Par exemple, en java, un pool de connexions peut gérer les connexions qui ne sont pas utilisées [x] pendant une longue période et les récupérer. pas élégant mais ça marche. une partie de la réponse à votre question est que la récupération de place est à peu près un processus non déterministe et non instantané et n'est pas pilotée par des objets qui ne sont plus utilisés mais par des contraintes / plafonds de mémoire qui sont déclenchés.
2015

Réponses:

10

Le modèle dont vous parlez, où les objets savent comment nettoyer leurs ressources, se divise en trois catégories pertinentes. Ne confondons pas les destructeurs avec les finaliseurs - un seul est lié à la récupération de place:

  • Le modèle du finaliseur : méthode de nettoyage déclarée automatiquement, définie par le programmeur, appelée automatiquement.

    Les finaliseurs sont appelés automatiquement avant la désallocation par un garbage collector. Le terme s'applique si l'algorithme de récupération de place utilisé peut déterminer les cycles de vie des objets.

  • Le modèle destructeur : méthode de nettoyage déclarée automatiquement, définie par le programmeur, appelée automatiquement seulement parfois.

    Les destructeurs peuvent être appelés automatiquement pour les objets alloués à la pile (car la durée de vie des objets est déterministe), mais doivent être explicitement appelés sur tous les chemins d'exécution possibles pour les objets alloués par segment (car la durée de vie des objets n'est pas déterministe).

  • Le modèle de broyeur : méthode de nettoyage déclarée, définie et appelée par le programmeur.

    Les programmeurs créent une méthode d'élimination et l'appellent eux-mêmes - c'est là que votre myObject.destroy()méthode personnalisée tombe. Si l'élimination est absolument nécessaire, les éliminateurs doivent être appelés sur tous les chemins d'exécution possibles.

Les finaliseurs sont les droïdes que vous recherchez.

Le modèle de finaliseur (le modèle sur lequel porte votre question) est le mécanisme permettant d'associer des objets à des ressources système (sockets, descripteurs de fichiers, etc.) pour une récupération mutuelle par un garbage collector. Mais, les finaliseurs sont fondamentalement à la merci de l'algorithme de collecte des ordures utilisé.

Considérez votre hypothèse:

Les langages qui offrent la collecte automatique des ordures ... savent avec 100% de certitude quand un objet n'est plus utilisé.

Techniquement faux (merci @babou). La collecte des ordures concerne fondamentalement la mémoire, pas les objets. Si ou quand un algorithme de collecte se rend compte que la mémoire d'un objet n'est plus utilisée, cela dépend de l'algorithme et (éventuellement) de la façon dont vos objets se réfèrent les uns aux autres. Parlons de deux types de ramasse-miettes d'exécution. Il existe de nombreuses façons de les modifier et de les étendre aux techniques de base:

  1. Tracing GC. Ces traces de mémoire, pas des objets. À moins qu'ils ne soient augmentés, ils ne conservent pas de références en arrière aux objets de la mémoire. Sauf s'ils sont augmentés, ces GC ne sauront pas quand un objet peut être finalisé, même s'ils savent quand sa mémoire est inaccessible. Par conséquent, les appels du finaliseur ne sont pas garantis.

  2. Comptage de référence GC . Ceux-ci utilisent des objets pour suivre la mémoire. Ils modélisent l'accessibilité des objets avec un graphe orienté de références. S'il y a un cycle dans votre graphique de référence d'objet, alors tous les objets du cycle n'auront jamais leur finaliseur appelé (jusqu'à la fin du programme, évidemment). Encore une fois, les appels du finaliseur ne sont pas garantis.

TLDR

La collecte des ordures est difficile et diversifiée. Un appel du finaliseur ne peut être garanti avant la fin du programme.

kdbanman
la source
Vous avez raison de dire que ce n'est pas statique contre dynamique. C'est un problème avec les langues récupérées. La récupération de place est un problème complexe et est probablement la principale raison car il y a de nombreux cas marginaux à considérer (par exemple, que se passe-t-il si la logique dans finalize()fait que l'objet nettoyé est à nouveau référencé?). Cependant, le fait de ne pas pouvoir garantir que le finaliseur soit appelé avant la fin du programme n'a pas empêché Java de le prendre en charge. Ne pas dire que votre réponse est incorrecte, peut-être juste incomplète. Encore un très bon post. Merci.
dbcb
Merci pour les commentaires. Voici une tentative pour compléter ma réponse: en omettant explicitement les finaliseurs, un langage force ses utilisateurs à gérer leurs propres ressources. Pour de nombreux types de problèmes, c'est probablement un inconvénient. Personnellement, je préfère le choix de Java, car j'ai le pouvoir des finaliseurs et rien ne m'empêche d'écrire et d'utiliser mon propre broyeur. Java dit: "Hé, programmeur. Vous n'êtes pas un idiot, alors voici un finaliseur. Faites juste attention."
kdbanman
1
Mise à jour de ma question d'origine pour refléter que cela concerne les langues récupérées. Accepter votre réponse. Merci d'avoir pris le temps de répondre.
dbcb
Heureux d'aider. Ma clarification des commentaires a-t-elle clarifié ma réponse?
kdbanman
2
C'est bon. Pour moi, la vraie réponse ici est que les langues choisissent de ne pas l'implémenter car la valeur perçue ne l'emporte pas sur les problèmes d'implémentation de la fonctionnalité. Ce n'est pas impossible (comme le démontrent Java et Python), mais il y a un compromis que de nombreuses langues choisissent de ne pas faire.
dbcb
5

En un mot

La finalisation n'est pas une affaire simple à gérer par les ramasseurs d'ordures. Il est facile à utiliser avec le GC de comptage de référence, mais cette famille de GC est souvent incomplète, nécessitant des fuites de mémoire pour être compensées par le déclenchement explicite de la destruction et la finalisation de certains objets et structures. Le traçage des collecteurs de déchets est beaucoup plus efficace, mais il est beaucoup plus difficile d'identifier l'objet à finaliser et à détruire, par opposition à simplement identifier la mémoire inutilisée, nécessitant ainsi une gestion plus complexe, avec un coût en temps et en espace, et en complexité de la mise en oeuvre.

introduction

Je suppose que ce que vous demandez, c'est pourquoi les langues de récupération de place ne gèrent pas automatiquement la destruction / finalisation dans le processus de récupération de place, comme indiqué par la remarque:

Je trouve extrêmement insuffisant que ces langages considèrent la mémoire comme la seule ressource à gérer. Qu'en est-il des sockets, des descripteurs de fichiers, des états d'application?

Je ne suis pas d'accord avec la réponse acceptée donnée par kdbanman . Bien que les faits déclarés soient pour la plupart corrects, bien que fortement biaisés vers le comptage des références, je ne pense pas qu'ils expliquent correctement la situation dénoncée dans la question.

Je ne pense pas que la terminologie développée dans cette réponse soit un gros problème, et elle est plus susceptible de semer la confusion. En effet, comme présenté, la terminologie est principalement déterminée par la façon dont les procédures sont activées plutôt que par ce qu'elles font. Le fait est que dans tous les cas, il est nécessaire de finaliser un objet qui n'est plus nécessaire avec un processus de nettoyage et de libérer toutes les ressources qu'il a utilisées, la mémoire n'étant que l'une d'entre elles. Idéalement, tout cela devrait être fait automatiquement lorsque l'objet n'est plus utilisé, au moyen d'un ramasse-miettes. Dans la pratique, le GC peut être manquant ou présenter des lacunes, ce qui est compensé par un déclenchement explicite par le programme de finalisation et de remise en état.

Le déclenchement explicite par le programme est un problème car il peut permettre d'analyser difficilement les erreurs de programmation, lorsqu'un objet encore en cours d'utilisation se termine explicitement.

Par conséquent, il est préférable de s'appuyer sur la collecte automatique des ordures pour récupérer les ressources. Mais il y a deux problèmes:

  • certaines techniques de récupération de place permettront des fuites de mémoire qui empêcheront la récupération complète des ressources. Ceci est bien connu pour le calcul de référence GC, mais peut apparaître pour d'autres techniques de GC lors de l'utilisation de certaines organisations de données sans précaution (point non discuté ici).

  • Bien que la technique GC puisse être efficace pour identifier les ressources de mémoire qui ne sont plus utilisées, la finalisation des objets qui y sont contenus peut ne pas être simple, et cela complique le problème de la récupération d'autres ressources utilisées par ces objets, ce qui est souvent le but de la finalisation.

Enfin, un point important souvent oublié est que les cycles de GC peuvent être déclenchés par n'importe quoi, pas seulement par une pénurie de mémoire, si les crochets appropriés sont fournis et si le coût d'un cycle de GC est considéré comme valable. Par conséquent, il est tout à fait correct d'initier un GC quand n'importe quel type de ressource est manquant, dans l'espoir d'en libérer certaines.

Compter les ordures ménagères

Le comptage des références est une technique de collecte des ordures faible , qui ne gère pas les cycles correctement. Il serait en effet faible sur la destruction de structures obsolètes et la récupération d'autres ressources simplement parce qu'il est faible sur la récupération de la mémoire. Mais les finaliseurs peuvent être utilisés plus facilement avec un récupérateur de déchets de comptage de référence (GC), car un GC de comptage de référence récupère une structure lorsque son nombre de références descend à 0, moment auquel son adresse est connue avec son type, soit statiquement ou dynamiquement. Par conséquent, il est possible de récupérer la mémoire précisément après avoir appliqué le finaliseur approprié et appelé récursivement le processus sur tous les objets pointés (éventuellement via la procédure de finalisation).

En un mot, la finalisation est facile à implémenter avec Ref Counting GC, mais souffre de l '"incomplétude" de ce GC, en raison des structures circulaires, exactement dans la même mesure que la récupération de mémoire en souffre. En d'autres termes, avec le nombre de références, la mémoire est précisément aussi mal gérée que d'autres ressources telles que les sockets, les descripteurs de fichiers, etc.

En effet, l' incapacité de Ref Count GC à récupérer des structures en boucle (en général) peut être considérée comme une fuite de mémoire . Vous ne pouvez pas vous attendre à ce que tous les CPG évitent les fuites de mémoire. Cela dépend de l'algorithme GC et des informations de structure de type disponibles dynamiquement (par exemple dans GC conservateur ).

Traçage des collecteurs d'ordures

La famille de GC la plus puissante, sans ces fuites, est la famille de traçage qui explore les parties actives de la mémoire, à partir de pointeurs racine bien identifiés. Toutes les parties de la mémoire qui ne sont pas visitées dans ce processus de traçage (qui peuvent en fait être décomposées de diverses manières, mais je dois simplifier) ​​sont des parties inutilisées de la mémoire qui peuvent ainsi être récupérées 1 . Ces collecteurs récupéreront toutes les parties de la mémoire auxquelles le programme ne pourra plus accéder, quoi qu'il fasse. Il récupère des structures circulaires, et les GC les plus avancés sont basés sur une certaine variation de ce paradigme, parfois très sophistiqué. Il peut être combiné avec le comptage des références dans certains cas et compenser ses faiblesses.

Un problème est que votre déclaration (à la fin de la question):

Les langages qui offrent la collecte automatique des ordures semblent être des candidats privilégiés pour prendre en charge la destruction / finalisation d'objets, car ils savent avec une certitude à 100% qu'un objet n'est plus utilisé.

est techniquement incorrect pour le traçage des collecteurs.

Ce que l'on sait avec certitude à 100%, c'est quelles parties de la mémoire ne sont plus utilisées . (Plus précisément, il faut dire qu'elles ne sont plus accessibles , car certaines parties, qui ne peuvent plus être utilisées selon la logique du programme, sont toujours considérées comme utilisées s'il y a encore un pointeur inutile vers elles dans le programme Mais un traitement supplémentaire et des structures appropriées sont nécessaires pour savoir quels objets inutilisés peuvent avoir été stockés dans ces parties de la mémoire qui ne sont plus utilisées . Cela ne peut pas être déterminé à partir de ce que l'on sait du programme, car le programme n'est plus connecté à ces parties de la mémoire.

Ainsi après un passage de garbage collection, vous vous retrouvez avec des fragments de mémoire qui contiennent des objets qui ne sont plus utilisés, mais il n'y a a priori aucun moyen de savoir ce que sont ces objets pour appliquer la finalisation correcte. De plus, si le collecteur de traçage est de type marquage et balayage, il se peut que certains des fragments contiennent des objets qui ont déjà été finalisés dans une passe GC précédente, mais qui n'ont pas été utilisés depuis pour des raisons de fragmentation. Cependant, cela peut être résolu en utilisant un typage explicite étendu.

Alors qu'un simple collectionneur récupérerait simplement ces fragments de mémoire, sans plus tarder, la finalisation nécessite une passe spécifique pour explorer cette mémoire inutilisée, identifier les objets qu'elle contenait et appliquer les procédures de finalisation. Mais une telle exploration nécessite la détermination du type d'objets qui y étaient stockés, et la détermination du type est également nécessaire pour appliquer la finalisation appropriée, le cas échéant.

Cela implique donc des coûts supplémentaires en temps GC (le passage supplémentaire) et éventuellement des coûts de mémoire supplémentaires pour rendre les informations de type appropriées disponibles pendant ce passage par diverses techniques. Ces coûts peuvent être importants dans la mesure où l'on ne souhaite souvent finaliser que quelques objets, tandis que le temps et l'espace requis peuvent concerner tous les objets.

Un autre point est que la surcharge de temps et d'espace peut concerner l'exécution du code du programme, et pas seulement l'exécution du GC.

Je ne peux pas donner de réponse plus précise, en pointant des questions spécifiques, car je ne connais pas les spécificités de nombreuses langues que vous citez. Dans le cas de C, la dactylographie est une question très difficile qui conduit au développement de collecteurs conservateurs. Je suppose que cela affecte également C ++, mais je ne suis pas un expert en C ++. Cela semble être confirmé par Hans Boehm qui a fait une grande partie de la recherche sur GC conservateur. GC conservateur ne peut pas récupérer systématiquement toute la mémoire inutilisée précisément car il peut manquer d'informations de type précises sur les données. Pour la même raison, il ne serait pas en mesure d'appliquer systématiquement les procédures de finalisation.

Il est donc possible de faire ce que vous demandez, comme vous le savez dans certaines langues. Mais cela ne vient pas gratuitement. Selon la langue et sa mise en œuvre, cela peut entraîner un coût même si vous n'utilisez pas la fonctionnalité. Diverses techniques et compromis peuvent être envisagés pour résoudre ces problèmes, mais cela dépasse la portée d'une réponse de taille raisonnable.

1 - il s'agit d'une présentation abstraite de la collection de traçage (englobant à la fois le GC de copie et de marquage et de balayage), les choses varient selon le type de collecteur de traçage, et l'exploration de la partie inutilisée de la mémoire est différente, selon qu'il s'agit d'une copie ou d'une marque et balayage est utilisé.

babou
la source
Vous donnez beaucoup de détails sur la collecte des ordures. Cependant, votre réponse n'est pas en désaccord avec la mienne - votre résumé et mon TLDR disent essentiellement la même chose. Et pour ce que ça vaut, ma réponse utilise GC de comptage de référence comme exemple, pas un "biais fort".
kdbanman
Après une lecture plus approfondie, je constate le désaccord. Je vais modifier en conséquence. De plus, ma terminologie devait être sans ambiguïté. La question était de confondre les finaliseurs et les destructeurs, et a même mentionné les broyeurs dans le même souffle. Cela vaut la peine de passer les bons mots.
kdbanman
@kdbanman La difficulté était que je m'adressais à vous deux, car votre réponse faisait office de référence. Vous ne pouvez pas utiliser ref count comme exemple paradigmatique car il s'agit d'un GC faible, rarement utilisé dans les langues (vérifiez les langues citées par l'OP), pour lesquelles l'ajout de finaliseurs serait en fait facile (mais avec une utilisation limitée). Les collecteurs de traçage sont presque toujours utilisés. Mais les finaliseurs sont difficiles à accrocher, car les objets mourants ne sont pas connus (contrairement à l'affirmation que vous considérez correcte). La distinction entre le typage statique et dynamique n'est pas pertinente, car le typage dynamique du magasin de données est essentiel.
babou
@kdbanman Concernant la terminologie, elle est utile en général, car elle correspond à différentes situations. Mais ici, cela n'aide pas, car la question concerne le transfert de la finalisation au GC. Le GC de base est censé faire uniquement la destruction. Ce qu'il faut, c'est une terminologie qui distingue getting memory recycledce que j'appelle reclamationet qui effectue un nettoyage avant cela, comme récupérer d'autres ressources ou mettre à jour certaines tables d'objets, que j'appelle finalization. Ces questions m'ont semblé pertinentes, mais j'ai peut-être manqué un point dans votre terminologie, qui était nouveau pour moi.
babou
1
Merci @kdbanman, babou. Bonne discussion. Je pense que vos deux articles couvrent des points similaires. Comme vous le faites remarquer tous les deux, le problème principal semble être la catégorie de garbage collector utilisée dans le runtime du langage. J'ai trouvé cet article , qui clarifie certaines idées fausses pour moi. Il semble que les gcs plus robustes ne gèrent que la mémoire brute de bas niveau, ce qui rend les types d'objets de niveau supérieur opaques pour le gc. Sans connaissance des internes de la mémoire, le GC ne peut pas détruire d'objets. Ce qui semble être votre conclusion.
dbcb
4

Le modèle de destructeur d'objets est fondamental pour la gestion des erreurs dans la programmation des systèmes, mais n'a rien à voir avec la récupération de place. Il s'agit plutôt de faire correspondre la durée de vie d'un objet à une étendue, et peut être implémenté / utilisé dans n'importe quel langage ayant des fonctions de première classe.

Exemple (pseudocode). Supposons que vous ayez un type de "fichier brut", comme le type de descripteur de fichier Posix. Il y a quatre opérations fondamentales, open(), close(), read(), write(). Vous souhaitez implémenter un type de fichier "sûr" qui nettoie toujours après lui-même. (C'est-à-dire, qui a un constructeur et un destructeur automatiques.)

Je suppose que notre langue a une gestion des exceptions avec throw, tryet finally(dans les langues sans gestion des exceptions, vous pouvez configurer une discipline dans laquelle l'utilisateur de votre type renvoie une valeur spéciale pour indiquer une erreur.)

Vous configurez une fonction qui accepte une fonction qui fait le travail. La fonction de travail accepte un argument (un handle vers le fichier "sûr").

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)

Vous fournissez également des implémentations de read()et write()pour safe_file(qui appellent simplement le raw_file read()et write()). Maintenant, l'utilisateur utilise le safe_filetype comme ceci:

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

Un destructeur C ++ est vraiment juste du sucre syntaxique pour un try-finallybloc. À peu près tout ce que j'ai fait ici est de convertir ce qu'une safe_fileclasse C ++ avec un constructeur et un destructeur compilerait. Notez que C ++ n'a pas finallyd'exceptions, en particulier parce que Stroustrup a estimé que l'utilisation d'un destructeur explicite était meilleure syntaxiquement (et il l'a introduit dans le langage avant que le langage ait des fonctions anonymes).

(Il s'agit d'une simplification de l'une des façons dont les gens gèrent les erreurs dans des langages de type Lisp depuis de nombreuses années. Je pense que je l'ai rencontré à la fin des années 1980 ou au début des années 1990, mais je ne me souviens pas où.)

Logique errante
la source
Cela décrit les éléments internes du modèle de destructeur basé sur la pile en C ++, mais n'explique pas pourquoi un langage récupéré ne mettrait pas en œuvre une telle fonctionnalité. Vous avez peut-être raison de dire que cela n'a rien à voir avec la récupération de place, mais cela est lié à la destruction / finalisation générale des objets, qui semble être difficile ou inefficace dans les langues de récupération de place. Donc, si la destruction générale n'est pas prise en charge, la destruction basée sur la pile semble également être omise.
dbcb
Comme je l'ai dit au début: tout langage récupéré qui a des fonctions de première classe (ou une approximation des fonctions de première classe) vous donne la possibilité de fournir des interfaces "pare-balles" comme safe_fileet with_file_opened_for_read(un objet qui se ferme quand il sort du domaine ). C'est la chose importante, qu'il n'a pas la même syntaxe que les constructeurs n'est pas pertinent. Lisp, Scheme, Java, Scala, Go, Haskell, Rust, Javascript, Clojure prennent tous en charge des fonctions de première classe suffisantes, ils n'ont donc pas besoin de destructeurs pour fournir la même fonctionnalité utile.
Wandering Logic
Je pense que je vois ce que vous dites. Étant donné que les langages fournissent les blocs de construction de base (try / catch / enfin, fonctions de première classe, etc.) pour implémenter manuellement des fonctionnalités de type destructeur, ils n'ont pas besoin de destructeurs? J'ai pu voir certaines langues emprunter cette voie pour des raisons de simplicité. Bien qu'il semble peu probable que ce soit la principale raison de toutes les langues répertoriées, mais c'est peut-être ce que c'est. Je suis peut-être juste dans la grande minorité qui aime les destructeurs C ++ et personne d'autre ne s'en soucie vraiment, ce qui pourrait très bien être la raison pour laquelle la plupart des langages n'implémentent pas de destructeurs. Ils s'en moquent.
dbcb
2

Ce n'est pas une réponse complète à la question, mais je voulais ajouter quelques observations qui n'ont pas été couvertes dans les autres réponses ou commentaires.

  1. La question suppose implicitement que nous parlons d'un langage orienté objet de style Simula, qui est lui-même limitant. Dans la plupart des langues, même celles avec des objets, tout n'est pas un objet. Le mécanisme pour implémenter les destructeurs imposerait un coût que tous les implémenteurs de langues ne sont pas prêts à payer.

  2. C ++ a des garanties implicites sur l'ordre de destruction. Si vous avez une structure de données arborescente, par exemple, les enfants seront détruits avant le parent. Ce n'est pas le cas dans les langues GC'd, donc les ressources hiérarchiques peuvent être publiées dans un ordre imprévisible. Pour les ressources autres que la mémoire, cela peut être important.

Pseudonyme
la source
2

Lorsque les deux cadres GC les plus populaires (Java et .NET) ont été conçus, je pense que les auteurs s'attendaient à ce que la finalisation fonctionne suffisamment bien pour éviter le besoin d'autres formes de gestion des ressources. De nombreux aspects de la conception du langage et du framework peuvent être grandement simplifiés s'il n'est pas nécessaire de disposer de toutes les fonctionnalités nécessaires pour permettre une gestion des ressources 100% fiable et déterministe. En C ++, il est nécessaire de distinguer les concepts de:

  1. Pointeur / référence qui identifie un objet qui appartient exclusivement au titulaire de la référence et qui n'est identifié par aucun pointeur / référence que le propriétaire ne connaît pas.

  2. Pointeur / référence identifiant un objet partageable qui n'appartient à personne.

  3. Pointeur / référence qui identifie un objet qui est exclusivement détenu par le titulaire de la référence, mais qui peut être accessible par des « vues » le propriétaire n'a aucun moyen de suivi.

  4. Pointeur / référence qui identifie un objet qui fournit une vue d'un objet qui appartient à quelqu'un d'autre.

Si un langage / framework GC n'a pas à se soucier de la gestion des ressources, tout ce qui précède peut être remplacé par un seul type de référence.

Je trouverais naïve l'idée que la finalisation éliminerait le besoin d'autres formes de gestion des ressources, mais que cette attente soit ou non raisonnable à l'époque, l'histoire a depuis montré qu'il existe de nombreux cas qui nécessitent une gestion des ressources plus précise que celle fournie par la finalisation. . Il se trouve que je pense que les récompenses de la reconnaissance de la propriété au niveau de la langue / du cadre seraient suffisantes pour justifier le coût (la complexité doit exister quelque part, et le déplacer vers le langage / le cadre simplifierait le code utilisateur), mais je reconnais qu'il existe d'importantes les avantages de la conception d'avoir un seul "type" de référence - quelque chose qui ne fonctionne que si le langage / le framework est indépendant des problèmes de nettoyage des ressources.

supercat
la source
2

Pourquoi le paradigme destructeur d'objets dans les langues de collecte des ordures est-il omniprésent?

Je viens d'un milieu C ++, donc ce domaine est déroutant pour moi.

Le destructeur en C ++ fait en fait deux choses combinées. Il libère de la RAM et libère des identifiants de ressources.

D'autres langues séparent ces préoccupations en confiant au GC la responsabilité de libérer la RAM tandis qu'une autre fonctionnalité linguistique prend en charge la libération des identifiants des ressources.

Je trouve extrêmement insuffisant que ces langages considèrent la mémoire comme la seule ressource à gérer.

C'est à cela que servent les GC. Ils ne font qu'une chose et c'est pour s'assurer que vous ne manquiez pas de mémoire. Si la RAM est infinie, tous les GC seraient retirés car il n'y a plus de raison réelle d'exister.

Qu'en est-il des sockets, des descripteurs de fichiers, des états d'application?

Les langues peuvent fournir différentes manières de libérer les identifiants de ressource en:

  • manuel .CloseOrDispose()dispersé dans le code

  • manuel .CloseOrDispose()dispersé dans un " finallybloc" manuel

  • manuelle « blocs id ressources » (c. -à using, with, try-Avec-ressources , etc.) qui automatise .CloseOrDispose()après le bloc effectuées

  • "blocs d'identification des ressources" garantis qui automatisent une.CloseOrDispose() fois le bloc terminé

De nombreuses langues utilisent des mécanismes manuels (par opposition à garantis) qui créent une opportunité de mauvaise gestion des ressources. Prenez ce simple code NodeJS:

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

..où le programmeur a oublié de fermer le fichier ouvert.

Tant que le programme continue de fonctionner, le fichier ouvert est bloqué dans les limbes. Ceci est facile à vérifier en essayant d'ouvrir le fichier à l'aide de HxD et en vérifiant que cela ne peut pas être fait:

entrez la description de l'image ici

La libération des ID de ressource dans les destructeurs C ++ n'est pas non plus garantie. Vous pourriez penser que RAII fonctionne comme des "blocs d'ID de ressource" garantis, mais contrairement aux "blocs d'ID de ressource", le langage C ++ n'empêche pas l'objet fournissant le bloc RAII de fuir , donc le bloc RAII peut ne jamais être fait .


Il semble que presque tous les langages modernes récupérés avec la prise en charge des objets OOPy comme Ruby, Javascript / ES6 / ES7, Actionscript, Lua, etc. omettent complètement le paradigme destructeur / finaliser. Python semble être le seul avec sa __del__()méthode de classe . Pourquoi est-ce?

Parce qu'ils gèrent les identifiants de ressource en utilisant d'autres méthodes, comme mentionné ci-dessus.

Quelles sont les décisions de conception de langage qui font que ces langages n'ont aucun moyen d'exécuter une logique personnalisée lors de l'élimination des objets?

Parce qu'ils gèrent les identifiants de ressource en utilisant d'autres méthodes, comme mentionné ci-dessus.

Pourquoi un langage aurait-il le concept intégré d'instances d'objets avec des structures de classe ou de classe avec une instanciation personnalisée (constructeurs), tout en omettant complètement la fonctionnalité de destruction / finalisation?

Parce qu'ils gèrent les identifiants de ressource en utilisant d'autres méthodes, comme mentionné ci-dessus.

Je pouvais voir un argument possible étant que le destructeur / finaliseur pourrait ne pas être appelé avant un moment indéterminé à l'avenir, mais cela n'a pas empêché Java ou Python de prendre en charge la fonctionnalité.

Java n'a pas de destructeurs.

Les documents Java mentionnent :

le but habituel de la finalisation, cependant, est d'effectuer des actions de nettoyage avant que l'objet ne soit irrévocablement rejeté. Par exemple, la méthode de finalisation d'un objet qui représente une connexion d'entrée / sortie peut effectuer des transactions d'E / S explicites pour interrompre la connexion avant que l'objet ne soit définitivement supprimé.

..mais mettre le code de gestion des identifiants de ressource à l'intérieur Object.finalizerest largement considéré comme un anti-modèle ( cf. ). Ce code devrait plutôt être écrit sur le site de l'appel.

Pour les personnes qui utilisent l'anti-modèle, leur justification est qu'elles ont peut-être oublié de libérer les identifiants de ressource sur le site d'appel. Ainsi, ils recommencent dans le finaliseur, au cas où.

Quelles sont les principales raisons de conception du langage pour ne prendre en charge aucune forme de finalisation d'objet?

Il n'y a pas beaucoup de cas d'utilisation pour les finaliseurs car ils servent à exécuter un morceau de code entre le moment où il n'y a plus de références fortes à l'objet et le moment où sa mémoire est récupérée par le GC.

Un cas d'utilisation possible est lorsque vous souhaitez conserver un journal du temps entre la collecte de l'objet par le GC et le moment où il n'y a plus de références fortes à l'objet, en tant que tel:

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

trouvé une référence à ce sujet dans le Dr Dobbs wrt c ++ qui a des idées plus générales qui soutiennent que les destructeurs sont problématiques dans un langage où ils sont implémentés. une idée approximative semble être ici que l'un des principaux objectifs des destructeurs est de gérer la désallocation de mémoire, ce qui est difficile à réaliser correctement. la mémoire est allouée par morceaux mais différents objets sont connectés et la responsabilité / les limites de désallocation ne sont pas si claires.

donc la solution à cela d'un garbage collector a évolué il y a des années, mais le garbage collection n'est pas basé sur des objets disparaissant de la portée aux sorties de méthode (c'est une idée conceptuelle qui est difficile à implémenter), mais sur un collecteur fonctionnant périodiquement, quelque peu de façon non déterministe, lorsque l'application subit une «pression sur la mémoire» (c'est-à-dire un manque de mémoire).

en d'autres termes, le simple concept humain d'un "objet nouvellement inutilisé" est en fait à certains égards une abstraction trompeuse en ce sens qu'aucun objet ne peut "instantanément" devenir inutilisé. les objets inutilisés ne peuvent être "découverts" qu'en exécutant un algorithme de récupération de place qui parcourt le graphe de référence d'objet et les algorithmes les plus performants s'exécutent par intermittence.

il est possible qu'un meilleur algorithme de collecte des ordures soit à découvrir qui puisse identifier presque instantanément les objets inutilisés, ce qui pourrait alors conduire à un code d'appel destructeur cohérent, mais aucun n'a été trouvé après de nombreuses années de recherche dans le domaine.

la solution aux domaines de gestion des ressources tels que les fichiers ou les connexions semble être d'avoir des «gestionnaires» d'objets qui tentent de gérer leur utilisation.

vzn
la source
2
Trouvaille intéressante. Merci. L'argument de l'auteur est basé sur le fait que le destructeur est appelé au mauvais moment en raison du passage des instances de classe par valeur lorsque la classe n'a pas de constructeur de copie approprié (ce qui est un vrai problème). Cependant, ce scénario n'existe pas vraiment dans la plupart (sinon la totalité) des langages dynamiques modernes car tout est passé par référence ce qui évite la situation de l'auteur. Bien que ce soit une perspective intéressante, je ne pense pas que cela explique pourquoi la plupart des langages récupérés ont choisi d'omettre la fonctionnalité destructeur / finalisation.
dbcb
2
Cette réponse dénature l'article du Dr Dobb: l'article ne prétend pas que les destructeurs sont problématiques en général. L'article fait valoir ceci: les primitives de gestion de la mémoire sont comme des instructions goto, car elles sont à la fois simples mais trop puissantes. De la même manière que les instructions goto sont mieux encapsulées dans des "structures de contrôle limitées de manière appropriée" (voir: Dijktsra), les primitives de gestion de la mémoire sont mieux encapsulées dans des "structures de données limitées de manière appropriée". Les destructeurs sont un pas dans cette direction, mais pas assez loin. Décidez par vous-même si c'est vrai ou non.
kdbanman