Después de las discusiones aquí sobre SO, ya leí varias veces la observación 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 #?
c#
struct
immutability
mutable
Dirk Vollmar
fuente
fuente
int
s mutablesbool
, y todos los demás tipos de valor son malvados. Hay casos de mutabilidad e inmutabilidad. Esos casos dependen del papel que juegan los datos, no del tipo de asignación / intercambio de memoria.int
y nobool
son mutables ...
-sintaxis, haciendo que las operaciones con datos refinados y datos con valores se vean iguales aunque sean claramente diferentes. Esto es un error de las propiedades de C #, no de las estructuras: algunos lenguajes ofrecen unaa[V][X] = 3.14
sintaxis alternativa para la mutación in situ. En C #, sería mejor ofrecer métodos mutantes de miembros de estructura como 'MutateV (Action <ref Vector2> mutator)' y usarlo comoa.MutateV((v) => { v.X = 3; })
(el ejemplo está demasiado simplificado debido a las limitaciones que C # tiene respecto a laref
palabra clave, pero con algunos soluciones alternativas deberían ser posibles) .x
), esta operación única consta de 4 instrucciones:ldloc.0
(carga la variable de índice 0 en ...T
es tipo Ref es solo una palabra clave que hace que la variable se pase a un método en sí mismo, no una copia del mismo. También tiene sentido para los tipos de referencia, ya que podemos cambiar la variable , es decir, la referencia fuera del método apuntará a otro objeto después de ser cambiado dentro del método. Comoref T
no es un tipo, pero es una forma de pasar un parámetro de método, no puede ponerlo<>
, porque solo se pueden poner tipos allí. Entonces es simplemente incorrecto. Tal vez sería conveniente hacerlo, tal vez el equipo de C # podría hacer esto para una nueva versión, pero ahora están trabajando en algunos ...Respuestas:
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 el original y 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)
fuente
Por dónde empezar ;-p
El blog de Eric Lippert siempre es bueno para una cita:
Primero, tiende a perder los cambios con bastante facilidad ... por ejemplo, sacar cosas de una lista:
¿Qué cambió eso? Nada útil ...
Lo mismo con las propiedades:
obligándote a hacer:
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, astring
, aDateTime
y abool
, puede grabar rápidamente mucha memoria. Con una clase, varias personas que llaman pueden compartir una referencia a la misma instancia (las referencias son pequeñas).fuente
++
operador. En este caso, el compilador simplemente escribe la asignación explícita en lugar de apresurar al programador.No diría mal, 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 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 realmente necesaria , el programador solo piensa que podría ser 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.
fuente
float
componentes de una transformación gráfica. Si dicho método devuelve una estructura de campo expuesto con seis componentes, es obvio que modificar los campos de la estructura no modificará el objeto gráfico del que se recibió. Si dicho método devuelve un objeto de clase mutable, tal vez cambiar sus propiedades cambiará el objeto gráfico subyacente y tal vez no lo hará; nadie lo sabe realmente.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" el 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, así que estoy a favor de preferir estructuras inmutables, que imponen la inmutabilidad, sobre estructuras mutables.
Sin embargo, en lugar de simplemente etiquetar las estructuras inmutables como "malvadas", recomendaría adoptar el lenguaje y abogar por una regla práctica 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 estructuras de solo lectura primero" .
fuente
struct
con campos públicos) que definir una clase que se pueda usar, torpemente, para lograr los mismos fines, o agregar un montón de basura a una estructura para hacer que emule dicha clase (en lugar de tenerla comportarse como un conjunto de variables pegadas con cinta adhesiva, que es lo que uno realmente quiere en primer lugar)Las estructuras con campos o propiedades públicas mutables no son malas.
Los métodos de estructura (a diferencia de los establecedores de propiedades) que mutan "esto" son algo malos, solo 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 ser invocables incluso en estructuras de solo lectura sin necesidad de copia defensiva. Los métodos que mutan "esto" no deberían ser invocables en absoluto en estructuras de solo lectura. Dado que .net no quiere prohibir que los métodos de estructura que no modifican "esto" se invoquen en estructuras de solo lectura, pero no quiere permitir que se muten estructuras de solo lectura, copia defensivamente estructuras en estructuras de lectura solo contextos, posiblemente obteniendo lo peor de ambos mundos.
Sin embargo, a pesar de los problemas con el manejo de los métodos de auto mutación 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:
Para cada método, responda las siguientes preguntas:
Respuestas:
Method1 no puede modificar foo y nunca obtiene una referencia. Method2 obtiene una referencia de corta duración para foo, que puede usar para modificar los campos de foo cualquier cantidad 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 inseguro, todas y cada una de las copias que se hayan hecho de su referencia 'foo' habrán desaparecido. Method3, a diferencia del Method2, obtiene una referencia promiscuamente compartible para foo, y no se sabe qué podría hacer con él. Puede que no cambie en absoluto, puede cambiar y luego volver, o puede dar una referencia a otro hilo que podría mutarlo de alguna manera arbitraria en algún momento futuro arbitrario.
Las matrices de estructuras ofrecen una semántica maravillosa. Dado RectArray [500] de tipo Rectangle, es claro y obvio cómo, por ejemplo, copiar el elemento 123 en el elemento 456 y luego, un tiempo después, establecer el ancho del elemento 123 en 555, sin alterar el elemento 456. "RectArray [432] = RectArray [321 ]; ...; RectArray [123] .Width = 555; ". Saber que Rectángulo es una estructura con un campo entero llamado Ancho le dirá a uno todo lo que uno necesita saber sobre las declaraciones anteriores.
Ahora suponga que RectClass era una clase con los mismos campos que Rectangle y se deseaba 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 apropiado sería algo así como "RectClassArray [321] .SetBounds (RectClassArray [456]); ...; RectClassArray [321] .X = 555;". Tal vez se supone que la matriz contiene instancias que no van a cambiar, por lo que el código apropiado sería más parecido a "RectClassArray [321] = RectClassArray [456]; ...; RectClassArray [321] = New RectClass (RectClassArray [321 ]); RectClassArray [321] .X = 555; " Para saber lo que se supone que debe hacer, habría que saber mucho más sobre RectClass (por ejemplo, ¿admite un constructor de copias, un método de copia, etc.? ) y el uso previsto de la matriz. Nada tan limpio como usar una estructura.
Para estar seguros, desafortunadamente no hay 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 se quisiera indexar una colección con, por ejemplo, una cadena, probablemente sería ofrecer un método genérico "ActOnItem" que aceptara 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 estructura, pero a menos que las personas vb.net y C # puedan ser perseguidas para ofrecer una buena sintaxis, el código tendrá un aspecto torpe incluso si es un rendimiento razonable (pasar un parámetro genérico sería 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. vomitar 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 por todas partes. 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 mutable encajan mejor que cualquier otro tipo de entidad.
fuente
Rectangle
, fácilmente podría llegar a una situación común en la que se obtiene un comportamiento muy poco claro . Tenga en cuenta que WinForms implementa unRectangle
tipo mutable utilizado en laBounds
propiedad del formulario . Si quiero cambiar los límites, me gustaría usar su buena sintaxis:form.Bounds.X = 10;
Sin embargo, esto no cambia exactamente nada en el formulario (y genera un error encantador que le informa al respecto). La inconsistencia es la ruina de la programación y es por eso que se desea la inmutabilidad.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 valores 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 Konrad menciona, 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 alguna dependencia de estado o contexto.
Espero que esto tenga sentido para ti. Sin duda, se trata más del concepto que intenta capturar con tipos de valor que de detalles prácticos.
fuente
int
iterador, que sería completamente inútil si fuera inmutable. Creo que está combinando "implementaciones de compilador / tiempo de ejecución de tipos de valor" con "variables escritas en un tipo de valor"; este último es ciertamente mutable a cualquiera de los valores posibles.Hay un par de otros casos de esquina que podrían conducir a un comportamiento impredecible desde el punto de vista del programador.
Tipos de valores inmutables y campos de solo lectura
Tipos de valores mutables y matriz
Supongamos que tenemos una matriz de nuestra
Mutable
estructura y estamos llamando alIncrementI
método para el primer elemento de esa matriz. ¿Qué comportamiento esperas de esta llamada? ¿Debería cambiar el valor de la matriz o solo una copia?Por lo tanto, las estructuras mutables no son malas siempre que usted y el resto del equipo entiendan claramente lo que está haciendo. Pero hay demasiados casos en los que el comportamiento del programa sería diferente de lo esperado, lo que podría conducir a errores sutiles difíciles de producir y difíciles de entender.
fuente
T[]
índice entero y un índice, y proporcionando que un acceso a una propiedad de tipoArrayRef<T>
se interpretará como un acceso al elemento de matriz apropiado) [si una clase quisiera exponer unArrayRef<T>
para cualquier otro propósito, podría proporcionar un método, en lugar de una propiedad, para recuperarlo]. Lamentablemente, no existen tales disposiciones.Si alguna vez ha programado en un lenguaje como C / C ++, las estructuras están bien para usar como mutables. Simplemente páselos con referencia, 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 al estúpido a usar una referencia a la estructura, en lugar de una Copia (como cuando una estructura es parte de una clase de C # )
Entonces, las estructuras mutables no son malas, C # las ha hecho malvadas. Uso estructuras mutables en C ++ todo el tiempo y son muy convenientes e intuitivas. Por el contrario, 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.
fuente
readonly
, pero si se evita hacer esas cosas, los campos de clase de tipos de estructura están bien. La única limitación realmente fundamental de las estructuras es que un campo de estructura de un tipo de clase mutableint[]
puede encapsular identidad o un conjunto de valores invariables, pero no puede usarse para encapsular valores mutables sin encapsular también una identidad no deseada.Si se atiene 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: los tratará como un conjunto 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 inherentemente malvadas, si se usan correctamente. Algunas estructuras son inmutables como pretenden (este es el caso de Python
namedtuple
), pero es otro paradigma a tener en cuenta.Sí: las estructuras implican mucha memoria, pero no será precisamente más memoria al hacer:
comparado con:
El consumo de memoria será al menos el mismo, 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 representa la estructura de datos (no es un objeto apropiado, por lo que no tienen identidad de ninguna manera), y los datos pueden modificarse. En otros idiomas, registro (en lugar de struct , como es el caso de Pascal) es la palabra y tiene el mismo propósito: solo una variable de registro de datos, destinada a ser leída de archivos, modificada y volcada en archivos (ese es el principal uso y, en muchos idiomas, incluso puede definir la alineación de datos en el registro, aunque ese no es necesariamente el caso de los objetos llamados correctamente).
¿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 que implementan 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). Entonces, para los archivos y el 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. Solo úselos para el propósito correcto.
Si piensas en términos de martillos, querrás tratar los tornillos como clavos, para encontrar que los tornillos son más difíciles de hundir en la pared, y será culpa de los tornillos, y serán los malvados.
fuente
Imagine que tiene una matriz de 1,000,000 de estructuras. Cada estructura representa una equidad con cosas 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 asignado en el montón no administrado para que algún otro subproceso de código nativo pueda acceder simultáneamente a la matriz (quizás algún código de alto rendimiento haciendo 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.
Imagine que esto se está haciendo decenas o incluso cientos de miles de veces por segundo.
Bueno, reconozcamos 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á; deben serlo porque hacer una copia de una estructura de 120 bytes a estas velocidades es una locura, especialmente cuando una actualización puede afectar solo un byte o dos.
Hugo
fuente
Cuando algo puede mutar, adquiere un sentido de identidad.
Como
Person
es mutable, es más natural pensar en cambiar la posición de Eric que clonar a Eric, mover el clon y destruir el original . Ambas operaciones tendrían éxito en cambiar el contenido deeric.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 a un método un clon de Eric casi siempre será sorprendente. Cualquiera que quiera mutarPerson
debe recordar pedir una referenciaPerson
o estará haciendo lo incorrecto.Si hace que el tipo sea inmutable, el problema desaparece; si no puedo modificar
eric
, no me importa si reciboeric
o si soy un cloneric
. En términos más generales, un tipo es seguro de pasar por valor si todo su estado observable se mantiene en miembros que son:Si se cumplen esas condiciones, un tipo de valor mutable se comporta como un tipo de referencia porque una copia superficial aún permitirá que el receptor modifique los datos originales.
Sin
Person
embargo, la intuición de un inmutable depende de lo que intentes hacer. SiPerson
solo representa un conjunto de datos sobre una persona, no tiene nada de intuitivo;Person
Las variables realmente representan valores abstractos , no objetos. (En ese caso, probablemente sería más apropiado cambiarle el nombrePersonData
). Si enPerson
realidad está modelando 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 hacerPerson
un tipo de referencia (es decir, una clase).De acuerdo, ya que la programación funcional nos ha enseñado que hacer que todo sea inmutable es beneficioso (nadie puede guardar una referencia en secreto
eric
y mutarlo), pero como eso no es idiomático en OOP, seguirá siendo poco intuitivo para cualquier otra persona que trabaje con su código.fuente
foo
contiene la única referencia a su objetivo en cualquier parte del universo, y nada ha capturado el valor de hash de identidad de ese objeto, entonces el campo mutantefoo.X
es semánticamente equivalente afoo
señalar un nuevo objeto que es exactamente como el que se refirió anteriormente, pero conX
manteniendo el valor deseado. Con los tipos de clase, generalmente es difícil saber si existen múltiples referencias a algo, pero con estructuras es fácil: no lo hacen.Thing
es un tipo de clase mutable, unaThing[]
se encapsulan las identidades de objetos - si uno quiere que o no - a menos que se puede asegurar que no hayThing
en la matriz a la cual existen referencias externas jamás se mutaron. Si uno no quiere que los elementos de la matriz encapsulen la identidad, generalmente debe asegurarse de que no se mutarán los elementos a los que tiene referencias, o que nunca existirán referencias externas a los elementos que posee [los enfoques híbridos también pueden funcionar ] Ningún enfoque es terriblemente conveniente. SiThing
es una estructura,Thing[]
solo encapsula valores.No tiene nada que ver con estructuras (y tampoco con C #), pero en Java puede tener problemas con objetos mutables cuando son, por ejemplo, claves en un mapa hash. Si los cambia después de agregarlos a un mapa y cambia su código hash , podrían suceder cosas malas.
fuente
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 manera controlada. Sin embargo, cuando tiene un conjunto público y obtiene funciones que hacen poco más que establecer el valor de lo que se pasa, ¿cómo es esto una mejora sobre 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 impide un uso válido de estructuras que se utilizan para organizar variables públicas, si quisiera control de acceso usaría una clase.
fuente
Hay varios problemas con el ejemplo del Sr. Eric Lippert. Está diseñado para ilustrar el punto en que las estructuras se copian y cómo eso 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 ninguna encapsulación. 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, llamaría a un método en la clase para mutar la estructura del miembro. Esto es lo que haría como un buen hábito de programación.
Una implementación adecuada sería la siguiente.
Parece que es un problema con el hábito de programación en lugar de un problema con la estructura misma. 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 esperaba:
1 2 3 Presione cualquier tecla para continuar. . .
fuente
Hay muchas ventajas y desventajas de los datos mutables. La desventaja del millón de dólares es un alias. Si se usa el mismo valor en varios lugares, y uno de ellos lo cambia, parecerá que ha cambiado mágicamente a los otros lugares que lo están usando. Esto está relacionado, pero no es idéntico con, las condiciones de carrera.
La ventaja del millón de dólares es la modularidad, a veces. El estado mutable puede permitirle ocultar información cambiante del código que no necesita saber al respecto.
El Arte del Intérprete entra en estas compensaciones con cierto detalle y da algunos ejemplos.
fuente
No creo que sean malvados si se usan correctamente. No lo incluiría en mi código de producción, pero lo haría para algo como simulaciones de pruebas de unidades estructuradas, donde la vida útil de una estructura es relativamente pequeña.
Usando el ejemplo de Eric, tal vez desee 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 ocurra 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 código heredado que define poco profundo un objeto particular (el punto de estructuras), pero al tener una estructura inmutable, esto evita su uso molesto.
fuente
Range<T>
tipo con miembrosMinimum
yMaximum
campos de tipoT
y códigoRange<double> myRange = foo.getRange();
, cualquier garantía sobre quéMinimum
y quéMaximum
contenido debería provenirfoo.GetRange();
. TenerRange
una estructura de campo expuesto dejaría en claro que no va a agregar ningún comportamiento propio.