Parece que la seguridad de subprocesos siempre se menciona a menudo como el principal beneficio de usar tipos inmutables y especialmente colecciones.
Tengo una situación en la que me gustaría asegurarme de que un método no modifique un diccionario de cadenas (que son inmutables en C #). Me gustaría restringir las cosas tanto como sea posible.
Sin embargo, no estoy seguro de si vale la pena agregar una dependencia a un nuevo paquete (Microsoft Immutable Collections). El rendimiento tampoco es un gran problema.
Entonces, supongo que mi pregunta es si se recomienda encarecidamente realizar colecciones inmutables cuando no hay requisitos de rendimiento estrictos y no hay problemas de seguridad de los hilos. Tenga en cuenta que la semántica de valores (como en mi ejemplo) podría o no ser un requisito también.
fuente
ConcurrentModificationException
que generalmente es causado por el mismo hilo que muta la colección en el mismo hilo, en el cuerpo de unforeach
bucle sobre la misma colección.hashCode()
oequals(Object)
se modifica el resultado, puede causar errores al usarloCollections
(por ejemplo, en unHashSet
objeto se almacenó en un "depósito" y después de mutarlo debería ir a otro).Respuestas:
La inmutabilidad simplifica la cantidad de información que necesita rastrear mentalmente al leer el código más adelante . Para las variables mutables, y especialmente los miembros de la clase mutable, es muy difícil saber en qué estado estarán en la línea específica sobre la que está leyendo, sin ejecutar el código con un depurador. Es fácil razonar sobre los datos inmutables : siempre serán los mismos. Si desea cambiarlo, debe crear un nuevo valor.
Sinceramente, preferiría hacer que las cosas sean inmutables por defecto , y luego cambiarlas a mutables donde se demuestre que deben ser, ya sea que esto requiera el rendimiento o un algoritmo que tenga no tiene sentido para la inmutabilidad.
fuente
val
, y solo en muy, muy raras ocasiones, encuentro que necesito cambiar algo a avar
. Muchas de las 'variables' que defino en cualquier idioma están ahí para mantener un valor que almacena el resultado de algunos cálculos, y no necesita ser actualizado.Su código debe expresar su intención. Si no desea que se modifique un objeto una vez creado, haga que sea imposible modificarlo.
La inmutabilidad tiene varios beneficios:
La intención del autor original se expresa mejor.
¿Cómo podría saber que en el siguiente código, modificar el nombre provocaría que la aplicación genere una excepción en algún momento posterior?
Es más fácil asegurarse de que el objeto no aparezca en un estado no válido.
Tienes que controlar esto en un constructor, y solo allí. Por otro lado, si tiene un conjunto de setters y métodos que modifican el objeto, dichos controles pueden volverse particularmente difíciles, especialmente cuando, por ejemplo, dos campos deberían cambiar al mismo tiempo para que el objeto sea válido.
Por ejemplo, un objeto es válido si la dirección no lo es
null
o las coordenadas GPS no lo sonnull
, pero no es válido si se especifican tanto la dirección como las coordenadas GPS. ¿Te imaginas el infierno para validar esto si tanto la dirección como las coordenadas GPS tienen un setter o ambas son mutables?Concurrencia.
Por cierto, en su caso, no necesita ningún paquete de terceros. .NET Framework ya incluye una
ReadOnlyDictionary<TKey, TValue>
clase.fuente
Hay muchas razones de un solo subproceso para usar la inmutabilidad. Por ejemplo
El objeto A contiene el objeto B.
El código externo consulta su objeto B y usted lo devuelve.
Ahora tienes tres situaciones posibles:
En el tercer caso, el código de usuario puede no darse cuenta de lo que ha hecho y puede hacer cambios en el objeto, y al hacerlo, cambiar los datos internos de su objeto sin tener control o visibilidad de que eso suceda.
fuente
La inmutabilidad también puede simplificar enormemente la implementación de recolectores de basura. Del wiki de GHC :
fuente
Ampliando lo que KChaloux resumió muy bien ...
Idealmente, tiene dos tipos de campos y, por lo tanto, dos tipos de código que los utilizan. Cualquiera de los campos son inmutables y el código no tiene que tener en cuenta la mutabilidad; o los campos son mutables, y necesitamos escribir código que tome una instantánea (
int x = p.x
) o maneje con gracia dichos cambios.En mi experiencia, la mayoría del código se encuentra entre los dos, siendo un código optimista : hace referencia libremente a datos mutables, suponiendo que la primera llamada
p.x
tendrá el mismo resultado que la segunda llamada. Y la mayoría de las veces, esto es cierto, excepto cuando resulta que ya no lo es. UpsEntonces, realmente, cambie esa pregunta: ¿Cuáles son mis razones para hacer esto mutable ?
¿Escribes código defensivo? La inmutabilidad le ahorrará algunas copias. ¿Escribes código optimista? La inmutabilidad le ahorrará la locura de ese error extraño e imposible.
fuente
Otro beneficio de la inmutabilidad es que es el primer paso para redondear estos objetos inmutables en un grupo. Luego puede administrarlos para no crear múltiples objetos que representen de manera conceptual y semántica la misma cosa. Un buen ejemplo sería la cadena de Java.
Es un fenómeno bien conocido en lingüística que algunas palabras aparecen mucho, también pueden aparecer en otro contexto. Entonces, en lugar de crear varios
String
objetos, puede usar uno inmutable. Pero luego debe mantener un administrador de grupo para cuidar estos objetos inmutables.Esto te ahorrará mucha memoria. Este es un artículo interesante para leer también: http://en.wikipedia.org/wiki/Zipf%27s_law
fuente
En Java, C # y otros lenguajes similares, los campos de tipo de clase se pueden usar para identificar objetos o para encapsular valores o estados en esos objetos, pero los lenguajes no hacen distinción entre tales usos. Supongamos que un objeto de clase
George
tiene un campo de tipochar[] chars;
. Ese campo puede encapsular una secuencia de caracteres en:Una matriz que nunca se modificará, ni se expondrá a ningún código que pueda modificarlo, pero al que puedan existir referencias externas.
Una matriz a la que no existen referencias externas, pero que George puede modificar libremente.
Una matriz que es propiedad de George, pero a la que pueden existir vistas externas que podrían representar el estado actual de George.
Además, la variable puede, en lugar de encapsular una secuencia de caracteres, encapsular una vista en vivo en una secuencia de caracteres propiedad de otro objeto
Si
chars
actualmente encapsula la secuencia de caracteres [wind], y George quierechars
encapsular la secuencia de caracteres [wand], hay varias cosas que George podría hacer:A. Construya una nueva matriz que contenga los caracteres [varita mágica] y cambie
chars
para identificar esa matriz en lugar de la anterior.B. Identifique de alguna manera una matriz de caracteres preexistente que siempre contendrá los caracteres [varita mágica] y cambiará
chars
para identificar esa matriz en lugar de la anterior.C. Cambie el segundo carácter de la matriz identificada por
chars
aa
.En el caso 1, (A) y (B) son formas seguras de lograr el resultado deseado. En el caso de que (2), (A) y (C) sean seguros, pero (B) no lo sería [no causaría problemas inmediatos, pero dado que George supondría que tiene la propiedad de la matriz, asumiría que podría cambiar la matriz a voluntad]. En el caso (3), las opciones (A) y (B) romperían las vistas externas y, por lo tanto, solo la opción (C) es correcta. Por lo tanto, saber cómo modificar la secuencia de caracteres encapsulada por el campo requiere saber qué tipo de campo semántico es.
Si en lugar de utilizar un campo de tipo
char[]
, que encapsula una secuencia de caracteres potencialmente mutable, el código ha utilizado el tipoString
, que encapsula una secuencia de caracteres inmutable, todos los problemas anteriores desaparecen. Todos los campos de tipoString
encapsulan una secuencia de caracteres usando un objeto compartible que nunca cambiará. En consecuencia, si un campo de tipoString
encapsula "wind", la única forma de encapsular "wand" es hacer que identifique un objeto diferente, uno que contenga "wand". En los casos en que el código contiene la única referencia al objeto, mutar el objeto puede ser más eficiente que crear uno nuevo, pero cada vez que una clase es mutable es necesario distinguir entre las diferentes formas en que puede encapsular el valor. Personalmente, creo que las aplicaciones húngaras deberían haberse utilizado para esto (consideraría que los cuatro usos dechar[]
son tipos semánticamente distintos, a pesar de que el sistema de tipos los considera idénticos, exactamente el tipo de situación en la que brilla la aplicación húngara), pero dado que No era la forma más fácil de evitar tales ambigüedades es diseñar tipos inmutables que solo encapsulan valores de una manera.fuente
Hay algunos buenos ejemplos aquí, pero quería saltar con algunos personales donde la inmutabilidad ayudó mucho. En mi caso, comencé a diseñar una estructura de datos simultánea inmutable principalmente con la esperanza de poder ejecutar el código con seguridad en paralelo con lecturas y escrituras superpuestas y no tener que preocuparme por las condiciones de la carrera. Hubo una charla que John Carmack me dio que me inspiró a hacerlo cuando habló de tal idea. Es una estructura bastante básica y bastante trivial para implementar de esta manera:
Por supuesto, con algunas campanas y silbatos más como poder eliminar elementos en tiempo constante y dejar agujeros recuperables y hacer que los bloques se vuelvan a deshacer si se vuelven vacíos y potencialmente liberados para una instancia inmutable dada. Pero, básicamente, para modificar la estructura, modifica una versión "transitoria" y confirma atómicamente los cambios que realizó para obtener una nueva copia inmutable que no toque la anterior, y la nueva versión solo crea nuevas copias de los bloques que deben hacerse únicos mientras se copian poco a poco y se cuentan las referencias.
Sin embargo, no me pareció queútil para propósitos de subprocesos múltiples. Después de todo, todavía existe el problema conceptual en el que, por ejemplo, un sistema de física aplica la física simultáneamente mientras un jugador está tratando de mover elementos en un mundo. ¿Con qué copia inmutable de los datos transformados vas, la que el jugador transformó o la que transformó el sistema físico? Por lo tanto, realmente no he encontrado una solución agradable y simple para este problema conceptual básico, excepto tener estructuras de datos mutables que simplemente se bloquean de una manera más inteligente y desalientan las lecturas y escrituras superpuestas en las mismas secciones del búfer para evitar detener los hilos. Eso es algo que John Carmack parece haber descubierto cómo resolver en sus juegos; al menos habla de eso como si casi pudiera ver una solución sin abrir un auto de gusanos. No he llegado tan lejos como él en ese sentido. Todo lo que puedo ver son interminables preguntas de diseño si solo intento paralelizar todo alrededor de los inmutables. Desearía poder pasar un día hurgando en su cerebro ya que la mayoría de mis esfuerzos comenzaron con esas ideas que él arrojó.
Sin embargo, encontré un enorme valor de esta estructura de datos inmutable en otras áreas. Incluso lo uso ahora para almacenar imágenes, lo cual es realmente extraño y hace que el acceso aleatorio requiera algunas instrucciones más (desplazamiento a la derecha y un poco a lo
and
largo junto con una capa de dirección indirecta del puntero), pero cubriré los beneficios a continuación.Deshacer sistema
Uno de los lugares más inmediatos que encontré para beneficiarme de esto fue el sistema de deshacer. El código del sistema de deshacer solía ser una de las cosas más propensas a errores en mi área (industria visual FX), y no solo en los productos en los que trabajé sino en los productos de la competencia (sus sistemas de deshacer también eran inestables) porque había muchas diferencias tipos de datos para preocuparse por deshacer y rehacer correctamente (sistema de propiedad, cambios de datos de malla, cambios de sombreador que no se basaron en propiedades como el intercambio de uno con el otro, cambios en la jerarquía de la escena como cambiar el padre de un hijo, cambios de imagen / textura, etc. etc. etc.).
Por lo tanto, la cantidad de código de deshacer requerida era enorme, a menudo rivalizando con la cantidad de código que implementaba el sistema para el cual el sistema de deshacer tenía que registrar los cambios de estado. Al apoyarme en esta estructura de datos, pude hacer que el sistema de deshacer se redujera a esto:
Normalmente, el código anterior sería enormemente ineficiente cuando los datos de su escena abarcan gigabytes para copiarlos en su totalidad. Pero esta estructura de datos solo copia cosas poco profundas que no se modificaron, y en realidad lo hizo lo suficientemente barato como para almacenar una copia inmutable de todo el estado de la aplicación. Así que ahora puedo implementar sistemas de deshacer tan fácilmente como el código anterior y solo enfocarme en usar esta estructura de datos inmutable para hacer que copiar partes no modificadas del estado de la aplicación sea más barato y más barato. Desde que comencé a usar esta estructura de datos, todos mis proyectos personales tienen sistemas de deshacer simplemente usando este patrón simple.
Ahora todavía hay algo de gastos generales aquí. La última vez que medí fue alrededor de 10 kilobytes solo para copiar superficialmente el estado completo de la aplicación sin hacer ningún cambio (esto es independiente de la complejidad de la escena ya que la escena está organizada en una jerarquía, por lo que si nada debajo de la raíz cambia, solo la raíz se copia poco profundo sin tener que descender hacia los niños). Eso está lejos de 0 bytes, ya que sería necesario para un sistema de deshacer que solo almacena deltas. Pero a 10 kilobytes de gastos generales de deshacer por operación, eso sigue siendo solo un megabyte por cada 100 operaciones de usuario. Además, aún podría aplastarlo aún más en el futuro si fuera necesario.
Excepción-Seguridad
La seguridad de excepción con una aplicación compleja no es un asunto trivial. Sin embargo, cuando el estado de su aplicación es inmutable y solo está utilizando objetos transitorios para intentar realizar transacciones de cambio atómico, entonces es inherentemente seguro para excepciones, ya que si se arroja alguna parte del código, el transitorio se descarta antes de dar una nueva copia inmutable . Así que eso trivializa una de las cosas más difíciles que siempre he encontrado en un código base de C ++ complejo.
Demasiadas personas a menudo solo usan recursos conformes con RAII en C ++ y piensan que es suficiente para estar a salvo de excepciones. A menudo no lo es, ya que una función generalmente puede causar efectos secundarios a estados más allá de los locales a su alcance. Por lo general, debe comenzar a tratar con protectores de alcance y una lógica de reversión sofisticada en esos casos. Esta estructura de datos lo hizo así que a menudo no necesito molestarme con eso ya que las funciones no están causando efectos secundarios. Están devolviendo copias inmutables transformadas del estado de la aplicación en lugar de transformar el estado de la aplicación.
Edición no destructiva
La edición no destructiva es básicamente operaciones de capas / apilamiento / conexión juntas sin tocar los datos del usuario original (solo datos de entrada y datos de salida sin tocar la entrada). Por lo general, es trivial implementarlo con una aplicación de imagen simple como Photoshop y es posible que no se beneficie tanto de esta estructura de datos, ya que muchas operaciones pueden simplemente querer transformar cada píxel de toda la imagen.
Sin embargo, con la edición de malla no destructiva, por ejemplo, muchas operaciones a menudo quieren transformar solo una parte de la malla. Una operación puede querer mover algunos vértices aquí. Otro podría simplemente querer subdividir algunos polígonos allí. Aquí, la estructura de datos inmutable ayuda mucho a evitar la necesidad de tener que hacer una copia completa de la malla completa solo para devolver una nueva versión de la malla con una pequeña parte de ella modificada.
Minimizando los efectos secundarios
Con estas estructuras en la mano, también facilita la escritura de funciones que minimizan los efectos secundarios sin incurrir en una gran penalización de rendimiento. Me he encontrado escribiendo más y más funciones que solo devuelven estructuras de datos inmutables por valor en estos días sin incurrir en efectos secundarios, incluso cuando parece un poco inútil.
Por ejemplo, típicamente la tentación de transformar un montón de posiciones podría ser aceptar una matriz y una lista de objetos y transformarlos de manera mutable. En estos días me encuentro devolviendo una nueva lista de objetos.
Cuando tiene más funciones como esta en su sistema que no causan efectos secundarios, definitivamente hace que sea más fácil razonar sobre su corrección y probar su corrección.
Los beneficios de las copias baratas
De todos modos, estas son las áreas donde encontré el mayor uso de estructuras de datos inmutables (o estructuras de datos persistentes). También me puse un poco entusiasta al principio e hice un árbol inmutable y una lista enlazada inmutable y una tabla hash inmutable, pero con el tiempo rara vez encontré tanto uso para esos. Principalmente encontré el mayor uso del grueso contenedor inmutable tipo matriz en el diagrama anterior.
También todavía tengo mucho código trabajando con mutables (considero que es una necesidad práctica al menos para el código de bajo nivel), pero el estado principal de la aplicación es una jerarquía inmutable, que se desglosa de una escena inmutable a componentes inmutables dentro de ella. Algunos de los componentes más baratos todavía se copian en su totalidad, pero los más caros, como las mallas y las imágenes, usan la estructura inmutable para permitir esas copias baratas parciales de solo las partes que necesitan ser transformadas.
fuente
Ya hay muchas buenas respuestas. Esta es solo una información adicional algo específica para .NET. Estaba investigando las publicaciones antiguas de blogs de .NET y encontré un buen resumen de beneficios desde el punto de vista de los desarrolladores de Microsoft Immutable Collections:
fuente