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 DbConnection
en un using
bloque 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 IDisposable
como tal:
class Foo : IDisposable {
DbConnection dbConn;
public void Dispose()
{
dbConn.Dispose();
}
}
y lo que es peor, cada usuario de Foo
debería recordar encerrarse Foo
en un using
bloque, 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?)
fuente
Respuestas:
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
fuente
manual
la 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.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
using
palabra clave, que llegó bastante tarde en el diseño de lo que se convirtió en C # 1.0. TodoIDisposable
esto es bastante miserable, y uno se pregunta si sería más eleganteusing
trabajar con la sintaxis del destructor C ++ que~
marca esas clases a las que elIDisposable
patrón de placa de caldera podría aplicarse automáticamente.fuente
~
sintaxis para ser el azúcar sintáctica paraIDisposable.Dispose()
~
como azúcar sintáctica paraIDisposable.Dispose()
, y es mucho más conveniente que la sintaxis de C #.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.
Foo
yFoo*
o entrefoo.bar
yfoo->bar
.clone()
. Muchos objetos simplemente no necesitan copiarse. Por ejemplo, los inmutables no).private
constructores de copias yoperator=
hacer que una clase no se pueda copiar. Si no desea copiar objetos de una clase, simplemente no escriba una función para copiarlo.swap
funciones. (A menos que esté escribiendo una rutina de clasificación).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 para
Dispose
( enlace original ), al igual que C #using
. Sin embargo, no resuelve el problema general de la destrucción determinista, porque puede crear un anónimogcnew FileStream("filename.ext")
y C ++ / CLI no lo desechará automáticamente.fuente
using
declaració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 referenciadoIDisposable
de aquellas que no lo hacen; sobrescribir o abandonar un lugar de almacenamiento que posee un referenciadoIDisposable
debe eliminar el objetivo en ausencia de una directiva en contrario.new Date(oldDate.getTime())
.Java7 introdujo algo similar a C #
using
: la declaración de prueba con recursosAsí que supongo que no eligieron conscientemente no implementar RAII o cambiaron de opinión mientras tanto.
fuente
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ácticaAutoCloseable
...using
no 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.using
/ try-with-resources no es lo mismo que RAII.using
y es probable que no estén cerca de RAII.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?
myObject
es un objeto basado en una pila local, se llama al constructor de copias (si el resultado se asigna a algo).myObject
es un objeto basado en la pila local y estamos devolviendo una referencia, el resultado no está definido.myObject
es un miembro / objeto global, se llama al constructor de copia (si el resultado se asigna a algo).myObject
es un miembro / objeto global y estamos devolviendo una referencia, se devuelve la referencia.myObject
es un puntero a un objeto basado en pila local, el resultado es indefinido.myObject
es un puntero a un miembro / objeto global, se devuelve ese puntero.myObject
es un puntero a un objeto basado en el montón, se devuelve ese puntero.Ahora, ¿qué hace el mismo código en Java?
myObject
devuelve 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.
fuente
Su descripción de los agujeros de
using
está incompleta. Considere el siguiente problema:En mi opinión, no tener RAII y GC fue una mala idea. Cuando se trata de cerrar archivos en Java, es
malloc()
yfree()
por allí.fuente
using
clá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).free()
en elfinally
.IEnumerable
no heredó deIDisposable
, y hubo un montón de iteradores especiales que nunca se pudieron implementar como resultado.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.
fuente
Dispose
a todos los campos marcados con unausing
directiva, y especificar si seIDisposable.Dispose
debe llamar automáticamente; (3) una directiva similar ausing
, pero que solo llamaríaDispose
en caso de una excepción; (4) una variación de laIDisposable
cual tomaría unException
parámetro, y ...using
si fuera apropiado; el parámetro seríanull
si elusing
bloque 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.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.
fuente
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.
fuente
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.
fuente
Ya llamó al equivalente aproximado de esto en C # con el
Dispose
método. Java también tienefinalize
. NOTA: Me doy cuenta de que la finalización de Java no es determinista y diferenteDispose
, 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.
fuente