Pregunta: ¿Por qué Java / C # no puede implementar RAII?
Aclaración: Soy consciente de que el recolector de basura no es determinista. Entonces, con las características del lenguaje actual, no es posible llamar automáticamente al método Dispose () de un objeto al salir del ámbito. Pero, ¿podría agregarse una característica tan determinista?
Mi entendimiento:
Creo que una implementación de RAII debe cumplir dos requisitos:
1. La vida útil de un recurso debe estar vinculada a un alcance.
2. Implícito. La liberación del recurso debe ocurrir sin una declaración explícita del programador. Análogo a un recolector de basura que libera memoria sin una declaración explícita. La "implicidad" solo necesita ocurrir en el punto de uso de la clase. El creador de la biblioteca de clases, por supuesto, debe implementar explícitamente un destructor o el método Dispose ().
Java / C # satisface el punto 1. En C #, un recurso que implementa IDisposable puede vincularse a un alcance "en uso":
void test()
{
using(Resource r = new Resource())
{
r.foo();
}//resource released on scope exit
}
Esto no satisface el punto 2. El programador debe vincular explícitamente el objeto a un alcance especial "de uso". Los programadores pueden (y lo hacen) olvidar vincular explícitamente el recurso a un ámbito, creando una fuga.
De hecho, el compilador convierte los bloques "en uso" en código try-finally-dispose (). Tiene la misma naturaleza explícita del patrón try-finally-dispose (). Sin un lanzamiento implícito, el gancho a un alcance es el azúcar sintáctico.
void test()
{
//Programmer forgot (or was not aware of the need) to explicitly
//bind Resource to a scope.
Resource r = new Resource();
r.foo();
}//resource leaked!!!
Creo que vale la pena crear una función de lenguaje en Java / C # que permita objetos especiales que se enganchan a la pila mediante un puntero inteligente. La característica le permitiría marcar una clase como vinculada al alcance, de modo que siempre se cree con un gancho en la pila. Podría haber opciones para diferentes tipos de punteros inteligentes.
class Resource - ScopeBound
{
/* class details */
void Dispose()
{
//free resource
}
}
void test()
{
//class Resource was flagged as ScopeBound so the tie to the stack is implicit.
Resource r = new Resource(); //r is a smart-pointer
r.foo();
}//resource released on scope exit.
Creo que lo implícito "vale la pena". Del mismo modo que la implicidad de la recolección de basura "vale la pena". El uso explícito de bloques es refrescante para los ojos, pero no ofrece ninguna ventaja semántica sobre try-finally-dispose ().
¿No es práctico implementar tal característica en los lenguajes Java / C #? ¿Se podría introducir sin romper el código antiguo?
Dispose
s se ejecuten nunca , independientemente de cómo se activen. Agregar destrucción implícita al final del alcance no ayudará.using
la ejecución deDispose
está garantizada (bueno, descontando el proceso de morir repentinamente sin que se produzca una excepción, en ese momento toda la limpieza presumiblemente se vuelve discutible).struct
), pero por lo general se evitan excepto en casos muy especiales. Ver también .Respuestas:
Tal extensión de lenguaje sería significativamente más complicada e invasiva de lo que parece pensar. No puedes simplemente agregar
a la sección correspondiente de la especificación del idioma y listo. Ignoraré el problema de los valores temporales (
new Resource().doSomething()
) que se pueden resolver con una redacción un poco más general, este no es el problema más grave. Por ejemplo, este código se rompería (y este tipo de cosas probablemente se vuelve imposible de hacer en general):Ahora necesita constructores de copia definidos por el usuario (o mover constructores) y comenzar a invocarlos en todas partes. Esto no solo conlleva implicaciones de rendimiento, sino que también hace que estas cosas valoren efectivamente los tipos, mientras que casi todos los demás objetos son tipos de referencia. En el caso de Java, esta es una desviación radical de cómo funcionan los objetos. En C #, menos (ya tiene
struct
s, pero no tiene constructores de copia definidos por el usuario para ellos AFAIK), pero todavía hace que estos objetos RAII sean más especiales. Alternativamente, una versión limitada de tipos lineales (cf. Rust) también puede resolver el problema, a costa de prohibir el alias, incluido el paso de parámetros (a menos que desee introducir aún más complejidad adoptando referencias prestadas similares a Rust y un verificador de préstamos).Se puede hacer técnicamente, pero terminas con una categoría de cosas que son muy diferentes de todo lo demás en el idioma. Esta es casi siempre una mala idea, con consecuencias para los implementadores (más casos extremos, más tiempo / costo en cada departamento) y usuarios (más conceptos para aprender, más posibilidad de errores). No vale la pena la conveniencia adicional.
fuente
File
esta manera, nada cambia yDispose
nunca se llama. Si siempre llamaDispose
, no puede hacer nada con objetos desechables. ¿O está proponiendo algún esquema para algunas veces deshacerse y otras no? Si es así, descríbalo en detalle y te diré situaciones en las que falla.Dispose
si se escapa una referencia? El análisis de escape es un problema antiguo y difícil, esto no siempre funcionará sin más cambios en el idioma. Cuando se pasa una referencia a otro método (virtual) (something.EatFile(f);
), ¿deberíaf.Dispose
llamarse al final del alcance? En caso afirmativo, interrumpe las llamadas que almacenanf
para su uso posterior. Si no, pierde el recurso si la persona que llama no almacenaf
. La única forma algo simple de eliminar esto es un sistema de tipo lineal, que (como ya discutí más adelante en mi respuesta) presenta muchas otras complicaciones.La mayor dificultad para implementar algo como esto para Java o C # sería definir cómo funciona la transferencia de recursos. Necesitaría alguna forma de extender la vida útil del recurso más allá del alcance. Considerar:
Lo peor es que esto puede no ser obvio para el implementador de
IWrapAResource
:Algo así como la
using
declaración de C # es probablemente lo más parecido a tener semántica RAII sin recurrir a recursos de conteo de referencia o forzar semántica de valor en todas partes como C o C ++. Debido a que Java y C # tienen un uso compartido implícito de los recursos administrados por un recolector de basura, lo mínimo que un programador debería poder hacer es elegir el alcance al que está vinculado un recurso, que es exactamente lo queusing
ya hace.fuente
using
declaración.IWrapSomething
disponer deT
. Quien haya creadoT
debe preocuparse por eso, ya sea usandousing
, siendo élIDisposable
mismo o teniendo un esquema de ciclo de vida de recursos ad-hoc.La razón por la cual RAII no puede funcionar en un lenguaje como C #, pero funciona en C ++, es porque en C ++ puede decidir si un objeto es realmente temporal (asignándolo en la pila) o si es de larga duración (por asignándolo en el montón usando
new
y usando punteros).Entonces, en C ++, puedes hacer algo como esto:
En C #, no puede diferenciar entre los dos casos, por lo que el compilador no tendría idea de si finalizar el objeto o no.
Lo que podría hacer es introducir algún tipo de variable local especial, que no pueda poner en campos, etc. * y que se eliminaría automáticamente cuando salga del alcance. Que es exactamente lo que hace C ++ / CLI. En C ++ / CLI, escribe código como este:
Esto compila básicamente al mismo IL que el siguiente C #:
Para concluir, si tuviera que adivinar por qué los diseñadores de C # no agregaron RAII, es porque pensaron que no vale la pena tener dos tipos diferentes de variables locales, principalmente porque en un lenguaje con GC, la finalización determinista no es útil que a menudo.
* No sin el equivalente del
&
operador, que en C ++ / CLI es%
. Aunque hacerlo es "inseguro" en el sentido de que una vez que finaliza el método, el campo hará referencia a un objeto dispuesto.fuente
struct
tipos como D.Si lo que le molesta con los
using
bloques es su explicidad, tal vez podamos dar un pequeño paso hacia una menor explicidad, en lugar de cambiar la propia especificación de C #. Considera este código:¿Ves la
local
palabra clave que agregué? Todo lo que hace es agregar un poco más de azúcar sintáctica, comousing
decirle al compilador que llameDispose
en unfinally
bloque al final del alcance de la variable. Eso es todo. Es totalmente equivalente a:pero con un alcance implícito, en lugar de uno explícito. Es más simple que las otras sugerencias, ya que no tengo que tener la clase definida como vinculada al alcance. Solo azúcar sintáctico más limpio e implícito.
Puede haber problemas aquí con ámbitos difíciles de resolver, aunque no puedo verlo en este momento, y agradecería a cualquiera que pueda encontrarlo.
fuente
using
palabra clave, podemos mantener el comportamiento existente y usarlo también, para los casos en que no necesitamos el alcance específico. Tener unusing
valor predeterminado sin llaves para el alcance actual.Para ver un ejemplo de cómo funciona RAII en un lenguaje recolectado, revise la
with
palabra clave en Python . En lugar de confiar en objetos destruidos determinísticamente, le permite asociar__enter__()
y__exit__()
métodos a un ámbito léxico dado. Un ejemplo común es:Al igual que con el estilo RAII de C ++, el archivo se cerraría al salir de ese bloque, sin importar si es una salida 'normal'
break
, una inmediatareturn
o una excepción.Tenga en cuenta que la
open()
llamada es la función habitual de apertura de archivos. Para que esto funcione, el objeto de archivo devuelto incluye dos métodos:Este es un idioma común en Python: los objetos que están asociados con un recurso generalmente incluyen estos dos métodos.
Tenga en cuenta que el objeto del archivo aún podría permanecer asignado después de la
__exit__()
llamada, lo importante es que esté cerrado.fuente
with
en Python es casi exactamente comousing
en C #, y como tal no RAII en lo que respecta a esta pregunta.defer
en el lenguaje Go).