¿Por qué las estructuras mutables son "malas"?
Después de las discusiones aquí sobre SO, ya leí varias veces el comentario de que las estructuras mutables son "malas" (como en la respuesta a esta pregunta ).
¿Cuál es el problema real con la mutabilidad y las estructuras en C#?
Las estructuras son tipos de valores, lo que significa que se copian cuando se pasan.
Entonces, si cambia una copia, solo está cambiando esa copia, no el original ni ninguna otra copia que pueda existir.
Si su estructura es inmutable, todas las copias automáticas resultantes de pasar por valor serán las mismas.
Si desea cambiarlo, debe hacerlo conscientemente creando una nueva instancia de la estructura con los datos modificados. (no una copia)
Por donde empezar ;-p
El blog de Eric Lippert siempre es bueno para una cita:
Ésta es otra razón más por la que los tipos de valores mutables son malos. Intente siempre hacer que los tipos de valores sean inmutables.
Primero, tiendes a perder cambios con bastante facilidad... por ejemplo, sacando cosas de una lista:
Foo foo = list[0];
foo.Name = "abc";
¿Qué cambió eso? Nada útil...
Lo mismo con las propiedades:
myObj.SomeProperty.Size = 22; // the compiler spots this one
obligarte a hacer:
Bar bar = myObj.SomeProperty;
bar.Size = 22;
myObj.SomeProperty = bar;
De manera menos crítica, hay una cuestión de tamaño; los objetos mutables tienden a tener múltiples propiedades; sin embargo, si tiene una estructura con dos int
s, a string
, a DateTime
y a bool
, puede consumir rápidamente mucha memoria. Con una clase, varias personas que llaman pueden compartir una referencia a la misma instancia (las referencias son pequeñas).
No diría mal , pero la mutabilidad es a menudo un signo de exceso de entusiasmo por parte del programador para proporcionar el máximo de funcionalidad. En realidad, esto a menudo no es necesario y eso, a su vez, hace que la interfaz sea más pequeña, más fácil de usar y más difícil de usar incorrectamente (= más robusta).
Un ejemplo de esto son los conflictos de lectura/escritura y escritura/escritura en condiciones de carrera. Esto simplemente no puede ocurrir en estructuras inmutables, ya que una escritura no es una operación válida.
Además, afirmo que la mutabilidad casi nunca es necesaria , el programador simplemente piensa que podría serlo en el futuro. Por ejemplo, simplemente no tiene sentido cambiar una fecha. Más bien, cree una nueva fecha basada en la anterior. Esta es una operación económica, por lo que el rendimiento no es una consideración.
Las estructuras mutables no son malas.
Son absolutamente necesarios en circunstancias de alto rendimiento. Por ejemplo, cuando las líneas de caché o la recolección de basura se convierten en un cuello de botella.
No llamaría "malvado" al uso de una estructura inmutable en estos casos de uso perfectamente válidos.
Puedo ver el punto de que la sintaxis de C# no ayuda a distinguir el acceso de un miembro de un tipo de valor o de un tipo de referencia, por lo que estoy totalmente a favor de preferir estructuras inmutables, que imponen la inmutabilidad, a estructuras mutables.
Sin embargo, en lugar de simplemente etiquetar las estructuras inmutables como "malvadas", recomendaría adoptar el lenguaje y defender reglas generales más útiles y constructivas.
Por ejemplo: "las estructuras son tipos de valores, que se copian de forma predeterminada. Necesita una referencia si no desea copiarlas" o "intente trabajar primero con estructuras de solo lectura" .
Las estructuras con campos o propiedades públicos mutables no son malas.
Los métodos estructurales (a diferencia de los establecedores de propiedades) que mutan "esto" son algo malos, sólo porque .net no proporciona un medio para distinguirlos de los métodos que no lo hacen. Los métodos de estructura que no mutan "esto" deberían poder invocarse incluso en estructuras de solo lectura sin necesidad de realizar copias defensivas. Los métodos que mutan "esto" no deberían ser invocables en estructuras de solo lectura. Dado que .net no quiere prohibir que se invoquen métodos de estructura que no modifican "esto" en estructuras de solo lectura, pero no quiere permitir que se muten estructuras de solo lectura, copia defensivamente estructuras en formato de lectura. sólo contextos, posiblemente obteniendo lo peor de ambos mundos.
A pesar de los problemas con el manejo de métodos automutantes en contextos de solo lectura, las estructuras mutables a menudo ofrecen una semántica muy superior a los tipos de clases mutables. Considere las siguientes tres firmas de métodos:
estructura PointyStruct {público int x,y,z;}; clase PointyClass {público int x,y,z;}; void Método1 (PointyStruct foo); void Método2 (ref PointyStruct foo); Método vacío3 (PointyClass foo);
Para cada método, responda las siguientes preguntas:
- Suponiendo que el método no utiliza ningún código "inseguro", ¿podría modificar foo?
- Si no existen referencias externas a 'foo' antes de llamar al método, ¿podría existir una referencia externa después?
Respuestas:
Pregunta 1:
Method1()
no (intención clara)
Method2()
: sí (intención clara)
Method3()
: sí (intención incierta)
Pregunta 2:
Method1()
no
Method2()
: no (a menos que sea inseguro)
Method3()
: sí
El método 1 no puede modificar foo y nunca obtiene una referencia. Method2 obtiene una referencia de corta duración a foo, que puede usar para modificar los campos de foo cualquier cantidad de veces, en cualquier orden, hasta que regrese, pero no puede conservar esa referencia. Antes de que Method2 regrese, a menos que use código no seguro, todas y cada una de las copias que se hayan podido hacer de su referencia 'foo' habrán desaparecido. El Método3, a diferencia del Método2, obtiene una referencia a foo que se puede compartir de forma promiscua, y no se sabe qué podría hacer con ella. Podría no cambiar foo en absoluto, podría cambiar foo y luego regresar, o podría dar una referencia a foo a otro hilo que podría mutarlo de alguna manera arbitraria en algún momento futuro arbitrario. La única forma de limitar lo que Method3 podría hacer con un objeto de clase mutable que se le pasa sería encapsular el objeto mutable en un contenedor de solo lectura, lo cual es feo y engorroso.
Los conjuntos de estructuras ofrecen una semántica maravillosa. Dado RectArray[500] de tipo Rectángulo, es claro y obvio cómo, por ejemplo, copiar el elemento 123 al elemento 456 y luego, algún tiempo después, establecer el ancho del elemento 123 en 555, sin alterar el elemento 456. "RectArray[432] = RectArray[321 ]; ...; RectArray[123].Ancho = 555;". Saber que Rectángulo es una estructura con un campo entero llamado Ancho le dirá todo lo que necesita saber sobre las declaraciones anteriores.
Ahora supongamos que RectClass fuera una clase con los mismos campos que Rectángulo y uno quisiera realizar las mismas operaciones en un RectClassArray[500] de tipo RectClass. Quizás se supone que la matriz contiene 500 referencias inmutables preinicializadas a objetos RectClass mutables. en ese caso, el código adecuado sería algo así como "RectClassArray[321].SetBounds(RectClassArray[456]); ...; RectClassArray[321].X = 555;". Quizás se supone que la matriz contiene instancias que no van a cambiar, por lo que el código adecuado sería más como "RectClassArray[321] = RectClassArray[456]; ...; RectClassArray[321] = New RectClass(RectClassArray[321 ]); RectClassArray[321].X = 555;" Para saber qué se supone que debemos hacer, habría que saber mucho más sobre RectClass (por ejemplo, si admite un constructor de copia, un método de copia, etc.) y el uso previsto de la matriz. Ni de lejos tan limpio como usar una estructura.
Sin duda, desafortunadamente no existe una buena manera para que cualquier clase de contenedor que no sea una matriz ofrezca la semántica limpia de una matriz de estructura. Lo mejor que se podría hacer, si uno quisiera indexar una colección, por ejemplo, con una cadena, probablemente sería ofrecer un método genérico "ActOnItem" que aceptaría una cadena para el índice, un parámetro genérico y un delegado que se pasaría. por referencia tanto el parámetro genérico como el elemento de colección. Eso permitiría casi la misma semántica que las matrices de estructuras, pero a menos que se pueda convencer a la gente de vb.net y C# para que ofrezcan una buena sintaxis, el código tendrá un aspecto torpe incluso si tiene un rendimiento razonable (pasar un parámetro genérico sería permitiría el uso de un delegado estático y evitaría la necesidad de crear instancias de clase temporales).
Personalmente, estoy enojado por el odio que Eric Lippert et al. arrojar sobre tipos de valores mutables. Ofrecen una semántica mucho más limpia que los tipos de referencia promiscuos que se utilizan en todas partes. A pesar de algunas de las limitaciones del soporte de .net para tipos de valores, hay muchos casos en los que los tipos de valores mutables se adaptan mejor que cualquier otro tipo de entidad.