En la programación orientada a objetos , el patrón de eliminación es un patrón de diseño para la gestión de recursos . En este patrón, un recurso está en manos de un objeto , y puesto en libertad por llamar a un convencional método - por lo general llama close
, dispose
, free
, release
dependiendo del idioma - que libera cualquier recurso que el objeto se aferra. Muchos lenguajes ofrecen construcciones de lenguaje para evitar tener que llamar al método dispose explícitamente en situaciones comunes.
El patrón de eliminación se usa principalmente en lenguajes cuyo entorno de ejecución tiene recolección automática de basura (vea la motivación a continuación).
Motivación
Envolviendo recursos en objetos
Envolver recursos en objetos es la forma de encapsulación orientada a objetos y subyace al patrón de eliminación.
Los recursos suelen estar representados por identificadores (referencias abstractas), concretamente normalmente números enteros, que se utilizan para comunicarse con un sistema externo que proporciona el recurso. Por ejemplo, los archivos los proporciona el sistema operativo (específicamente el sistema de archivos ), que en muchos sistemas representa archivos abiertos con un descriptor de archivo (un número entero que representa el archivo).
Estos identificadores se pueden usar directamente, almacenando el valor en una variable y pasándolo como argumento a las funciones que usan el recurso. Sin embargo, con frecuencia es útil abstraerse del identificador en sí (por ejemplo, si diferentes sistemas operativos representan archivos de manera diferente) y almacenar datos auxiliares adicionales con el identificador, de modo que los identificadores se puedan almacenar como un campo en un registro , junto con otros datos; si se trata de un tipo de datos opaco , entonces se oculta la información y se abstrae al usuario de la representación real.
Por ejemplo, en la entrada / salida de archivos C , los archivos se representan mediante objetos del FILE
tipo (confusamente denominados " identificadores de archivo ": son una abstracción a nivel de idioma), que almacena un identificador (del sistema operativo) en el archivo (como un descriptor de archivo ), junto con información auxiliar como el modo de E / S (lectura, escritura) y la posición en el flujo. Estos objetos se crean llamando fopen
(en términos orientados a objetos, un constructor ), que adquiere el recurso y le devuelve un puntero; el recurso se libera llamando fclose
a un puntero al FILE
objeto. [1] En código:
ARCHIVO * f = fopen ( nombre de archivo , modo ); // Haz algo con f. fclose ( f );
Tenga en cuenta que fclose
es una función con un FILE *
parámetro. En la programación orientada a objetos, este es en cambio un método de instancia en un objeto de archivo, como en Python:
f = abrir ( nombre de archivo ) # Hacer algo con f. f . cerrar ()
Este es precisamente el patrón de disposición, y solo difiere en sintaxis y estructura de código [a] de la apertura y cierre de archivos tradicionales. Otros recursos se pueden administrar exactamente de la misma manera: se adquieren en un constructor o fábrica y se liberan mediante un método close
o explícito dispose
.
Liberación inmediata
El problema fundamental que la liberación de recursos pretende resolver es que los recursos son costosos (por ejemplo, puede haber un límite en la cantidad de archivos abiertos) y, por lo tanto, deben liberarse rápidamente. Además, a veces se necesita algún trabajo de finalización, particularmente para E / S, como vaciar búferes para garantizar que todos los datos se escriban realmente.
Si un recurso es ilimitado o efectivamente ilimitado, y no es necesaria una finalización explícita, no es importante liberarlo y, de hecho, los programas de corta duración a menudo no liberan recursos explícitamente: debido al tiempo de ejecución corto, es poco probable que agoten los recursos. y dependen del sistema de tiempo de ejecución o del sistema operativo para realizar cualquier finalización.
Sin embargo, en general, se deben administrar los recursos (particularmente para programas de larga duración, programas que usan muchos recursos, o por seguridad, para garantizar que los datos se escriban). La eliminación explícita significa que la finalización y liberación de los recursos es determinista y rápida: el dispose
método no se completa hasta que se realiza.
Una alternativa a requerir la eliminación explícita es vincular la administración de recursos a la vida útil del objeto : los recursos se adquieren durante la creación del objeto y se liberan durante la destrucción del objeto . Este enfoque se conoce como el lenguaje de adquisición de recursos es inicialización (RAII) y se utiliza en lenguajes con gestión de memoria determinista (por ejemplo, C ++ ). En este caso, en el ejemplo anterior, el recurso se adquiere cuando se crea el objeto de archivo, y cuando se sale del alcance de la variable f
, el objeto de archivo al que se f
refiere se destruye y, como parte de esto, se libera el recurso.
RAII se basa en que la vida útil del objeto es determinista; sin embargo, con la administración automática de memoria, la vida útil del objeto no es una preocupación del programador: los objetos se destruyen en algún momento después de que ya no se usan, pero cuando se abstraen. De hecho, la vida útil a menudo no es determinista, aunque puede serlo, sobre todo si se utiliza el recuento de referencias . De hecho, en algunos casos no hay garantía de que los objetos se finalicen alguna vez : cuando el programa finaliza, es posible que no finalice los objetos y, en su lugar, deje que el sistema operativo recupere memoria; si se requiere la finalización (p. ej., para vaciar los búferes), puede producirse una pérdida de datos.
Por lo tanto, al no acoplar la administración de recursos a la vida útil del objeto, el patrón de eliminación permite que los recursos se liberen rápidamente, al tiempo que brinda flexibilidad de implementación para la administración de memoria. El costo de esto es que los recursos deben administrarse manualmente, lo que puede ser tedioso y propenso a errores.
Salida anticipada
Un problema clave con el patrón de eliminación es que si dispose
no se llama al método, se filtra el recurso. Una causa común de esto es la salida anticipada de una función, debido a un regreso anticipado o una excepción.
Por ejemplo:
def func ( nombre de archivo ): f = open ( nombre de archivo ) si a : return x f . cerrar () volver y
Si la función regresa en la primera devolución, el archivo nunca se cierra y el recurso se filtra.
def func ( nombre de archivo ): f = open ( nombre de archivo ) g ( f ) # Haga algo con f que pueda generar una excepción. f . cerrar ()
Si el código que interviene genera una excepción, la función se cierra antes y el archivo nunca se cierra, por lo que se filtra el recurso.
Ambos pueden ser manejados por una try...finally
construcción, lo que asegura que la cláusula finalmente se ejecute siempre al salir:
def func ( nombre de archivo ): intente : f = open ( nombre de archivo ) # Haga algo. finalmente : f . cerrar ()
Más genéricamente:
Recurso de recursos = getResource (); try { // Se ha adquirido el recurso; realizar acciones con el recurso. ... } finalmente { // Liberar recurso, incluso si se lanzó una excepción. recurso . disponer (); }
La try...finally
construcción es necesaria para la seguridad adecuada de excepciones , ya que el finally
bloque permite la ejecución de la lógica de limpieza independientemente de si se lanza una excepción o no en el try
bloque.
Una desventaja de este enfoque es que requiere que el programador agregue explícitamente código de limpieza en un finally
bloque. Esto conduce a un aumento del tamaño del código, y si no lo hace, se producirá una pérdida de recursos en el programa.
Construcciones del lenguaje
Para que el uso seguro del patrón de eliminación sea menos detallado, varios lenguajes tienen algún tipo de soporte integrado para los recursos almacenados y liberados en el mismo bloque de código .
El lenguaje C # presenta la using
declaración [2] que llama automáticamente al Dispose
método en un objeto que implementa la IDisposable
interfaz :
using ( Resource resource = GetResource ()) { // Realizar acciones con el recurso. ... }
que es igual a:
Resource resource = GetResource () try { // Realizar acciones con el recurso. ... } finalmente { // Es posible que el recurso no se haya adquirido o que ya se haya liberado if ( resource ! = null ) (( IDisposable ) resource ). Dispose (); }
De manera similar, el lenguaje Python tiene una with
declaración que se puede usar con un efecto similar con un objeto de administrador de contexto . El protocolo del administrador de contexto requiere implementación __enter__
y __exit__
métodos que son llamados automáticamente por la with
construcción de la declaración, para evitar la duplicación de código que de otro modo ocurriría con el patrón try
/ finally
. [3]
con resource_context_manager () como recurso : # Realizar acciones con el recurso. ... # Realizar otras acciones en las que se garantice la desasignación del recurso. ...
El lenguaje Java introdujo una nueva sintaxis llamada try
-with-resources en Java versión 7. [4] Se puede usar en objetos que implementan la interfaz AutoCloseable (que define el método close ()):
try ( OutputStream x = new OutputStream (...)) { // Hacer algo con x } catch ( IOException ex ) { // Manejar la excepción // El recurso x se cierra automáticamente } // intenta
Problemas
Más allá del problema clave de la correcta gestión de recursos en presencia de devoluciones y excepciones, y la gestión de recursos basada en montones (eliminación de objetos en un ámbito diferente al de su creación), existen muchas complejidades adicionales asociadas con el patrón de eliminación. RAII evita en gran medida estos problemas . Sin embargo, en el uso común simple estas complejidades no surgen: adquirir un solo recurso, hacer algo con él, liberarlo automáticamente.
Un problema fundamental es que tener un recurso ya no es una clase invariante (el recurso se retiene desde la creación del objeto hasta que se elimina, pero el objeto todavía está activo en este punto), por lo que el recurso puede no estar disponible cuando el objeto intenta utilícelo, por ejemplo, intentando leer de un archivo cerrado. Esto significa que todos los métodos en el objeto que usan el recurso fallan potencialmente, concretamente generalmente al devolver un error o generar una excepción. En la práctica, esto es menor, ya que el uso de los recursos generalmente también puede fallar por otras razones (por ejemplo, intentar leer más allá del final de un archivo), por lo que estos métodos ya pueden fallar, y no tener un recurso solo agrega otra posible falla. . Una forma estándar de implementar esto es agregar un campo booleano al objeto, llamado disposed
, que está establecido en verdadero por dispose
y verificado por una cláusula de protección para todos los métodos (que usan el recurso), generando una excepción (como ObjectDisposedException
en .NET ) si el objeto ha sido eliminado. [5]
Además, es posible llamar dispose
a un objeto más de una vez. Si bien esto puede indicar un error de programación (cada objeto que contiene un recurso debe eliminarse exactamente una vez), es más simple, más robusto y, por lo tanto, generalmente es preferible dispose
que sea idempotente (lo que significa que "llamar varias veces es lo mismo que llamar una vez"). [5] Esto se implementa fácilmente usando el mismo disposed
campo booleano y verificándolo en una cláusula de protección al comienzo de dispose
, en ese caso regresando inmediatamente, en lugar de generar una excepción. [5] Java distingue los tipos desechables (aquellos que implementan AutoCloseable ) de los tipos desechables donde dispose es idempotente (el subtipo Closeable ).
La eliminación en presencia de herencia y composición de objetos que contienen recursos tiene problemas análogos a la destrucción / finalización (a través de destructores o finalizadores). Además, dado que el patrón de eliminación generalmente no tiene soporte de idioma para esto, es necesario un código repetitivo . En primer lugar, si una clase derivada reemplaza un dispose
método en la clase base, el método que reemplaza en la clase derivada generalmente necesita llamar al dispose
método en la clase base, para liberar adecuadamente los recursos que se encuentran en la base. En segundo lugar, si un objeto tiene una relación "tiene" con otro objeto que contiene un recurso (es decir, si un objeto usa indirectamente un recurso a través de otro objeto que usa directamente un recurso), ¿debería ser desechable el objeto que lo usa indirectamente? Esto corresponde a si la relación es de propiedad ( composición de objetos ) o visualización ( agregación de objetos ), o incluso simplemente comunicando ( asociación ), y se encuentran ambas convenciones (el usuario indirecto es responsable del recurso o no es responsable). Si el uso indirecto es responsable del recurso, debe ser desechable y eliminar los objetos de propiedad cuando se elimine (análogo a destruir o finalizar los objetos de propiedad).
La composición (poseer) proporciona encapsulación (solo el objeto que se usa necesita ser rastreado), pero a costa de una complejidad considerable cuando hay más relaciones entre objetos, mientras que la agregación (visualización) es considerablemente más simple, a costa de carecer de encapsulación. En .NET , la convención es que solo el usuario directo de los recursos sea responsable: "Debe implementar IDisposable solo si su tipo usa recursos no administrados directamente". [6] Consulte la gestión de recursos para obtener detalles y más ejemplos.
Ver también
- Vida útil del objeto
- La adquisición de recursos es inicialización (RAII)
Notas
- ^ En la programación basada en clases , los métodos se definen en una clase, utilizando unparámetroimplícito
this
oself
, en lugar de como funciones que toman un parámetro explícito.
Referencias
- ^ - Referencia de definiciones de base, la especificación única de UNIX , número 7 de The Open Group
- ^ Microsoft MSDN: using Statement (referencia de C #)
- ^ Guido van Rossum , Nick Coghlan (13 de junio de 2011). "PEP 343: El" con "Declaración" . Fundación de software Python.
- ^ Tutorial de Oracle Java: La declaración try-with-resources
- ^ a b c "Desechar patrón" .
- ^ "Interfaz IDisposable" . Consultado el 3 de abril de 2016 .
Otras lecturas
- Red de desarrolladores de Microsoft: patrón de eliminación