¿Los desarrolladores de Java abandonaron conscientemente RAII?

82

Como programador de C # desde hace mucho tiempo, recientemente he aprendido más sobre las ventajas de la adquisición de recursos es la inicialización (RAII). En particular, he descubierto que el idioma de C #:

using (var dbConn = new DbConnection(connStr)) {
    // do stuff with dbConn
}

tiene el equivalente de C ++:

{
    DbConnection dbConn(connStr);
    // do stuff with dbConn
}

lo que significa que recordar encerrar el uso de recursos como DbConnectionen un usingbloque es innecesario en C ++. Esto parece una gran ventaja de C ++. Esto es aún más convincente cuando considera una clase que tiene un miembro de instancia de tipo DbConnection, por ejemplo

class Foo {
    DbConnection dbConn;

    // ...
}

En C #, necesitaría que Foo implemente IDisposablecomo tal:

class Foo : IDisposable {
    DbConnection dbConn;

    public void Dispose()
    {       
        dbConn.Dispose();
    }
}

y lo que es peor, cada usuario de Foodebería recordar encerrarse Fooen un usingbloque, como:

   using (var foo = new Foo()) {
       // do stuff with "foo"
   }

Ahora, mirando C # y sus raíces de Java, me pregunto ... ¿los desarrolladores de Java apreciaron plenamente lo que estaban renunciando cuando abandonaron la pila en favor del montón, abandonando así RAII?

(Del mismo modo, ¿ Stroustrup apreció completamente la importancia de RAII?)

JoelFan
fuente
55
No estoy seguro de lo que está hablando con no incluir recursos en C ++. El objeto DBConnection probablemente maneja el cierre de todos los recursos en su destructor.
maple_shaft
16
@maple_shaft, ¡exactamente mi punto! Esa es la ventaja de C ++ que estoy abordando en esta pregunta. En C # necesita encerrar recursos en "usar" ... en C ++ no lo hace.
JoelFan el
12
Tengo entendido que RAII, como estrategia, solo se entendió una vez que los compiladores de C ++ fueron lo suficientemente buenos como para usar plantillas avanzadas, que es bastante posterior a Java. el C ++ que estaba realmente disponible para su uso cuando se creó Java era un estilo muy primitivo, "C con clases", con quizás plantillas básicas, si tuvo suerte.
Sean McMillan
66
"Tengo entendido que RAII, como estrategia, solo se entendió una vez que los compiladores de C ++ fueron lo suficientemente buenos como para usar plantillas avanzadas, que es mucho después de Java". - Eso no es realmente correcto. Los constructores y destructores han sido características centrales de C ++ desde el primer día, mucho antes del uso generalizado de plantillas y mucho antes de Java.
Jim en Texas el
8
@JimInTexas: Creo que Sean tiene una semilla de verdad básica en algún lugar (aunque no las plantillas, pero las excepciones es el quid). Constructores / Destructores estuvieron allí desde el principio, pero su importancia y el concepto de RAII no se dieron inicialmente (cuál es la palabra que estoy buscando). Los compiladores tardaron algunos años y algún tiempo en recuperarse antes de darnos cuenta de lo crucial que es todo el RAII.
Martin York

Respuestas:

38

Ahora, mirando C # y sus raíces de Java, me pregunto ... ¿los desarrolladores de Java apreciaron plenamente lo que estaban renunciando cuando abandonaron la pila en favor del montón, abandonando así RAII?

(Del mismo modo, ¿Stroustrup apreció completamente la importancia de RAII?)

Estoy bastante seguro de que Gosling no entendió la importancia de RAII cuando diseñó Java. En sus entrevistas, a menudo habló sobre las razones para dejar de lado los genéricos y la sobrecarga del operador, pero nunca mencionó destructores deterministas y RAII.

Curiosamente, incluso Stroustrup no era consciente de la importancia de los destructores deterministas en el momento en que los diseñó. No puedo encontrar la cita, pero si realmente le gusta, puede encontrarla entre sus entrevistas aquí: http://www.stroustrup.com/interviews.html

Nemanja Trifunovic
fuente
11
@maple_shaft: En resumen, no es posible. Excepto si inventó una forma de tener una recolección de basura determinista (que parece imposible en general e invalida todas las optimizaciones de GC de las últimas décadas en cualquier caso), tendría que introducir objetos asignados a la pila, pero eso abre varias latas de gusanos: estos objetos necesitan semántica, "problema de corte" con subtipificación (y, por lo tanto, NO polimorfismo), punteros colgantes a menos que tal vez si se le imponen restricciones significativas o se hacen cambios masivos en el sistema de tipos incompatibles. Y eso está fuera de mi cabeza.
13
@DeadMG: Entonces sugiere que volvamos a la gestión manual de la memoria. Es un enfoque válido para la programación en general y, por supuesto, permite la destrucción determinista. Pero eso no responde a esta pregunta, que se refiere a una configuración de solo GC que quiere proporcionar seguridad en la memoria y un comportamiento bien definido, incluso si todos actuamos como idiotas. Eso requiere GC para todo y ninguna forma de iniciar la destrucción de objetos manualmente (y todo el código Java existente existe al menos en el primero), por lo que puede hacer que GC sea determinista o no tenga suerte.
26
@delan. No llamaría a manualla administración de memoria de punteros inteligentes C ++ . Son más como un recolector de basura controlable determinista de grano fino. Si se usan correctamente, los punteros inteligentes son las rodillas de las abejas.
Martin York
10
@LokiAstari: Bueno, yo diría que son un poco menos automáticos que los GC completos (tienes que pensar qué tipo de inteligencia realmente quieres) e implementarlos como biblioteca requiere punteros en bruto (y, por lo tanto, administración manual de memoria) para construir . Además, no conozco ningún puntero inteligente que maneje las referencias cíclicas automáticamente, lo cual es un requisito estricto para la recolección de basura en mis libros. Los punteros inteligentes son ciertamente increíblemente geniales y útiles, pero hay que tener en cuenta que no pueden proporcionar algunas garantías (ya sea que los consideres útiles o no) de un lenguaje GC'd completo y exclusivo.
11
@delan: Tengo que estar en desacuerdo allí. Creo que son más automáticos que GC, ya que son deterministas. OKAY. Para ser eficiente, debe asegurarse de usar el correcto (se lo daré). std :: weak_ptr maneja los ciclos perfectamente bien. Cycles siempre se trota, pero en realidad casi nunca es un problema porque el objeto base generalmente se basa en la pila y cuando esto sucede, ordenará el resto. Para los casos raros donde puede ser un problema std :: weak_ptr.
Martin York
60

Sí, los diseñadores de C # (y, estoy seguro, Java) decidieron específicamente en contra de la finalización determinista. Le pregunté a Anders Hejlsberg sobre esto varias veces alrededor de 1999-2002.

Primero, la idea de una semántica diferente para un objeto basada en si está basada en pila o en montón es ciertamente contraria al objetivo de diseño unificador de ambos lenguajes, que era aliviar a los programadores de exactamente tales problemas.

En segundo lugar, incluso si reconoce que existen ventajas, existen importantes complejidades de implementación e ineficiencias involucradas en la contabilidad. Realmente no se pueden poner objetos de tipo pila en la pila en un lenguaje administrado. Te queda decir "semántica tipo pila" y comprometerte con un trabajo significativo (los tipos de valor ya son lo suficientemente difíciles, piensa en un objeto que sea una instancia de una clase compleja, con referencias entrando y volviendo a la memoria administrada).

Debido a eso, no desea una finalización determinista en cada objeto en un sistema de programación donde "(casi) todo es un objeto". Por lo tanto , debe introducir algún tipo de sintaxis controlada por el programador para separar un objeto con seguimiento normal de uno que tenga una finalización determinista.

En C #, tiene la usingpalabra clave, que llegó bastante tarde en el diseño de lo que se convirtió en C # 1.0. Todo IDisposableesto es bastante miserable, y uno se pregunta si sería más elegante usingtrabajar con la sintaxis del destructor C ++ que ~marca esas clases a las que el IDisposablepatrón de placa de caldera podría aplicarse automáticamente.

Larry OBrien
fuente
2
¿Qué pasa con lo que ha hecho C ++ / CLI (.NET), donde los objetos en el montón administrado también tienen un "identificador" basado en pila, que proporciona RIAA?
JoelFan
3
C ++ / CLI tiene un conjunto muy diferente de decisiones de diseño y restricciones. Algunas de esas decisiones significan que puede exigir más reflexión sobre la asignación de memoria y las implicaciones de rendimiento de los programadores: todo el compromiso de "darles suficiente cuerda para ahorcarse". E imagino que el compilador C ++ / CLI es considerablemente más complejo que el de C # (especialmente en sus primeras generaciones).
Larry OBrien el
55
+1 esta es la única respuesta correcta hasta ahora: es porque Java intencionalmente no tiene objetos basados ​​en pilas (no primitivos).
BlueRaja - Danny Pflughoeft
8
@ Peter Taylor - a la derecha. Pero creo que el destructor no determinista de C # vale muy poco, ya que no puede confiar en él para administrar ningún tipo de recurso restringido. Por lo tanto, en mi opinión, que podría haber sido mejor utilizar la ~sintaxis para ser el azúcar sintáctica paraIDisposable.Dispose()
Larry O'Brien
3
@ Larry: estoy de acuerdo. C ++ / CLI se utiliza ~como azúcar sintáctica para IDisposable.Dispose(), y es mucho más conveniente que la sintaxis de C #.
dan04
41

Tenga en cuenta que Java se desarrolló en 1991-1995 cuando C ++ era un lenguaje muy diferente. Las excepciones (que hicieron que RAII fuera necesario ) y las plantillas (que facilitaron la implementación de punteros inteligentes) fueron características "novedosas". La mayoría de los programadores de C ++ provenían de C y estaban acostumbrados a administrar la memoria manualmente.

Así que dudo que los desarrolladores de Java deliberadamente hayan decidido abandonar RAII. Sin embargo, fue una decisión deliberada de Java preferir la semántica de referencia en lugar de la semántica de valor. La destrucción determinista es difícil de implementar en un lenguaje semántico de referencia.

Entonces, ¿por qué usar semántica de referencia en lugar de semántica de valor?

Porque hace que el lenguaje sea mucho más simple.

  • No hay necesidad de una distinción sintáctica entre Fooy Foo*o entre foo.bary foo->bar.
  • No hay necesidad de una tarea sobrecargada, cuando todo lo que hace es copiar un puntero.
  • No hay necesidad de copiar constructores. ( Ocasionalmente, es necesaria una función de copia explícita como clone(). Muchos objetos simplemente no necesitan copiarse. Por ejemplo, los inmutables no).
  • No es necesario declarar privateconstructores de copias y operator=hacer que una clase no se pueda copiar. Si no desea copiar objetos de una clase, simplemente no escriba una función para copiarlo.
  • No hay necesidad de swapfunciones. (A menos que esté escribiendo una rutina de clasificación).
  • No hay necesidad de referencias rvalue de estilo C ++ 0x.
  • No hay necesidad de (N) RVO.
  • No hay problema de corte.
  • Es más fácil para el compilador determinar el diseño de los objetos, porque las referencias tienen un tamaño fijo.

El principal inconveniente de la semántica de referencia es que cuando cada objeto potencialmente tiene múltiples referencias, es difícil saber cuándo eliminarlo. Prácticamente tienes que tener una administración automática de memoria.

Java eligió usar un recolector de basura no determinista.

¿GC no puede ser determinista?

Sí puede. Por ejemplo, la implementación C de Python utiliza el recuento de referencias. Y luego agregó GC de rastreo para manejar la basura cíclica donde fallan los reembolsos.

Pero el recuento es terriblemente ineficiente. Se gastaron muchos ciclos de CPU actualizando los recuentos. Peor aún en un entorno de subprocesos múltiples (como el tipo para el que fue diseñado Java) donde esas actualizaciones deben sincronizarse. Es mucho mejor usar el recolector de basura nulo hasta que necesite cambiar a otro.

Se podría decir que Java eligió optimizar el caso común (memoria) a expensas de recursos no fungibles como archivos y sockets. Hoy, a la luz de la adopción de RAII en C ++, esto puede parecer una elección incorrecta. Pero recuerde que gran parte del público objetivo para Java eran programadores de C (o "C con clases") que estaban acostumbrados a cerrar explícitamente estas cosas.

Pero, ¿qué pasa con los "objetos de pila" de C ++ / CLI?

Solo son azúcar sintáctica paraDispose ( enlace original ), al igual que C # using. Sin embargo, no resuelve el problema general de la destrucción determinista, porque puede crear un anónimo gcnew FileStream("filename.ext")y C ++ / CLI no lo desechará automáticamente.

dan04
fuente
3
Además, buenos enlaces (especialmente el primero, que es muy relevante para esta discusión) .
BlueRaja - Danny Pflughoeft
La usingdeclaración maneja muy bien muchos problemas relacionados con la limpieza, pero quedan muchos otros. Sugeriría que el enfoque correcto para un lenguaje y un marco sería distinguir declarativamente entre ubicaciones de almacenamiento que "poseen" un referenciado IDisposablede aquellas que no lo hacen; sobrescribir o abandonar un lugar de almacenamiento que posee un referenciado IDisposabledebe eliminar el objetivo en ausencia de una directiva en contrario.
supercat
1
"No hay necesidad de constructores de copias" suena bien, pero falla mucho en la práctica. java.util.Date y Calendar son quizás los ejemplos más notorios. Nada más hermoso que new Date(oldDate.getTime()).
Kevin Cline
2
iow RAII no fue "abandonado", simplemente no existía para ser abandonado :) En cuanto a los constructores, nunca me han gustado, es muy fácil equivocarse, son una fuente constante de dolores de cabeza cuando alguien en el fondo (de lo contrario) se olvidó de hacer una copia profunda, lo que hace que los recursos se compartan entre copias que no deberían ser
Jwenting
20

Java7 introdujo algo similar a C # using: la declaración de prueba con recursos

Una trydeclaración que declara uno o más recursos. Un recurso es como un objeto que debe cerrarse después de que el programa haya terminado con él. La trydeclaración -with-resources asegura que cada recurso se cierre al final de la declaración. Cualquier objeto que implemente java.lang.AutoCloseable, que incluye todos los objetos que implementan java.io.Closeable, puede usarse como un recurso ...

Así que supongo que no eligieron conscientemente no implementar RAII o cambiaron de opinión mientras tanto.

Patricio
fuente
Interesante, pero parece que esto solo funciona con objetos que se implementan java.lang.AutoCloseable. Probablemente no sea un gran problema, pero no me gusta cómo esto se siente algo limitado. Tal vez tengo algún otro objeto que debe ser relased de forma automática, pero es muy raro semánticamente para que sea poner en práctica AutoCloseable...
FrustratedWithFormsDesigner
99
@Patrick: Er, entonces? usingno es lo mismo que RAII: en un caso, la persona que llama se preocupa por deshacerse de los recursos, en el otro caso, la persona que llama lo maneja.
BlueRaja - Danny Pflughoeft
1
+1 No sabía acerca de probar con recursos; debería ser útil para descargar más repetitivo.
Jprete
3
-1 para using/ try-with-resources no es lo mismo que RAII.
Sean McMillan
44
@Sean: De acuerdo. usingy es probable que no estén cerca de RAII.
DeadMG
18

Java no tiene intencionalmente objetos basados ​​en pila (también conocidos como objetos de valor). Estos son necesarios para que el objeto se destruya automáticamente al final del método como ese.

Debido a esto y al hecho de que Java se recolecta basura, la finalización determinista es más o menos imposible (por ejemplo, ¿qué pasa si mi objeto "local" se hace referencia en otro lugar? Entonces, cuando el método termina, no queremos que se destruya ) .

Sin embargo, esto está bien para la mayoría de nosotros, porque casi nunca hay necesidad de una finalización determinista, ¡excepto cuando se interactúa con recursos nativos (C ++)!


¿Por qué Java no tiene objetos basados ​​en pila?

(Aparte de las primitivas ..)

Porque los objetos basados ​​en pila tienen una semántica diferente a las referencias basadas en el montón. Imagine el siguiente código en C ++; ¿Qué hace?

return myObject;
  • Si myObjectes un objeto basado en una pila local, se llama al constructor de copias (si el resultado se asigna a algo).
  • Si myObjectes un objeto basado en la pila local y estamos devolviendo una referencia, el resultado no está definido.
  • Si myObjectes un miembro / objeto global, se llama al constructor de copia (si el resultado se asigna a algo).
  • Si myObjectes un miembro / objeto global y estamos devolviendo una referencia, se devuelve la referencia.
  • Si myObjectes un puntero a un objeto basado en pila local, el resultado es indefinido.
  • Si myObjectes un puntero a un miembro / objeto global, se devuelve ese puntero.
  • Si myObjectes un puntero a un objeto basado en el montón, se devuelve ese puntero.

Ahora, ¿qué hace el mismo código en Java?

return myObject;
  • Se myObjectdevuelve la referencia a . No importa si la variable es local, miembro o global; y no hay objetos basados ​​en pila o casos de puntero de los que preocuparse.

Lo anterior muestra por qué los objetos basados ​​en pila son una causa muy común de errores de programación en C ++. Por eso, los diseñadores de Java los eliminaron; y sin ellos, no tiene sentido usar RAII en Java.

BlueRaja - Danny Pflughoeft
fuente
66
No sé a qué te refieres con "no tiene sentido RAII" ... Creo que quieres decir "no hay capacidad para proporcionar RAII en Java" ... RAII es independiente de cualquier idioma ... no se vuelve "inútil" porque 1 idioma en particular no lo proporciona
JoelFan
44
Esa no es una razón válida. Un objeto no tiene que vivir realmente en la pila para usar RAII basado en pila. Si existe una "referencia única", el destructor se puede disparar una vez que sale del alcance. Vea, por ejemplo, cómo funciona con el lenguaje de programación D: d-programming-language.org/exception-safe.html
Nemanja Trifunovic
3
@Nemanja: Un objeto no tiene que vivir en la pila para tener una semántica basada en la pila, y nunca dije que lo hiciera. Pero ese no es el problema; El problema, como mencioné, es la semántica basada en la pila.
BlueRaja - Danny Pflughoeft
44
@Aaronaught: El demonio está en "casi siempre" y "la mayor parte del tiempo". Si no cierra su conexión db y deja que el GC active el finalizador, funcionará bien con sus pruebas unitarias y se romperá severamente cuando se implemente en producción. La limpieza determinista es importante independientemente del idioma.
Nemanja Trifunovic el
8
@NemanjaTrifunovic: ¿Por qué realiza pruebas unitarias en una conexión de base de datos en vivo? Eso no es realmente una prueba unitaria. No, lo siento, no lo estoy comprando. De todos modos, no debería crear conexiones DB por todas partes, debería pasarlas a través de constructores o propiedades, y eso significa que no desea una semántica de autodestrucción tipo pila. Muy pocos objetos que dependen de una conexión de base de datos deberían poseerla. Si la limpieza no determinista te muerde con tanta frecuencia, así de difícil, entonces es debido al mal diseño de la aplicación, no al mal diseño del lenguaje.
Aaronaught
17

Su descripción de los agujeros de usingestá incompleta. Considere el siguiente problema:

interface Bar {
    ...
}
class Foo : Bar, IDisposable {
    ...
}

Bar b = new Foo();

// Where's the Dispose?

En mi opinión, no tener RAII y GC fue una mala idea. Cuando se trata de cerrar archivos en Java, es malloc()y free()por allí.

DeadMG
fuente
2
Estoy de acuerdo en que RAII son las rodillas de las abejas. Pero la usingcláusula es un gran paso adelante para C # sobre Java. Permite la destrucción determinista y, por lo tanto, corrige la gestión de recursos (no es tan bueno como RAII como debe recordar hacerlo, pero definitivamente es una buena idea).
Martin York
8
"Cuando se trata de cerrar archivos en Java, es malloc () y free () allí". - Absolutamente.
Konrad Rudolph el
99
@KonradRudolph: es peor que malloc y es gratis. Al menos en C no tienes excepciones.
Nemanja Trifunovic
1
@Nemanja: Seamos justos, puedes free()en el finally.
DeadMG
44
@Loki: El problema de la clase base es mucho más importante como problema. Por ejemplo, el original IEnumerableno heredó de IDisposable, y hubo un montón de iteradores especiales que nunca se pudieron implementar como resultado.
DeadMG
14

Soy bastante viejo Estuve allí y lo vi y me golpeé la cabeza muchas veces.

Estaba en una conferencia en Hursley Park donde los chicos de IBM nos contaban lo maravilloso que era este nuevo lenguaje Java, solo alguien preguntó ... ¿por qué no hay un destructor para estos objetos? No quiso decir lo que conocemos como destructor en C ++, pero tampoco había finalizador (o tenía finalizadores pero básicamente no funcionaban). Esto es hace mucho tiempo, y decidimos que Java era un lenguaje de juguete en ese momento.

ahora agregaron Finalisers a las especificaciones del lenguaje y Java vio algo de adopción.

Por supuesto, más tarde a todos se les dijo que no pusieran finalizadores en sus objetos porque ralentizaba enormemente el GC. (ya que no solo tenía que bloquear el montón, sino también mover los objetos a finalizar a un área temporal, ya que estos métodos no podían invocarse ya que el GC ha detenido la ejecución de la aplicación. En su lugar, se llamarían inmediatamente antes del siguiente Ciclo GC) (y lo que es peor, a veces nunca se llama al finalizador cuando la aplicación se estaba cerrando. Imagina que nunca se cierra el identificador de archivo)

Luego tuvimos C #, y recuerdo el foro de discusión en MSDN donde nos dijeron lo maravilloso que era este nuevo lenguaje C #. Alguien preguntó por qué no había una finalización determinista y los chicos de MS nos dijeron que no necesitábamos tales cosas, luego nos dijeron que teníamos que cambiar nuestra forma de diseñar aplicaciones, luego nos dijeron cuán increíble era GC y cómo eran todas nuestras aplicaciones antiguas basura y nunca funcionó debido a todas las referencias circulares. Luego cedieron a la presión y nos dijeron que habían agregado este patrón IDispose a la especificación que podríamos usar. Pensé que era más o menos la vuelta a la gestión de memoria manual para nosotros en las aplicaciones de C # en ese momento.

Por supuesto, los chicos de MS más tarde descubrieron que todo lo que nos habían dicho era ... bueno, hicieron que IDispose fuera un poco más que una interfaz estándar, y luego agregaron la declaración de uso. W00t! Se dieron cuenta de que la finalización determinista era algo que faltaba en el lenguaje después de todo. Por supuesto, aún debe recordar colocarlo en todas partes, por lo que todavía es un poco manual, pero es mejor.

Entonces, ¿por qué lo hicieron cuando podrían haber tenido una semántica con estilo colocada automáticamente en cada bloque de alcance desde el principio? Probablemente eficiencia, pero me gusta pensar que simplemente no se dieron cuenta. Al igual que finalmente se dieron cuenta de que todavía necesita punteros inteligentes en .NET (google SafeHandle), pensaron que el GC realmente resolvería todos los problemas. Se olvidaron de que un objeto es más que solo memoria y que GC está diseñado principalmente para manejar la administración de memoria. quedaron atrapados en la idea de que el GC manejaría esto, y olvidaron que pusiste otras cosas allí, un objeto no es solo una gota de memoria que no importa si no lo eliminas por un tiempo.

Pero también creo que la falta de un método de finalización en el Java original tenía un poco más que eso: que los objetos que creaste tenían que ver con la memoria y si querías eliminar algo más (como un controlador de base de datos o un socket o lo que sea ) entonces se esperaba que lo hicieras manualmente .

Recuerde que Java fue diseñado para entornos embebidos donde las personas estaban acostumbradas a escribir código C con muchas asignaciones manuales, por lo que no tener un automático automático no era un gran problema; nunca lo hicieron antes, entonces, ¿por qué lo necesitarían en Java? El problema no tenía nada que ver con subprocesos, o pila / montón, probablemente solo estaba allí para facilitar un poco la asignación de memoria (y, por lo tanto, la desasignación). En general, la declaración try / finally es probablemente un mejor lugar para manejar recursos que no son de memoria.

En mi humilde opinión, la forma en que .NET simplemente copió el mayor defecto de Java es su mayor debilidad. .NET debería haber sido un mejor C ++, no un mejor Java.

gbjbaanb
fuente
En mi humilde opinión, cosas como 'usar' bloques son el enfoque correcto para la limpieza determinista, pero también se necesitan algunas cosas más: (1) un medio para asegurar que los objetos se eliminen si sus destructores arrojan una excepción; (2) un medio para generar automáticamente un método de rutina para llamar Disposea todos los campos marcados con una usingdirectiva, y especificar si se IDisposable.Disposedebe llamar automáticamente; (3) una directiva similar a using, pero que solo llamaría Disposeen caso de una excepción; (4) una variación de la IDisposablecual tomaría un Exceptionparámetro, y ...
supercat
... que se usaría automáticamente usingsi fuera apropiado; el parámetro sería nullsi el usingbloque saliera normalmente, o indicaría qué excepción estaba pendiente si saliera por excepción. Si tales cosas existieran, sería mucho más fácil administrar los recursos de manera efectiva y evitar fugas.
supercat
11

Bruce Eckel, autor de "Pensar en Java" y "Pensar en C ++" y miembro del Comité de Estándares de C ++, es de la opinión de que, en muchas áreas (no solo RAII), Gosling y el equipo de Java no hicieron su trabajo deberes.

... Para comprender cómo el lenguaje puede ser tanto desagradable como complicado, y bien diseñado al mismo tiempo, debe tener en cuenta la decisión de diseño principal sobre la que todo dependía de C ++: la compatibilidad con C. Stroustrup decidió, y correctamente , parecería, que la forma de hacer que las masas de programadores de C se muevan a objetos es hacer que el movimiento sea transparente: permitirles compilar su código C sin cambios en C ++. Esta fue una gran limitación, y siempre ha sido la mayor fortaleza de C ++ ... y su ruina. Es lo que hizo que C ++ fuera tan exitoso como era y tan complejo como es.

También engañó a los diseñadores de Java que no entendían C ++ lo suficientemente bien. Por ejemplo, pensaron que la sobrecarga del operador era demasiado difícil de usar para los programadores. Lo cual es básicamente cierto en C ++, porque C ++ tiene asignación de pila y asignación de montón y debe sobrecargar a sus operadores para manejar todas las situaciones y no causar pérdidas de memoria. Difícil de hecho. Sin embargo, Java tiene un mecanismo de asignación de almacenamiento único y un recolector de basura, lo que hace que la sobrecarga del operador sea trivial, como se mostró en C # (pero ya se había mostrado en Python, que era anterior a Java). Pero durante muchos años, la línea parcial del equipo de Java fue "La sobrecarga del operador es demasiado complicada". Esta y muchas otras decisiones donde alguien claramente no

Hay muchos otros ejemplos. Las primitivas "tenían que ser incluidas para la eficiencia". La respuesta correcta es mantenerse fiel a "todo es un objeto" y proporcionar una puerta trampa para realizar actividades de nivel inferior cuando se requiera eficiencia (esto también habría permitido que las tecnologías de punto de acceso hicieran las cosas de manera transparente, como eventualmente lo harían). tener). Ah, y el hecho de que no puede usar el procesador de coma flotante directamente para calcular las funciones trascendentales (en su lugar, se hace en software). He escrito sobre temas como este tanto como puedo soportar, y la respuesta que escucho siempre ha sido una respuesta tautológica al efecto de que "esta es la forma de Java".

Cuando escribí sobre cuán mal diseñados estaban los genéricos, obtuve la misma respuesta, junto con "debemos ser compatibles con las decisiones anteriores (malas) anteriores tomadas en Java". Últimamente, más y más personas han adquirido suficiente experiencia con los genéricos para ver que realmente son muy difíciles de usar; de hecho, las plantillas de C ++ son mucho más potentes y consistentes (y mucho más fáciles de usar ahora que los mensajes de error del compilador son tolerables). La gente incluso se ha tomado en serio la reificación, algo que sería útil, pero que no afectará tanto a un diseño que esté paralizado por las restricciones autoimpuestas.

La lista continúa hasta el punto en que es tedioso ...

Gnawme
fuente
55
Esto suena como una respuesta Java versus C ++, en lugar de centrarse en RAII. Creo que C ++ y Java son lenguajes diferentes, cada uno con sus fortalezas y debilidades. Además, los diseñadores de C ++ no hicieron su tarea en muchas áreas (no se aplica el principio KISS, mecanismo de importación simple para las clases que faltan, etc.). Pero el foco de la pregunta era RAII: esto falta en Java y hay que programarlo manualmente.
Giorgio
44
@Giorgio: El objetivo del artículo es que Java parece haberse perdido el rumbo en una serie de cuestiones, algunas de las cuales se relacionan directamente con RAII. Con respecto a C ++ y su impacto en Java, Eckels señala: "Debe tener en cuenta la decisión de diseño principal sobre la que todo colgaba en C ++: compatibilidad con C. Esta fue una gran restricción, y siempre ha sido la mayor fortaleza de C ++ ... y su perdición. También engañó a los diseñadores de Java que no entendían C ++ lo suficientemente bien ". El diseño de C ++ influyó directamente en Java, mientras que C # tuvo la oportunidad de aprender de ambos. (Si lo hizo es otra pregunta)
Gnawme
2
@Giorgio Estudiar los idiomas existentes en un paradigma particular y una familia de idiomas es, de hecho, una parte de la tarea requerida para el desarrollo del nuevo lenguaje. Este es un ejemplo en el que simplemente lo mezclaron con Java. Tenían C ++ y Smalltalk para mirar. C ++ no tenía Java para mirar cuando se desarrolló.
Jeremy
1
@Gnawme: "Java parece haberse perdido el barco en una serie de problemas, algunos de los cuales se relacionan directamente con RAII": ¿puede mencionar estos problemas? El artículo que publicó no menciona RAII.
Giorgio
2
@Giorgio Claro, ha habido innovaciones desde el desarrollo de C ++ que explican muchas de las características que le faltan. ¿Alguna de esas características que deberían haber encontrado en los lenguajes se estableció antes del desarrollo de C ++? Ese es el tipo de tarea de la que estamos hablando con Java: no hay ninguna razón para que no tengan en cuenta todas las características de C ++ en el desarrollo de Java. A algunos les gusta la herencia múltiple que omitieron intencionalmente, otros como RAII que parecen haber pasado por alto.
Jeremy
10

La mejor razón es mucho más simple que la mayoría de las respuestas aquí.

No puede pasar objetos asignados a la pila a otros hilos.

Detente y piensa en eso. Sigue pensando ... Ahora C ++ no tenía hilos cuando todos estaban tan interesados ​​en RAII. Incluso Erlang (montones separados por hilo) se vuelve repulsivo cuando pasas demasiados objetos. C ++ solo obtuvo un modelo de memoria en C ++ 2011; ahora casi puede razonar sobre la concurrencia en C ++ sin tener que consultar la "documentación" de su compilador.

Java fue diseñado desde (casi) el primer día para múltiples hilos.

Todavía tengo mi copia anterior de "El lenguaje de programación C ++", donde Stroustrup me asegura que no necesitaré hilos.

La segunda razón dolorosa es evitar rebanar.

Tim Williscroft
fuente
1
El diseño de Java para múltiples hilos también explica por qué el GC no se basa en el recuento de referencias.
dan04
44
@NemanjaTrifunovic: No puede comparar C ++ / CLI con Java o C #, fue diseñado casi con el propósito expreso de interoperar con código C / C ++ no administrado; es más como un lenguaje no administrado que da acceso al marco .NET que viceversa.
Aaronaught
3
@NemanjaTrifunovic: Sí, C ++ / CLI es un ejemplo de cómo se puede hacer de una manera totalmente inapropiada para las aplicaciones normales . Es solamente útil para la interoperabilidad de C / C ++. No solo los desarrolladores normales no necesitan cargar con una decisión totalmente irrelevante de "apilar o apilar", sino que si alguna vez intentas refactorizarla, es trivialmente fácil crear accidentalmente un puntero nulo / error de referencia y / o una pérdida de memoria. Lo siento, pero tengo que preguntarme si alguna vez has programado realmente en Java o C #, porque no creo que alguien que lo haya hecho quiera usar la semántica en C ++ / CLI.
Aaronaught
2
@Aaronaught: He programado con Java (un poco) y C # (mucho) y mi proyecto actual es prácticamente todo C #. Créame, sé de lo que estoy hablando, y no tiene nada que ver con "apilar contra montón": tiene todo que ver con asegurarse de que todos sus recursos se liberen tan pronto como no los necesite. Automáticamente. Si no es así - que se meten en problemas.
Nemanja Trifunovic el
44
@NemanjaTrifunovic: Eso es genial, realmente genial, pero tanto C # como C ++ / CLI requieren que declares explícitamente cuándo quieres que esto suceda, solo usan una sintaxis diferente. Nadie discute el punto esencial sobre el que está divagando actualmente (que "los recursos se liberan tan pronto como no los necesita"), pero está dando un salto lógico gigantesco a "todos los lenguajes administrados deberían tener automático pero solo "disposición clasista basada en la pila de llamadas". Simplemente no retiene el agua.
Aaronaught
5

En C ++, utiliza funciones de lenguaje de nivel inferior más generales (destructores llamados automáticamente en objetos basados ​​en pila) para implementar uno de nivel superior (RAII), y este enfoque es algo que la gente de C # / Java parece no ser demasiado aficionado Prefieren diseñar herramientas específicas de alto nivel para necesidades específicas, y proporcionarlas a los programadores listos para usar, integrados en el lenguaje. El problema con estas herramientas específicas es que a menudo son imposibles de personalizar (en parte, eso es lo que las hace tan fáciles de aprender). Cuando se construye a partir de bloques más pequeños, con el tiempo puede surgir una mejor solución, mientras que si solo tiene construcciones integradas de alto nivel, es menos probable.

Así que sí, creo que (en realidad no estaba allí ...) fue una decisión consciente, con el objetivo de hacer que los idiomas fueran más fáciles de aprender, pero en mi opinión, fue una mala decisión. Por otra parte, generalmente prefiero la filosofía de C ++ de dar a los programadores una oportunidad para rodar su propia filosofía, así que estoy un poco sesgado.

imre
fuente
77
La "dar a los programadores una oportunidad para rodar su propia filosofía" funciona bien HASTA que necesite combinar bibliotecas escritas por programadores que cada una lanzó sus propias clases de cadenas y punteros inteligentes.
dan04
@ dan04 por lo que los lenguajes administrados que le dan clases de cadenas predefinidas, luego le permiten parchearlos, lo cual es una receta para el desastre si es el tipo de persona que no puede hacer frente a una cadena diferente enrollada clase.
gbjbaanb
-1

Ya llamó al equivalente aproximado de esto en C # con el Disposemétodo. Java también tiene finalize. NOTA: Me doy cuenta de que la finalización de Java no es determinista y diferente Dispose, solo estoy señalando que ambos tienen un método de limpieza de recursos junto con el GC.

Si algo, C ++ se vuelve más doloroso porque un objeto tiene que ser físicamente destruido. En lenguajes de nivel superior como C # y Java, dependemos de un recolector de basura para limpiarlo cuando ya no hay referencias a él. No existe tal garantía de que el objeto DBConnection en C ++ no tenga referencias o punteros falsos.

Sí, el código C ++ puede ser más intuitivo de leer, pero puede ser una pesadilla para depurar porque los límites y las limitaciones que establecen los lenguajes como Java descartan algunos de los errores más agravantes y difíciles, así como protegen a otros desarrolladores de errores comunes de novatos.

Tal vez se trata de preferencias, algunos como el poder de bajo nivel, el control y la pureza de C ++, donde otros como yo prefieren un lenguaje más limitado que es mucho más explícito.

árbol de arce
fuente
12
En primer lugar de Java "finalizar" es no determinista ... se no el equivalente de C # 's 'disponer' o de C ++' destructores s ... También, C ++ también tiene un recolector de basura si se utiliza .NET
JoelFan
2
@DeadMG: El problema es que puede que no seas un idiota, pero ese otro tipo que acaba de dejar la empresa (y que escribió el código que ahora mantienes) podría haberlo sido.
Kevin
77
Ese tipo va a escribir código de mierda, hagas lo que hagas. No puedes tomar un mal programador y hacer que escriba un buen código. Los punteros colgantes son la menor de mis preocupaciones cuando trato con idiotas. Los buenos estándares de codificación utilizan punteros inteligentes para la memoria que debe rastrearse, por lo que la administración inteligente debería hacer obvio cómo desasignar y acceder a la memoria de manera segura.
DeadMG
3
Lo que dijo DeadMG. Hay muchas cosas malas sobre C ++. Pero RAII no es uno de ellos por mucho tiempo. De hecho, la falta de Java y .NET para explicar adecuadamente la gestión de recursos (porque la memoria es el único recurso, ¿verdad?) Es uno de sus mayores problemas.
Konrad Rudolph el
8
El finalizador en mi opinión es un desastre de diseño inteligente. Como está forzando el uso correcto de un objeto del diseñador al usuario del objeto (no en términos de administración de memoria sino de administración de recursos). En C ++, es responsabilidad del diseñador de la clase obtener la gestión correcta de los recursos (solo una vez). En Java, es responsabilidad del usuario de la clase obtener la gestión correcta de los recursos y, por lo tanto, debe hacerse cada vez que la clase que usamos. stackoverflow.com/questions/161177/…
Martin York