c# - ¿Por qué son las estructuras mutables "malvadas"?
immutability (16)
Siguiendo las discusiones aquí en SO, ya leí varias veces la observación de que las estructuras mutables son "malas" (como en la respuesta a esta question ).
¿Cuál es el problema real con la mutabilidad y las estructuras en C #?
Hay otros casos de esquinas que podrían llevar a un comportamiento impredecible desde el punto de vista de los programadores. Aquí un par de ellos.
- Tipos de valores inmutables y campos de solo lectura
// Simple mutable structure.
// Method IncrementI mutates current state.
struct Mutable
{
public Mutable(int i) : this()
{
I = i;
}
public void IncrementI() { I++; }
public int I {get; private set;}
}
// Simple class that contains Mutable structure
// as readonly field
class SomeClass
{
public readonly Mutable mutable = new Mutable(5);
}
// Simple class that contains Mutable structure
// as ordinary (non-readonly) field
class AnotherClass
{
public Mutable mutable = new Mutable(5);
}
class Program
{
void Main()
{
// Case 1. Mutable readonly field
var someClass = new SomeClass();
someClass.mutable.IncrementI();
// still 5, not 6, because SomeClass.mutable field is readonly
// and compiler creates temporary copy every time when you trying to
// access this field
Console.WriteLine(someClass.mutable.I);
// Case 2. Mutable ordinary field
var anotherClass = new AnotherClass();
anotherClass.mutable.IncrementI();
//Prints 6, because AnotherClass.mutable field is not readonly
Console.WriteLine(anotherClass.mutable.I);
}
}
- Tipos de valor mutable y matriz
Supongamos que tenemos una matriz de nuestra estructura Mutable y estamos llamando al método IncrementI para el primer elemento de esa matriz. ¿Qué comportamiento estás esperando de esta llamada? ¿Debería cambiar el valor de la matriz o solo una copia?
Mutable[] arrayOfMutables = new Mutable[1];
arrayOfMutables[0] = new Mutable(5);
// Now we actually accessing reference to the first element
// without making any additional copy
arrayOfMutables[0].IncrementI();
//Prints 6!!
Console.WriteLine(arrayOfMutables[0].I);
// Every array implements IList<T> interface
IList<Mutable> listOfMutables = arrayOfMutables;
// But accessing values through this interface lead
// to different behavior: IList indexer returns a copy
// instead of an managed reference
listOfMutables[0].IncrementI(); // Should change I to 7
// Nope! we still have 6, because previous line of code
// mutate a copy instead of a list value
Console.WriteLine(listOfMutables[0].I);
Por lo tanto, las estructuras mutables no son malas siempre que usted y el resto del equipo entiendan claramente lo que están haciendo. Pero hay demasiados casos de esquina cuando el comportamiento del programa sería diferente del esperado, lo que podría llevar a errores sutiles difíciles de producir y difíciles de entender.
Imagina que tienes un conjunto de 1,000,000 estructuras. Cada estructura representa un capital con elementos como bid_price, offer_price (quizás decimales), etc., esto es creado por C # / VB.
Imagine que la matriz se crea en un bloque de memoria asignada en el montón no administrado, de modo que otros subprocesos de código nativo puedan acceder simultáneamente a la matriz (quizás algún código de alto rendimiento que haga matemáticas).
Imagine que el código C # / VB está escuchando una fuente de cambios de precios en el mercado, ese código puede tener que acceder a algún elemento de la matriz (para cualquier seguridad) y luego modificar algunos campos de precios.
Imagina que esto se está haciendo decenas o incluso cientos de miles de veces por segundo.
Bueno, enfrentemos los hechos, en este caso realmente queremos que estas estructuras sean mutables, deben serlo porque están siendo compartidas por algún otro código nativo, por lo que crear copias no ayudará. tienen que serlo porque hacer una copia de una estructura de 120 bytes a estas tasas es una locura, especialmente cuando una actualización puede afectar solo a uno o dos bytes.
Hugo
Las estructuras mutables no son malas.
Son absolutamente necesarios en circunstancias de alto rendimiento. Por ejemplo, cuando las líneas de caché y / 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 a favor de las estructuras inmutables, que imponen la inmutabilidad, en lugar de las estructuras mutables.
Sin embargo, en lugar de simplemente etiquetar estructuras inmutables como "malvadas", aconsejaría adoptar el lenguaje y abogar por una regla de pulgares más útil y constructiva.
Por ejemplo: "las estructuras son tipos de valor, que se copian de forma predeterminada. Necesita una referencia si no desea copiarlas" o "intente trabajar con las estructuras de solo lectura primero" .
Las estructuras son tipos de valor, lo que significa que se copian cuando se pasan.
Entonces, si cambia una copia, solo está cambiando esa copia, no la original y ninguna otra copia que pueda estar alrededor.
Si su estructura es inmutable, todas las copias automáticas que resulten de ser pasadas 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)
Los tipos de valor básicamente representan conceptos inmutables. Fx, no tiene sentido tener un valor matemático como un entero, un vector, etc. y luego poder modificarlo. Eso sería como redefinir el significado de un valor. En lugar de cambiar un tipo de valor, tiene más sentido asignar otro valor único. Piense en el hecho de que los tipos de valor se comparan comparando todos los valores de sus propiedades. El punto es que si las propiedades son las mismas, entonces es la misma representación universal de ese valor.
Como menciona Konrad, tampoco tiene sentido cambiar una fecha, ya que el valor representa ese punto único en el tiempo y no una instancia de un objeto de tiempo que tenga algún estado o dependencia del contexto.
Espera que esto tenga algún sentido para ti. Es más sobre el concepto que intenta capturar con tipos de valor que sobre detalles prácticos, para estar seguro.
No diría maldad, pero la mutabilidad es a menudo una señal de exceso de entusiasmo por parte del programador para proporcionar la máxima 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 mal (= 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 realmente necesaria , el programador solo 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 barata, por lo que el rendimiento no es una consideración.
Por dónde empezar ;-p
El blog de Eric Lippert siempre es bueno para una cita:
Esta es otra razón por la cual los tipos de valores mutables son malos. Intenta siempre hacer tipos de valor inmutables.
Primero, tiende a perder los cambios con bastante facilidad ... por ejemplo, eliminando 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
obligándote a hacer:
Bar bar = myObj.SomeProperty;
bar.Size = 22;
myObj.SomeProperty = bar;
menos críticamente, hay un problema de tamaño; Los objetos mutables tienden a tener múltiples propiedades; sin embargo, si tiene una estructura con dos int
s, una string
, un DateTime
y un bool
, puede grabar rápidamente gran cantidad de memoria. Con una clase, varias personas que llaman pueden compartir una referencia a la misma instancia (las referencias son pequeñas).
Si alguna vez ha programado en un lenguaje como C / C ++, las estructuras pueden usarse como mutables. Simplemente pase con ref, alrededor y no hay nada que pueda salir mal. El único problema que encuentro son las restricciones del compilador de C # y que, en algunos casos, no puedo forzar la estupidez de usar una referencia a la estructura, en lugar de una Copia (como cuando una estructura es parte de una clase C # ).
Entonces, las estructuras mutables no son malas, C # las ha hecho malas. Uso estructuras mutables en C ++ todo el tiempo y son muy convenientes e intuitivas. En contraste, C # me ha hecho abandonar completamente las estructuras como miembros de clases debido a la forma en que manejan los objetos. Su conveniencia nos ha costado la nuestra.
Si se adhiere a qué estructuras están destinadas (en C #, Visual Basic 6, Pascal / Delphi, tipo de estructura C ++ (o clases) cuando no se usan como punteros), encontrará que una estructura no es más que una variable compuesta . Esto significa que los tratará como un conjunto empaquetado de variables, bajo un nombre común (una variable de registro de la que hace referencia a los miembros).
Sé que eso confundiría a mucha gente profundamente acostumbrada a la POO, pero esa no es razón suficiente para decir que tales cosas son intrínsecamente malas, si se usan correctamente. Algunas estructuras son inmutables como pretenden (este es el caso de la namedtuple
de Python), pero es otro paradigma a considerar.
Sí: las estructuras implican mucha memoria, pero no será precisamente más memoria haciendo:
point.x = point.x + 1
comparado con:
point = Point(point.x + 1, point.y)
El consumo de memoria será al menos igual, o incluso más en el caso inmutable (aunque ese caso sería temporal, para la pila actual, dependiendo del idioma).
Pero, finalmente, las estructuras son estructuras , no objetos. En POO, la propiedad principal de un objeto es su identidad , que la mayoría de las veces no es más que su dirección de memoria. Struct significa estructura de datos (no es un objeto adecuado y, por lo tanto, no tienen identidad) y los datos pueden modificarse. En otros idiomas, registro (en lugar de estructura , como es el caso de Pascal) es la palabra y tiene el mismo propósito: solo una variable de registro de datos, que debe leerse desde archivos, modificarse y volcarse en archivos (que es el principal use y, en muchos idiomas, incluso puede definir la alineación de los datos en el registro, mientras que ese no es necesariamente el caso de los correctamente llamados Objetos).
¿Quieres un buen ejemplo? Las estructuras se utilizan para leer archivos fácilmente. Python tiene esta biblioteca porque, dado que está orientada a objetos y no tiene soporte para estructuras, tuvo que implementarla de otra manera, lo cual es algo feo. Los lenguajes de implementación de estructuras tienen esa característica ... incorporada.Intente leer un encabezado de mapa de bits con una estructura apropiada en lenguajes como Pascal o C. Será fácil (si la estructura está correctamente construida y alineada; en Pascal no usaría un acceso basado en registros, sino funciones para leer datos binarios arbitrarios). Por lo tanto, para archivos y acceso directo a la memoria (local), las estructuras son mejores que los objetos. En cuanto a hoy, estamos acostumbrados a JSON y XML, por lo que olvidamos el uso de archivos binarios (y, como efecto secundario, el uso de estructuras). Pero sí: existen, y tienen un propósito.
No son malvados. Sólo úsalos para el propósito correcto.
Si piensa en términos de martillos, querrá tratar los tornillos como clavos, para encontrar los tornillos es más difícil hundirlos en la pared, y será culpa de los tornillos, y serán los malos.
Las estructuras con campos o propiedades mutables públicas no son malas.
Los métodos Struct (a diferencia de los establecedores de propiedades) que mutan "esto" son algo malvados, solo porque .net no proporciona un medio para distinguirlos de los métodos que no lo hacen. Los métodos de Struct que no mutan "esto" deberían ser invocables incluso en estructuras de solo lectura sin necesidad de copia defensiva. Los métodos que mutan "esto" no deben ser invocables en absoluto en estructuras de solo lectura. Como .net no quiere prohibir que los métodos de estructura que no modifican "esto" se invoque en estructuras de solo lectura, pero no quiere que se muten estructuras de solo lectura, copia defensivamente las estructuras en lectura Sólo contextos, posiblemente lo peor de ambos mundos.
Sin embargo, a pesar de los problemas con el manejo de los métodos de automutación en contextos de solo lectura, las estructuras mutables a menudo ofrecen una semántica muy superior a los tipos de clase mutables. Considere las siguientes tres firmas de métodos:
struct PointyStruct {public int x,y,z;}; class PointyClass {public int x,y,z;}; void Method1(PointyStruct foo); void Method2(ref PointyStruct foo); void Method3(PointyClass foo);
Para cada método, conteste las siguientes preguntas:
- Suponiendo que el método no use 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í
Method1 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 número de veces, en cualquier orden, hasta que regrese, pero no puede persistir esa referencia. Antes de que el Método 2 regrese, a menos que use un código no seguro, todas las copias que se hayan hecho de su referencia ''foo'' habrán desaparecido. El Método 3, a diferencia del Método 2, obtiene una referencia promiscuiblemente compartible a foo, y no se sabe qué podría hacer con él. Puede que no cambie foo en absoluto, puede cambiar foo y luego regresar, o puede dar una referencia a foo a otro hilo que podría mutarlo de alguna manera arbitraria en algún momento futuro arbitrario. La única manera de limitar lo que Method3 podría hacer a un objeto de clase mutable pasado a él sería encapsular el objeto mutable en un contenedor de solo lectura, que es feo y engorroso.
Arreglos de estructuras ofrecen una semántica maravillosa. Dado el RectArray [500] de tipo Rectángulo, es claro y obvio cómo, por ejemplo, copiar el elemento 123 al elemento 456 y luego, un tiempo después, establecer el ancho del elemento 123 en 555, sin molestar al elemento 456. "RectArray [432] = RectArray [321 ]; ...; RectArray [123] .Width = 555; ". Saber que el Rectángulo es una estructura con un campo entero llamado Ancho le dirá a uno todo lo que uno necesita saber acerca de las afirmaciones anteriores.
Ahora supongamos que RectClass era una clase con los mismos campos que Rectangle y uno quería hacer 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 correcto 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 correcto sería más parecido a "RectClassArray [321] = RectClassArray [456]; ...; RectClassArray [321] = New RectClass (RectClassArray [321 ]); RectClassArray [321] .X = 555; " Para saber qué se supone que uno debe hacer, uno tendría que saber mucho más sobre RectClass (por ejemplo, admite un constructor de copia, un método de copia de, etc.) y el uso previsto de la matriz. Nada tan limpio como usar una estructura.
Para estar seguro, desafortunadamente no existe una forma agradable para que cualquier clase de contenedor que no sea una matriz ofrezca la semántica limpia de una matriz de estructura. Lo mejor que podría hacer, si quisiera que una colección se indexara con, por ejemplo, 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 aprobaría. por referencia tanto el parámetro genérico como el elemento de colección. Eso permitiría casi la misma semántica que los arrays struct, pero a menos que se pueda persuadir a las personas vb.net y C # para que ofrezcan una buena sintaxis, el código tendrá una apariencia torpe incluso si es razonablemente eficaz (pasar un parámetro genérico permitir el uso de un delegado estático y evitaría la necesidad de crear instancias de clase temporales).
Personalmente, estoy molesto por el odio Eric Lippert et al. Spew con respecto a los tipos de valores mutables. Ofrecen una semántica mucho más limpia que los tipos de referencia promiscuos que se utilizan en todo el lugar. A pesar de algunas de las limitaciones con el soporte de .net para los tipos de valor, hay muchos casos en los que los tipos de valor mutables son más adecuados que cualquier otro tipo de entidad.
Cuando algo puede ser mutado, gana un sentido de identidad.
struct Person {
public string name; // mutable
public Point position = new Point(0, 0); // mutable
public Person(string name, Point position) { ... }
}
Person eric = new Person("Eric Lippert", new Point(4, 2));
Debido a que Person
es mutable, es más natural pensar en cambiar la posición de Eric que en clonar a Eric, mover el clon y destruir el original . Ambas operaciones tendrían éxito en cambiar los contenidos de eric.position
, pero una es más intuitiva que la otra. Del mismo modo, es más intuitivo pasar a Eric (como referencia) por métodos para modificarlo. Darle un método a un clon de Eric es casi siempre sorprendente. Cualquier persona que desee mutar Person
debe recordar pedir una referencia Person
o hará lo incorrecto.
Si haces el tipo inmutable, el problema desaparece; si no puedo modificarlo eric
, no me importa si lo recibo eric
o un clon de eric
. Más generalmente, un tipo es seguro de pasar por valor si todo su estado observable se mantiene en miembros que son:
- inmutable
- tipos de referencia
- seguro para pasar por valor
Si se cumplen esas condiciones, entonces un tipo de valor mutable se comporta como un tipo de referencia porque una copia superficial todavía permitirá que el receptor modifique los datos originales.
La intuición de un inmutable Person
depende de lo que estés tratando de hacer. Si Person
solo representa un conjunto de datos sobre una persona, no hay nada intuitivo al respecto; Person
Las variables realmente representan valores abstractos , no objetos. (En ese caso, probablemente sería más apropiado cambiarle el nombre a PersonData
). Si en Person
realidad es modelar a una persona, la idea de crear y mover clones constantemente es una tontería incluso si ha evitado la trampa de pensar que está modificando el original. En ese caso, probablemente sería más natural simplemente hacer Person
un tipo de referencia (es decir, una clase).
Por supuesto, como la programación funcional nos ha enseñado, hay beneficios en hacer que todo sea inmutable (nadie puede aferrarse en secreto a una referencia eric
y mutarlo), pero como eso no es idiomático en OOP, no será intuitivo para nadie que trabaje con usted. código.
Personalmente cuando miro el código, lo siguiente me parece bastante torpe:
data.value.set (data.value.get () + 1);
en lugar de simplemente
data.value ++; o data.value = data.value + 1;
La encapsulación de datos es útil cuando se pasa una clase y desea asegurarse de que el valor se modifique de forma controlada. Sin embargo, cuando tiene configuraciones públicas y obtiene funciones que hacen poco más que establecer el valor de lo que se pasa, ¿cómo es esto una mejora en lugar de simplemente pasar una estructura de datos pública?
Cuando creo una estructura privada dentro de una clase, creé esa estructura para organizar un conjunto de variables en un grupo. Quiero poder modificar esa estructura dentro del alcance de la clase, no obtener copias de esa estructura y crear nuevas instancias.
Para mí, esto evita un uso válido de las estructuras que se utilizan para organizar las variables públicas. Si quisiera un control de acceso, usaría una clase.
Hay muchas ventajas y desventajas de los datos mutables. La desventaja del millón de dólares es el aliasing. Si se usa el mismo valor en varios lugares, y uno de ellos lo cambia, parecerá que se ha cambiado mágicamente a los otros lugares que lo están utilizando. Esto está relacionado con, pero no idéntico a, las condiciones de la raza.
La ventaja del millón de dólares es la modularidad, a veces. El estado mutable puede permitirle ocultar información cambiante de un código que no necesita conocerla.
El Arte del Intérprete entra en estas concesiones con cierto detalle, y da algunos ejemplos.
Hay varios problemas con el ejemplo del Sr. Eric Lippert. Se ha diseñado para ilustrar el punto en el que se copian las estructuras y cómo podría ser un problema si no tiene cuidado. Mirando el ejemplo, lo veo como resultado de un mal hábito de programación y no es realmente un problema con la estructura o la clase.
Se supone que una estructura solo tiene miembros públicos y no debe requerir ningún encapsulamiento. Si lo hace, entonces realmente debería ser un tipo / clase. Realmente no necesitas dos construcciones para decir lo mismo.
Si tiene una clase que encierra una estructura, debería llamar a un método de la clase para mutar la estructura miembro. Esto es lo que yo haría como un buen hábito de programación.
Una implementación adecuada sería la siguiente.
struct Mutable {
public int x;
}
class Test {
private Mutable m = new Mutable();
public int mutate()
{
m.x = m.x + 1;
return m.x;
}
}
static void Main(string[] args) {
Test t = new Test();
System.Console.WriteLine(t.mutate());
System.Console.WriteLine(t.mutate());
System.Console.WriteLine(t.mutate());
}
Parece que es un problema con el hábito de la programación en lugar de un problema con la estructura en sí. Se supone que las estructuras son mutables, esa es la idea y la intención.
El resultado de los cambios voila se comporta como se espera:
1 2 3 Pulse cualquier tecla para continuar. . .
No creo que sean malvados si se usan correctamente. No lo pondría en mi código de producción, pero lo haría para algo así como simulacros de pruebas de unidades estructuradas, donde la vida útil de una estructura es relativamente pequeña.
Usando el ejemplo de Eric, tal vez quiera crear una segunda instancia de ese Eric, pero haga ajustes, ya que esa es la naturaleza de su prueba (es decir, duplicación, luego modificación). No importa lo que pase con la primera instancia de Eric si solo estamos usando Eric2 para el resto del script de prueba, a menos que esté planeando usarlo como una comparación de prueba.
Esto sería principalmente útil para probar o modificar el código heredado que superficial define un objeto particular (el punto de las estructuras), pero al tener una estructura inmutable, esto evita su uso de manera molesta.
No tiene nada que ver con las estructuras (y tampoco con C #, pero) en Java puede tener problemas con los objetos mutables cuando son, por ejemplo, claves en un mapa hash. Si los cambias después de agregarlos a un mapa y cambia su código hash , podrían ocurrir cosas malas.