¿Por qué está ausente el paradigma del destructor de objetos en los idiomas recolectados de basura?

27

Buscando una idea de las decisiones sobre el diseño del lenguaje recolectado basura. ¿Quizás un experto en idiomas podría iluminarme? Vengo de un fondo C ++, por lo que esta área es desconcertante para mí.

Parece que casi todos los lenguajes modernos recolectados de basura con soporte de objetos OOPy como Ruby, Javascript / ES6 / ES7, Actionscript, Lua, etc. omiten por completo el paradigma destructor / finalize. Python parece ser el único con su class __del__()método. ¿Por qué es esto? ¿Existen limitaciones funcionales / teóricas dentro de los lenguajes con recolección automática de basura que impiden implementaciones efectivas de un método destructor / finalize en objetos?

Me parece extremadamente escaso que estos lenguajes consideren la memoria como el único recurso que vale la pena administrar. ¿Qué pasa con los sockets, identificadores de archivos, estados de aplicación? Sin la capacidad de implementar una lógica personalizada para limpiar recursos y estados que no sean de memoria en la finalización del objeto, debo llenar mi aplicación con myObject.destroy()llamadas de estilo personalizadas , colocar la lógica de limpieza fuera de mi "clase", romper el intento de encapsulación y relegar mi aplicación a fugas de recursos debido a un error humano en lugar de ser manejado automáticamente por el gc.

¿Cuáles son las decisiones de diseño del lenguaje que hacen que estos lenguajes no tengan ninguna forma de ejecutar una lógica personalizada en la eliminación de objetos? Tengo que imaginar que hay una buena razón. Me gustaría comprender mejor las decisiones técnicas y teóricas que dieron como resultado que estos lenguajes no tengan soporte para la destrucción / finalización de objetos.

Actualizar:

Quizás una mejor manera de formular mi pregunta:

¿Por qué un lenguaje tendría el concepto incorporado de instancias de objeto con clase o estructuras de clase junto con instanciación personalizada (constructores), pero omitiría por completo la funcionalidad de destrucción / finalización? Los idiomas que ofrecen recolección de basura automática parecen ser los principales candidatos para apoyar la destrucción / finalización de objetos, ya que saben con 100% de certeza cuando un objeto ya no está en uso. Sin embargo, la mayoría de esos idiomas no lo admiten.

No creo que sea un caso en el que nunca se llame al destructor, ya que eso sería una pérdida de memoria central, que los gcs están diseñados para evitar. Pude ver un posible argumento que dice que el destructor / finalizador no puede ser llamado hasta cierto tiempo indeterminado en el futuro, pero eso no impidió que Java o Python admitieran la funcionalidad.

¿Cuáles son las razones principales del diseño del lenguaje para no admitir ninguna forma de finalización de objetos?

dbcb
fuente
99
Tal vez porque finalize/ destroyes una mentira? No hay garantía de que alguna vez se ejecute. E, incluso si, no sabe cuándo (dada la recolección automática de basura), y si es necesario, el contexto todavía está allí (puede que ya se haya recolectado). Por lo tanto, es más seguro garantizar un estado coherente de otras maneras, y uno podría obligar al programador a hacerlo.
Raphael
1
Creo que esta pregunta es limítrofe fuera del tema. ¿Es una pregunta de diseño de lenguaje de programación del tipo que queremos entretener, o es una pregunta para un sitio más orientado a la programación? Votos de la comunidad, por favor.
Raphael
14
Es una buena pregunta en el diseño PL, hagámoslo.
Andrej Bauer
3
Esto no es realmente una distinción estática / dinámica. Muchos lenguajes estáticos no tienen finalizadores. De hecho, ¿no son idiomas con finalizadores en minoría?
Andrej Bauer
1
creo que hay alguna pregunta aquí ... sería mejor si definieras los términos un poco más. Java tiene finalmente un bloque que no está vinculado con la destrucción de objetos sino con la salida del método. También hay otras formas de lidiar con los recursos. Por ejemplo, en Java, un grupo de conexiones puede ocuparse de conexiones que no se utilizan [x] en tiempo y reclamarlas. No es elegante pero funciona. Parte de la respuesta a su pregunta es que la recolección de basura es más o menos un proceso no determinista, no instantáneo y no está impulsado por objetos que ya no se utilizan, sino por restricciones / límites de memoria que se activan.
vzn

Respuestas:

10

El patrón del que habla, donde los objetos saben cómo limpiar sus recursos, se divide en tres categorías relevantes. No confundamos destructores con finalizadores : solo uno está relacionado con la recolección de basura:

  • El patrón finalizador : método de limpieza declarado automáticamente, definido por el programador, llamado automáticamente.

    Los finalizadores son llamados automáticamente antes de la desasignación por un recolector de basura. El término se aplica si el algoritmo de recolección de basura empleado puede determinar los ciclos de vida de los objetos.

  • El patrón destructor : método de limpieza declarado automáticamente, definido por el programador, llamado automáticamente solo a veces.

    Se puede llamar a los destructores automáticamente para los objetos asignados a la pila (porque la duración del objeto es determinista), pero se debe invocar explícitamente en todas las rutas de ejecución posibles para los objetos asignados en el montón (porque la duración del objeto no es determinista).

  • El patrón de disposición : método de limpieza declarado, definido y llamado por el programador.

    Los programadores hacen un método de eliminación y lo llaman ellos mismos: aquí es donde myObject.destroy()cae su método personalizado . Si la eliminación es absolutamente necesaria, se debe llamar a los eliminadores en todas las rutas de ejecución posibles.

Los finalizadores son los droides que estás buscando.

El patrón finalizador (el patrón sobre el que pregunta su pregunta) es el mecanismo para asociar objetos con recursos del sistema (sockets, descriptores de archivos, etc.) para la recuperación mutua por parte de un recolector de basura. Pero los finalizadores están fundamentalmente a merced del algoritmo de recolección de basura en uso.

Considere esta suposición suya:

Los idiomas que ofrecen recolección automática de basura ... saben con 100% de certeza cuando un objeto ya no está en uso.

Técnicamente falso (gracias, @babou). La recolección de basura se trata fundamentalmente de memoria, no de objetos. Si un algoritmo de recopilación se da cuenta de que la memoria de un objeto ya no está en uso depende del algoritmo y (posiblemente) de cómo se refieren sus objetos entre sí. Hablemos de dos tipos de recolectores de basura en tiempo de ejecución. Hay muchas formas de alterar y aumentar estas técnicas básicas:

  1. Rastreo GC. Estos rastrean la memoria, no los objetos. A menos que se aumente para hacerlo, no conservan referencias a objetos de la memoria. A menos que se aumenten, estos GC no sabrán cuándo se puede finalizar un objeto, incluso si saben cuándo su memoria es inalcanzable. Por lo tanto, las llamadas finalizadas no están garantizadas.

  2. Recuento de referencia GC . Estos usan objetos para rastrear la memoria. Modelan la accesibilidad de objetos con un gráfico dirigido de referencias. Si hay un ciclo en su gráfico de referencia de objetos, entonces todos los objetos en el ciclo nunca tendrán su finalizador llamado (hasta la finalización del programa, obviamente). Nuevamente, las llamadas al finalizador no están garantizadas.

TLDR

La recolección de basura es difícil y diversa. No se puede garantizar una llamada finalista antes de la finalización del programa.

revs kdbanman
fuente
Tienes razón en que esto no es estático v. Dinámico. Es un problema con los idiomas recolectados de basura. La recolección de basura es un problema complejo y probablemente sea la razón principal, ya que hay muchos casos extremos a considerar (por ejemplo, ¿qué sucede si la lógica finalize()hace que se vuelva a hacer referencia al objeto que se está limpiando?). Sin embargo, no poder garantizar que se llame al finalizador antes de la finalización del programa no impidió que Java lo admitiera. No digo que tu respuesta sea incorrecta, quizás incompleta. Sigue siendo una muy buena publicación. Gracias.
dbcb
Gracias por la respuesta. Aquí hay un intento de completar mi respuesta: al omitir explícitamente los finalizadores, un lenguaje obliga a sus usuarios a administrar sus propios recursos. Para muchos tipos de problemas, eso es probablemente una desventaja. Personalmente, prefiero la elección de Java, porque tengo el poder de los finalizadores y no hay nada que me impida escribir y usar mi propio eliminador. Java dice: "Oye, programador. No eres un idiota, así que aquí hay un finalizador. Solo ten cuidado".
kdbanman
1
Actualicé mi pregunta original para reflejar que se trata de idiomas recolectados de basura. Aceptando tu respuesta. Gracias por tomarte el tiempo de responder.
dbcb
Feliz de ayudar. ¿La aclaración de mi comentario hizo que mi respuesta fuera más clara?
kdbanman
2
Es bueno. Para mí, la respuesta real aquí es que los idiomas eligen no implementarlo porque el valor percibido no supera los problemas de implementación de la funcionalidad. No es imposible (como demuestran Java y Python), pero hay una compensación que muchos lenguajes eligen no hacer.
dbcb
5

En una palabra

La finalización no es un asunto simple que deben manejar los recolectores de basura. Es fácil de usar con GC de conteo de referencias, pero esta familia de GC a menudo es incompleta, lo que requiere que las fugas de memoria sean compensadas por un disparo explícito de destrucción y finalización de algunos objetos y estructuras. Los recolectores de basura de rastreo son mucho más efectivos, pero hacen que sea mucho más difícil identificar el objeto que se finalizará y destruirá, en lugar de solo identificar la memoria no utilizada, lo que requiere una administración más compleja, con un costo en tiempo y espacio, y en complejidad de la implementación.

Introducción

Supongo que lo que está preguntando es por qué los idiomas recolectados de basura no manejan automáticamente la destrucción / finalización dentro del proceso de recolección de basura, como lo indica el comentario:

Me parece extremadamente escaso que estos lenguajes consideren la memoria como el único recurso que vale la pena administrar. ¿Qué pasa con los sockets, identificadores de archivos, estados de aplicación?

No estoy de acuerdo con la respuesta aceptada dada por kdbanman . Si bien los hechos indicados son en su mayoría correctos, aunque están fuertemente sesgados hacia el conteo de referencias, no creo que expliquen adecuadamente la situación de la que se queja en la pregunta.

No creo que la terminología desarrollada en esa respuesta sea un gran problema, y ​​es más probable que confunda las cosas. De hecho, como se presenta, la terminología está determinada principalmente por la forma en que se activan los procedimientos y no por lo que hacen. El punto es que, en todos los casos, existe la necesidad de finalizar un objeto que ya no se necesita con algún proceso de limpieza y liberar cualquier recurso que haya estado utilizando, la memoria es solo uno de ellos. Idealmente, todo debe hacerse automáticamente cuando el objeto ya no se use, por medio de un recolector de basura. En la práctica, GC puede estar ausente o tener deficiencias, y esto se compensa con la activación explícita del programa de finalización y recuperación.

El trigerring explícito por parte del programa es un problema, ya que puede permitir errores de programación difíciles de analizar, cuando un objeto que todavía está en uso se termina explícitamente.

Por lo tanto, es mucho mejor confiar en la recolección automática de basura para recuperar recursos. Pero hay dos problemas:

  • alguna técnica de recolección de basura permitirá pérdidas de memoria que evitarán la recuperación total de los recursos. Esto es bien conocido para el conteo de referencias GC, pero puede aparecer para otras técnicas de GC cuando se usan algunas organizaciones de datos sin cuidado (punto no discutido aquí).

  • Si bien la técnica GC puede ser buena para identificar los recursos de memoria que ya no se utilizan, finalizar los objetos contenidos en él puede no ser simple, y eso complica el problema de recuperar otros recursos utilizados por estos objetos, que a menudo es el propósito de la finalización.

Finalmente, un punto importante que a menudo se olvida es que los ciclos de GC pueden ser activados por cualquier cosa, no solo por la escasez de memoria, si se proporcionan los ganchos adecuados y si se considera que el costo de un ciclo de GC vale la pena. Por lo tanto, está perfectamente bien iniciar un GC cuando falta algún tipo de recurso, con la esperanza de liberar algunos.

Colectores de basura de conteo de referencia

El conteo de referencias es una técnica de recolección de basura débil , que no manejará los ciclos correctamente. De hecho, sería débil al destruir estructuras obsoletas y reclamar otros recursos simplemente porque es débil al reclamar memoria. Pero los finalizadores se pueden usar más fácilmente con un recolector de basura de conteo de referencia (GC), ya que un GC de recuento de ref reclama una estructura cuando su recuento de ref se reduce a 0, momento en el que su dirección se conoce junto con su tipo, ya sea estáticamente o dinámicamente Por lo tanto, es posible recuperar la memoria con precisión después de aplicar el finalizador adecuado y llamar de forma recursiva al proceso en todos los objetos puntiagudos (posiblemente a través del procedimiento de finalización).

En pocas palabras, la finalización es fácil de implementar con Ref Counting GC, pero sufre de la "incompletitud" de ese GC, de hecho debido a estructuras circulares, precisamente en la misma medida que sufre la recuperación de memoria. En otras palabras, con el recuento de referencias, la memoria está precisamente tan mal gestionada como otros recursos como sockets, identificadores de archivos, etc.

De hecho, la incapacidad de Ref Count GC para recuperar estructuras en bucle (en general) puede verse como una pérdida de memoria . No puede esperar que todos los GC eviten pérdidas de memoria. Depende del algoritmo de GC y de la información de estructura de tipo disponible dinámicamente (por ejemplo, en GC conservador ).

Rastreando recolectores de basura

La familia más poderosa de GC, sin tales fugas, es la familia de rastreo que explora las partes vivas de la memoria, comenzando por punteros de raíz bien identificados. Todas las partes de la memoria que no se visitan en este proceso de rastreo (que en realidad se pueden descomponer de varias maneras, pero tengo que simplificar) son partes no utilizadas de la memoria que se pueden recuperar 1 . Estos recolectores reclamarán todas las partes de la memoria a las que el programa ya no puede acceder, sin importar lo que haga. Reclama estructuras circulares, y los GC más avanzados se basan en alguna variación de este paradigma, a veces altamente sofisticado. Se puede combinar con el recuento de referencias en algunos casos y compensar sus debilidades.

Un problema es que su declaración (al final de la pregunta):

Los idiomas que ofrecen recolección de basura automática parecen ser los principales candidatos para apoyar la destrucción / finalización de objetos, ya que saben con 100% de certeza cuando un objeto ya no está en uso.

es técnicamente incorrecto para rastrear colectores.

Lo que se sabe con 100% de certeza es qué partes de la memoria ya no están en uso . (Más precisamente, debe decirse que ya no son accesibles , porque algunas partes, que ya no se pueden usar de acuerdo con la lógica del programa, todavía se consideran en uso si todavía hay un puntero inútil en el programa datos.) Pero se necesitan más procesamiento y estructuras apropiadas para saber qué objetos no utilizados pueden haberse almacenado en estas partes de la memoria ahora no utilizadas . Esto no se puede determinar a partir de lo que se conoce del programa, ya que el programa ya no está conectado a estas partes de la memoria.

Por lo tanto, después de un paso de recolección de basura, le quedan fragmentos de memoria que contienen objetos que ya no están en uso, pero a priori no hay forma de saber cuáles son estos objetos para aplicar la finalización correcta. Además, si el colector de rastreo es del tipo de marcado y barrido, puede ser que algunos de los fragmentos puedan contener objetos que ya se hayan finalizado en un pase de GC anterior, pero que no se utilizaron desde entonces por razones de fragmentación. Sin embargo, esto puede tratarse con una escritura explícita extendida.

Si bien un simple recopilador solo reclamaría estos fragmentos de memoria, sin más preámbulos, la finalización requiere un pase específico para explorar esa memoria no utilizada, identificar los objetos allí contenidos y aplicar los procedimientos de finalización. Pero tal exploración requiere la determinación del tipo de objetos que se almacenaron allí, y la determinación del tipo también es necesaria para aplicar la finalización adecuada, si corresponde.

Eso implica costos adicionales en el tiempo de GC (el pase adicional) y posiblemente costos adicionales de memoria para hacer que la información de tipo adecuada esté disponible durante ese pase mediante diversas técnicas. Estos costos pueden ser significativos, ya que a menudo uno solo querrá finalizar unos pocos objetos, mientras que el tiempo y el espacio de arriba podrían afectar a todos los objetos.

Otro punto es que la sobrecarga de tiempo y espacio puede referirse a la ejecución del código del programa, y ​​no solo a la ejecución del GC.

No puedo dar una respuesta más precisa, señalando problemas específicos, porque no conozco los detalles de muchos de los idiomas que enumera. En el caso de C, escribir es un tema muy difícil que conduce al desarrollo de coleccionistas conservadores. Supongo que esto también afecta a C ++, pero no soy un experto en C ++. Esto parece ser confirmado por Hans Boehm, quien realizó gran parte de la investigación sobre GC conservador. El GC conservador no puede reclamar sistemáticamente toda la memoria no utilizada precisamente porque puede carecer de información de tipo precisa sobre los datos. Por la misma razón, no podría aplicar sistemáticamente los procedimientos de finalización.

Por lo tanto, es posible hacer lo que está pidiendo, como sabe de algunos idiomas. Pero no viene gratis. Dependiendo del idioma y su implementación, puede implicar un costo incluso si no utiliza la función. Se pueden considerar varias técnicas y compensaciones para abordar estos problemas, pero eso está más allá del alcance de una respuesta de tamaño razonable.

1: esta es una presentación abstracta de la colección de rastreo (que abarca tanto GC como copiar y marcar y barrer), las cosas varían según el tipo de recopilador de rastreo, y explorar la parte no utilizada de la memoria es diferente, dependiendo de si copiar o marcar y Se utiliza el barrido.

babou
fuente
Usted da muchos detalles excelentes sobre la recolección de basura. Sin embargo, su respuesta en realidad no está en desacuerdo con la mía: su resumen y mi TLDR esencialmente dicen lo mismo. Y para lo que vale, mi respuesta utiliza el conteo de referencias GC como ejemplo, no un "sesgo fuerte".
kdbanman
Después de leer más a fondo, veo el desacuerdo. Lo editaré en consecuencia. Además, mi terminología debía ser inequívoca. La pregunta era combinar finalizadores y destructores, e incluso mencionar los trituradores en el mismo aliento. Vale la pena difundir las palabras correctas.
kdbanman
@kdbanman La dificultad era que me dirigía a ustedes dos, ya que su respuesta era la referencia. No puede usar el recuento de referencias como un ejemplo paradigmático porque es un GC débil, que rara vez se usa en idiomas (verifique los idiomas citados por el OP), para los cuales agregar finalizadores sería realmente fácil (pero con un uso limitado). Los colectores de rastreo casi siempre se usan. Pero los finalizadores son difíciles de enganchar, porque no se conocen los objetos moribundos (al contrario de lo que usted considera correcto). La distinción entre la tipificación estática y dinámica es irrelevante, ya que la tipificación dinámica del almacén de datos es esencial.
babou
@kdbanman Respecto a la terminología, es útil en general, ya que corresponde a diferentes situaciones. Pero aquí no ayuda, ya que la pregunta es sobre la transferencia de la finalización al GC. Se supone que el GC básico solo hace la destrucción. Lo que se necesita es una terminología que distinga getting memory recycled, a la que llamo reclamation, y que haga algo de limpieza antes de eso, como reclamar otros recursos o actualizar algunas tablas de objetos, a las que llamo finalization. Estos me parecieron los problemas relevantes, pero es posible que haya perdido un punto en su terminología, que era nuevo para mí.
babou
1
Gracias @kdbanman, babou. Buena discusión Creo que ambas publicaciones cubren puntos similares. Como ambos señalan, el tema central parece ser la categoría de recolector de basura empleado en el tiempo de ejecución del lenguaje. Encontré este artículo , que aclara algunas ideas falsas para mí. Parece que los gcs más robustos solo manejan memoria sin procesar de bajo nivel, lo que hace que los tipos de objetos de mayor nivel sean opacos para el gc. Sin el conocimiento de la memoria interna, el gc no puede destruir objetos. Cuál parece ser tu conclusión.
dbcb
4

El patrón destructor de objetos es fundamental para el manejo de errores en la programación de sistemas, pero no tiene nada que ver con la recolección de basura. Más bien, tiene que ver con hacer coincidir la vida útil del objeto con un ámbito, y puede implementarse / usarse en cualquier lenguaje que tenga funciones de primera clase.

Ejemplo (pseudocódigo). Supongamos que tiene un tipo de "archivo sin formato", como el tipo de descriptor de archivo Posix. Hay cuatro operaciones fundamentales, open(), close(), read(), write(). Desea implementar un tipo de archivo "seguro" que siempre se limpia después de sí mismo. (Es decir, que tiene un constructor y destructor automático).

Asumiré que nuestro idioma tiene un manejo de excepciones throw, tryy finally(en idiomas sin manejo de excepciones, puede configurar una disciplina en la que el usuario de su tipo devuelva un valor especial para indicar un error).

Configura una función que acepta una función que hace el trabajo. La función de trabajo acepta un argumento (un identificador para el archivo "seguro").

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)

También proporciona implementaciones de read()y write()para safe_file(que solo llaman a raw_file read()y write()). Ahora el usuario usa el safe_filetipo de esta manera:

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

Un destructor de C ++ es solo azúcar sintáctico para un try-finallybloque. Casi todo lo que he hecho aquí es convertir en lo que safe_filese compilaría una clase C ++ con un constructor y un destructor. Tenga en cuenta que C ++ no tiene finallysus excepciones, específicamente porque Stroustrup sintió que usar un destructor explícito era mejor sintácticamente (y lo introdujo en el lenguaje antes de que el lenguaje tuviera funciones anónimas).

(Esta es una simplificación de una de las formas en que las personas han estado manejando errores en lenguajes similares a Lisp durante muchos años. Creo que me topé por primera vez a fines de los 80 o principios de los 90, pero no recuerdo dónde).

Lógica Errante
fuente
Esto describe los aspectos internos del patrón destructor basado en pila en C ++, pero no explica por qué un lenguaje recolectado basura no implementaría tal funcionalidad. Puede tener razón en que esto no tiene nada que ver con la recolección de basura, pero está relacionado con la destrucción / finalización general de objetos, que parece ser difícil o ineficiente en los idiomas recolectados de basura. Entonces, si la destrucción general no es compatible, la destrucción basada en la pila también parece omitirse.
dbcb
Como dije al principio: cualquier lenguaje recolectado de basura que tenga funciones de primera clase (o alguna aproximación de las funciones de primera clase) le brinda la capacidad de proporcionar interfaces "a prueba de balas" como safe_filey with_file_opened_for_read(un objeto que se cierra cuando sale del alcance ) Eso es lo importante, que no tenga la misma sintaxis que los constructores es irrelevante. Lisp, Scheme, Java, Scala, Go, Haskell, Rust, Javascript, Clojure soportan suficientes funciones de primera clase, por lo que no necesitan destructores para proporcionar la misma característica útil.
Lógica errante
Creo que veo lo que estás diciendo. Como los lenguajes proporcionan los componentes básicos (prueba / captura / finalmente, funciones de primera clase, etc.) para implementar manualmente una funcionalidad similar a un destructor, ¿no necesitan destructores? Pude ver algunos idiomas tomando esa ruta por razones de simplicidad. Aunque, parece poco probable que esa sea la razón principal de todos los idiomas enumerados, pero tal vez eso es lo que es. Tal vez solo pertenezco a la gran minoría que ama los destructores C ++ y a nadie más le importa, lo cual podría ser la razón por la cual la mayoría de los lenguajes no implementan destructores. Simplemente no les importa.
dbcb
2

Esta no es una respuesta completa a la pregunta, pero quería agregar un par de observaciones que no se han cubierto en las otras respuestas o comentarios.

  1. La pregunta supone implícitamente que estamos hablando de un lenguaje orientado a objetos al estilo Simula, que en sí mismo es limitante. En la mayoría de los idiomas, incluso aquellos con objetos, no todo es un objeto. La maquinaria para implementar destructores impondría un costo que no todos los implementadores de idiomas están dispuestos a pagar.

  2. C ++ tiene algunas garantías implícitas sobre el orden de destrucción. Si tiene una estructura de datos en forma de árbol, por ejemplo, los elementos secundarios se destruirán antes que los elementos primarios. Este no es el caso en los lenguajes GC'd, por lo que los recursos jerárquicos pueden liberarse en un orden impredecible. Para recursos que no son de memoria, esto puede importar.

Seudónimo
fuente
2

Cuando se diseñaron los dos marcos GC más populares (Java y .NET), creo que los autores esperaban que la finalización funcionara lo suficientemente bien como para evitar la necesidad de otras formas de gestión de recursos. Muchos aspectos del diseño del lenguaje y el marco pueden simplificarse enormemente si no se necesitan todas las características necesarias para acomodar una gestión de recursos 100% confiable y determinista. En C ++, es necesario distinguir entre los conceptos de:

  1. Puntero / referencia que identifica un objeto que es propiedad exclusiva del titular de la referencia, y que no está identificado por ningún puntero / referencia que el propietario no conozca.

  2. Puntero / referencia que identifica un objeto compartible que no es propiedad exclusiva de nadie.

  3. Puntero / referencia que identifica un objeto que es propiedad exclusiva del titular de la referencia, pero al que puede accederse a través de "vistas", el propietario no tiene forma de seguimiento.

  4. Puntero / referencia que identifica un objeto que proporciona una vista de un objeto que es propiedad de otra persona.

Si un lenguaje / marco de GC no tiene que preocuparse por la gestión de recursos, todo lo anterior se puede reemplazar por un solo tipo de referencia.

Me parecería ingenua la idea de que la finalización eliminaría la necesidad de otras formas de gestión de recursos, pero ya sea que tal expectativa fuera o no razonable en ese momento, la historia ha demostrado que hay muchos casos que requieren una gestión de recursos más precisa que la que proporciona la finalización. . Creo que las recompensas de reconocer la propiedad a nivel de lenguaje / marco serían suficientes para justificar el costo (la complejidad debe existir en algún lugar, y moverlo al idioma / marco simplificaría el código del usuario), pero reconozco que hay importantes El diseño se beneficia de tener un solo "tipo" de referencia, algo que solo funciona si el lenguaje / marco es independiente de los problemas de limpieza de recursos.

Super gato
fuente
2

¿Por qué está ausente el paradigma del destructor de objetos en los idiomas recolectados de basura?

Vengo de un fondo C ++, por lo que esta área es desconcertante para mí.

El destructor en C ++ en realidad hace dos cosas combinadas. Libera RAM y libera identificadores de recursos.

Otros idiomas separan estas preocupaciones haciendo que el GC se encargue de liberar RAM, mientras que otra función de idioma se encarga de liberar los identificadores de recursos.

Me parece extremadamente escaso que estos lenguajes consideren la memoria como el único recurso que vale la pena administrar.

De eso se tratan los GC. Solo hacen una cosa y es asegurarse de que no se quede sin memoria. Si la RAM es infinita, todos los GC se retirarían ya que ya no hay ninguna razón real para que existan.

¿Qué pasa con los sockets, identificadores de archivos, estados de aplicación?

Los idiomas pueden proporcionar diferentes formas de liberar identificadores de recursos mediante:

  • manual .CloseOrDispose()disperso en el código

  • manual .CloseOrDispose()disperso dentro del " finallybloque" manual

  • manuales "bloques de ID de recurso" (es decir using, with, try-con-recursos , etc.) que automatiza .CloseOrDispose()después de que el bloque se realiza

  • "bloques de identificación de recursos" garantizados que se automatizan.CloseOrDispose() después de que se realiza el bloque

Muchos idiomas utilizan mecanismos manuales (en lugar de garantizados) que crean una oportunidad para la mala gestión de los recursos. Tome este simple código NodeJS:

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

..donde el programador ha olvidado cerrar el archivo abierto.

Mientras el programa siga ejecutándose, el archivo abierto quedaría atascado en el limbo. Esto es fácil de verificar al intentar abrir el archivo usando HxD y verificar que no se puede hacer:

ingrese la descripción de la imagen aquí

La liberación de identificadores de recursos en destructores de C ++ tampoco está garantizada. Se podría pensar RAII funciona como garantizados "bloques" ID de recurso, sin embargo, a diferencia de los "bloques" ID de recurso, el lenguaje C ++ no se detiene el objeto proporcionar el bloque RAII de ser filtrado , por lo que el bloque de RAII no puede ser hecho .


Parece que casi todos los lenguajes modernos recolectados de basura con soporte de objetos OOPy como Ruby, Javascript / ES6 / ES7, Actionscript, Lua, etc. omiten por completo el paradigma destructor / finalize. Python parece ser el único con su __del__()método de clase . ¿Por qué es esto?

Porque administran los identificadores de recursos de otras maneras, como se mencionó anteriormente.

¿Cuáles son las decisiones de diseño del lenguaje que hacen que estos lenguajes no tengan ninguna forma de ejecutar una lógica personalizada en la eliminación de objetos?

Porque administran los identificadores de recursos de otras maneras, como se mencionó anteriormente.

¿Por qué un lenguaje tendría el concepto incorporado de instancias de objeto con clase o estructuras de clase junto con instanciación personalizada (constructores), pero omitiría por completo la funcionalidad de destrucción / finalización?

Porque administran los identificadores de recursos de otras maneras, como se mencionó anteriormente.

Pude ver un posible argumento que dice que el destructor / finalizador no puede ser llamado hasta cierto tiempo indeterminado en el futuro, pero eso no impidió que Java o Python admitieran la funcionalidad.

Java no tiene destructores.

Los documentos de Java mencionan :

Sin embargo, el propósito habitual de finalizar es realizar acciones de limpieza antes de que el objeto se descarte irrevocablemente. Por ejemplo, el método de finalización para un objeto que representa una conexión de entrada / salida podría realizar transacciones de E / S explícitas para romper la conexión antes de que el objeto se descarte permanentemente.

... pero poner el código de gestión de identificación de recursos dentro Object.finalizerse considera en gran medida como un antipatrón ( cf. ). Esos códigos deberían escribirse en el sitio de la llamada.

Para las personas que usan el antipatrón, su justificación es que podrían haberse olvidado de liberar los identificadores de recursos en el sitio de la llamada. Por lo tanto, lo hacen de nuevo en el finalizador, por si acaso.

¿Cuáles son las razones principales del diseño del lenguaje para no admitir ninguna forma de finalización de objetos?

No hay muchos casos de uso para los finalizadores, ya que son para ejecutar un fragmento de código entre el momento en que ya no hay referencias fuertes al objeto y el momento en que el GC reclama su memoria.

Un posible caso de uso es cuando desea mantener un registro del tiempo entre el objeto recopilado por el GC y el momento en que ya no hay referencias fuertes al objeto, como tal:

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

Encontré una referencia sobre esto en el Dr. Dobbs wrt c ++ que tiene ideas más generales que argumenta que los destructores son problemáticos en un lenguaje donde se implementan. Una idea aproximada aquí parece ser que un propósito principal de los destructores es manejar la desasignación de memoria, y eso es difícil de lograr correctamente. la memoria se asigna por partes, pero se conectan diferentes objetos y luego la responsabilidad / límites de desasignación no son tan claros.

así que la solución a esto de un recolector de basura evolucionó hace años, pero la recolección de basura no se basa en objetos que desaparecen del alcance en las salidas del método (esa es una idea conceptual que es difícil de implementar), sino en un recolector que se ejecuta periódicamente, algo no determinista, cuando la aplicación experimenta "presión de memoria" (es decir, quedarse sin memoria).

en otras palabras, el mero concepto humano de un "objeto recién no utilizado" es, de alguna manera, una abstracción engañosa en el sentido de que ningún objeto puede "instantáneamente" quedar sin usar. los objetos no utilizados solo se pueden "descubrir" ejecutando un algoritmo de recolección de basura que atraviese el gráfico de referencia del objeto y los algoritmos de mejor desempeño se ejecuten de manera intermitente.

es posible que haya un mejor algoritmo de recolección de basura a la espera de ser descubierto que pueda identificar casi instantáneamente objetos no utilizados, lo que podría conducir a un código de llamada de destructor consistente, pero no se ha encontrado uno después de muchos años de investigación en el área.

La solución para las áreas de administración de recursos, como archivos o conexiones, parece ser tener "administradores" de objetos que intenten manejar su uso.

vzn
fuente
2
Interesante hallazgo. Gracias. El argumento del autor se basa en que se llama al destructor en el momento equivocado debido a que pasa instancias de clase por valor donde la clase no tiene un constructor de copia adecuado (lo cual es un problema real). Sin embargo, este escenario realmente no existe en la mayoría (si no todos) los lenguajes dinámicos modernos porque todo se pasa por referencia, lo que evita la situación del autor. Aunque esta es una perspectiva interesante, no creo que explique por qué la mayoría de los lenguajes recolectados de basura han optado por omitir la funcionalidad de destrucción / finalización.
dbcb
2
Esta respuesta tergiversa el artículo del Dr. Dobb: el artículo no argumenta que los destructores son problemáticos en general. El artículo realmente argumenta esto: las primitivas de administración de memoria son como declaraciones de goto, porque ambas son simples pero demasiado poderosas. De la misma manera que las declaraciones goto se encapsulan mejor en "estructuras de control adecuadamente limitadas" (Ver: Dijktsra), las primitivas de administración de memoria se encapsulan mejor en "estructuras de datos adecuadamente limitadas". Los destructores son un paso en esta dirección, pero no lo suficientemente lejos. Decide por ti mismo si eso es cierto o no.
kdbanman