¿Por qué las estructuras mutables son "malvadas"?

485

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 #?

Dirk Vollmar
fuente
12
Afirmar que las estructuras mutables son malas es como reclamar ints mutables bool, 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.
Slipp D. Thompson
41
@slipp inty nobool son mutables ..
Blorgbeard sale el
2
... .-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 una a[V][X] = 3.14sintaxis 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 como a.MutateV((v) => { v.X = 3; }) (el ejemplo está demasiado simplificado debido a las limitaciones que C # tiene respecto a la refpalabra clave, pero con algunos soluciones alternativas deberían ser posibles) .
Slipp D. Thompson,
2
@Slipp Bueno, pienso exactamente lo contrario acerca de este tipo de estructuras. ¿Por qué crees que las estructuras que ya están implementadas en la biblioteca .NET, como DateTime o TimeSpan (tan similares) son inmutables? Tal vez podría ser útil cambiar solo un miembro de la var de esa estructura, pero es demasiado inconveniente y genera demasiados problemas. En realidad, está equivocado acerca de lo que calcula el procesador, ya que C # no se compila en ensamblador, se compila en IL. En IL (siempre que tengamos la variable nombrada x), esta operación única consta de 4 instrucciones: ldloc.0(carga la variable de índice 0 en ...
Sushi271
1
... tipo. Tes 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. Como ref Tno 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 ...
Sushi271

Respuestas:

290

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)

vagabundo
fuente
82
"Si su estructura es inmutable, todas las copias serán iguales". No, significa que debe hacer una copia conscientemente si desea un valor diferente. Significa que no quedará atrapado modificando una copia pensando que está modificando el original.
Lucas
19
@Lucas Creo que está hablando de un tipo diferente de copia. Estoy hablando de las copias automáticas hechas como resultado de que se pasaron por valor. Su 'copia hecha conscientemente' es diferente a propósito, no la hizo por error y es No es realmente una copia, es un nuevo instante deliberado que contiene datos diferentes.
Trampster
3
Su edición (16 meses después) lo hace un poco más claro. Sin embargo, sigo con "(estructura inmutable) significa que no te atraparán modificando una copia pensando que estás modificando el original".
Lucas
66
@Lucas: El peligro de hacer una copia de una estructura, modificarla y de alguna manera pensar que uno está modificando el original (cuando el hecho de que uno está escribiendo un campo de estructura hace evidente el hecho de que solo se está escribiendo una copia) bastante pequeño en comparación con el peligro de que alguien que posee un objeto de clase como un medio para mantener la información contenida en él mutee el objeto para actualizar su propia información y en el proceso corrompa la información contenida en algún otro objeto.
supercat
2
El tercer párrafo suena mal o poco claro en el mejor de los casos. Si su estructura es inmutable, entonces simplemente no podrá modificar sus campos o los campos de las copias realizadas. "Si desea cambiar a ella hay que ..." que está demasiado engañosa, no se puede cambiar que nunca , ni consciente ni inconscientemente. Crear una nueva instancia en la que los datos que desea no tienen nada que ver con la copia original aparte de tener la misma estructura de datos.
Saeb Amini
167

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. Intente siempre hacer que los tipos de valor sean inmutables.

Primero, tiende a perder los cambios con bastante facilidad ... por ejemplo, sacar 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 ints, a string, a DateTimey a bool, 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).

Marc Gravell
fuente
55
Bueno, sí, pero el compilador es simplemente estúpido de esa manera. No permitir la asignación a miembros de estructura de propiedad fue en mi humilde opinión una decisión estúpida de diseño, ya que está permitido para el ++operador. En este caso, el compilador simplemente escribe la asignación explícita en lugar de apresurar al programador.
Konrad Rudolph el
12
@Konrad: myObj.SomeProperty.Size = 22 modificaría una COPIA de myObj.SomeProperty. El compilador te está salvando de un error obvio. Y NO está permitido para ++.
Lucas
@Lucas: Bueno, el compilador Mono C # ciertamente lo permite, ya que no tengo Windows, no puedo buscar el compilador de Microsoft.
Konrad Rudolph
66
@Konrad: con una indirección menos, debería funcionar; es el "mutar un valor de algo que solo existe como un valor transitorio en la pila y que está a punto de evaporarse en la nada", que es el caso que está bloqueado.
Marc Gravell
2
@Marc Gravell: en el código anterior, terminas con un "Foo" cuyo nombre es "abc" y cuyos otros atributos son los de la Lista [0], sin perturbar la Lista [0]. Si Foo fuera una clase, sería necesario clonarlo y luego cambiar la copia. En mi opinión, el gran problema con la distinción entre el tipo de valor y la clase es el uso del "." operador para dos propósitos. Si tuviera mis druthers, las clases podrían apoyar a ambos "." y "->" para métodos y propiedades, pero la semántica normal para "." las propiedades serían crear una nueva instancia con el campo apropiado modificado.
Supercat
71

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.

Konrad Rudolph
fuente
1
Eric Lippert dice que son ... mira mi respuesta.
Marc Gravell
42
Por mucho que respeto a Eric Lippert, él no es Dios (o al menos todavía no). La publicación de blog a la que se vincula y su publicación anterior son argumentos razonables para hacer que las estructuras sean inmutables, por supuesto, pero en realidad son muy débiles como argumentos para nunca usar estructuras mutables. Esta publicación, sin embargo, es un +1.
Stephen Martin el
2
Al desarrollarse en C #, generalmente necesita mutabilidad de vez en cuando, especialmente con su modelo de negocio, donde desea que la transmisión, etc., funcione sin problemas con las soluciones existentes. Escribí un artículo sobre cómo trabajar con datos mutables e inmutables, resolviendo la mayoría de los problemas relacionados con la mutabilidad (espero): rickyhelgesson.wordpress.com/2012/07/17/…
Ricky Helgesson
3
@StephenMartin: las estructuras que encapsulan un solo valor a menudo deben ser inmutables, pero las estructuras son, con mucho, el mejor medio para encapsular conjuntos fijos de variables independientes pero relacionadas (como las coordenadas X e Y de un punto) que no tienen "identidad" como un grupo. Las estructuras que se usan para ese propósito generalmente deben exponer sus variables como campos públicos. Consideraría que la idea de que es más apropiado usar una clase que una estructura para tales propósitos es simplemente errónea. Las clases inmutables son a menudo menos eficientes, y las clases mutables a menudo tienen una semántica terrible.
supercat
2
@StephenMartin: considere, por ejemplo, un método o propiedad que se supone que devuelve los seis floatcomponentes 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.
supercat
48

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" .

JE42
fuente
8
También afirmaría que si uno quiere sujetar un conjunto fijo de variables junto con cinta adhesiva para que sus valores puedan procesarse o almacenarse por separado o como una unidad, tiene mucho más sentido pedirle al compilador que fije un conjunto fijo de variables. variables juntas (es decir, declarar a structcon 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)
supercat
41

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:

struct PointyStruct {public int x, y, z;};
clase PointyClass {public int x, y, z;};

Método 1 vacío (PointyStruct foo);
Método nulo2 (ref PointyStruct foo);
Método 3 vacío (PointyClass foo);

Para cada método, responda las siguientes preguntas:

  1. Suponiendo que el método no usa ningún código "inseguro", ¿podría modificar foo?
  2. 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 no sea seguro)
Method3() : sí

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.

Super gato
fuente
1
@Ron Warholic: no es evidente que SomeRect sea un rectángulo. Podría ser algún otro tipo que pueda ser implícitamente encasillado desde Rectangle. Aunque, el único tipo definido por el sistema que se puede encasillar implícitamente desde Rectangle es RectangleF, y el compilador graznaría si uno intentara pasar los campos de un RectangleF al constructor de Rectangle (ya que los primeros son Single y el último Integer) , podría haber estructuras definidas por el usuario que permitan tales tipos de letra implícitos. Por cierto, la primera declaración funcionaría igualmente bien si SomeRect fuera un Rectangle o un RectangleF.
supercat
Todo lo que has demostrado es que en un ejemplo artificial crees que un método es más claro. Si tomamos su ejemplo con 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 un Rectangletipo mutable utilizado en la Boundspropiedad 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.
Ron Warholic
3
@Ron Warholic: Por cierto, me gustaría poder decir "form.Bounds.X = 10;" y que funcione, pero el sistema no proporciona ninguna forma limpia de hacerlo. Una convención para exponer propiedades de tipo de valor como métodos que aceptan devoluciones de llamada podría ofrecer un código mucho más limpio, eficiente y confirmablemente correcto que cualquier enfoque que use clases.
supercat
44
Esta respuesta es mucho más perspicaz que algunas de las respuestas más votadas. Es un poco absurdo que el argumento en contra de los tipos de valores mutables se base en la noción de "lo que esperas" que suceda cuando mezclas aliasing y mutación. ¡Eso es algo terrible de todos modos !
Eamon Nerbonne
2
@supercat: Quién sabe, tal vez esa función de devolución de referencia de la que están hablando para C # 7 podría cubrir esa base (en realidad no la he visto en detalle, pero superficialmente suena similar).
Eamon Nerbonne
24

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.

Morten Christiansen
fuente
Bueno, deberían representar conceptos inmutables, al menos ;-p
Marc Gravell
3
Bueno, supongo que podrían haber hecho que System.Drawing.Point sea inmutable, pero habría sido un grave error de diseño en mi humilde opinión. Creo que los puntos son en realidad un tipo de valor arquetípico y son mutables. Y no causan ningún problema a nadie más allá de la programación muy temprana de 101 principiantes.
Stephen Martin el
3
En principio, creo que los puntos también deberían ser inmutables, pero si hace que el tipo sea más difícil o menos elegante de usar, por supuesto, eso también debe tenerse en cuenta. No tiene sentido tener construcciones de código que defiendan los mejores principios si nadie quiere usarlos;)
Morten Christiansen
3
Los tipos de valor son útiles para representar conceptos inmutables simples, pero las estructuras de campo expuesto son los mejores tipos para usar o retener pequeños conjuntos fijos de valores relacionados pero independientes (como las coordenadas de un punto). Una ubicación de almacenamiento de ese tipo de valor encapsula los valores de sus campos y nada más. Por el contrario, una ubicación de almacenamiento de un tipo de referencia mutable puede usarse con el propósito de mantener el estado del objeto mutable, pero también encapsula la identidad de todas las demás referencias en todo el universo que existen para ese mismo objeto.
supercat
44
"Los tipos de valor básicamente representan conceptos inmutables". No, ellos no. Una de las aplicaciones más antiguas y útiles de una variable de tipo valor es un intiterador, 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.
Slipp D. Thompson
19

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

    // 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 valores mutables y matriz

Supongamos que tenemos una matriz de nuestra Mutableestructura y estamos llamando al IncrementImé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?

    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á 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.

Sergey Teplyakov
fuente
55
Lo que debería suceder, si los lenguajes .net tuvieran un soporte de tipo de valor ligeramente mejor, se prohibiría que los métodos de estructura muten 'esto' a menos que se declaren explícitamente como tales, y los métodos que se declaran así deberían prohibirse en solo lectura contextos Las matrices de estructuras mutables ofrecen una semántica útil que no se puede lograr de manera eficiente por otros medios.
supercat
2
Estos son buenos ejemplos de problemas muy sutiles que surgirían de estructuras mutables. No hubiera esperado nada de este comportamiento. ¿Por qué una matriz te da una referencia, pero una interfaz te da un valor? Hubiera pensado, aparte de los valores de todo el tiempo (que es lo que realmente esperaría), que al menos sería al revés: la interfaz da referencias; matrices que dan valores ...
Dave Cousineau
@Sahuagin: Desafortunadamente, no existe un mecanismo estándar por el cual una interfaz pueda exponer una referencia. Hay formas en que .net podría permitir que tales cosas se hagan de manera segura y útil (por ejemplo, definiendo una estructura especial "ArrayRef <T>" que contenga un T[]índice entero y un índice, y proporcionando que un acceso a una propiedad de tipo ArrayRef<T>se interpretará como un acceso al elemento de matriz apropiado) [si una clase quisiera exponer un ArrayRef<T>para cualquier otro propósito, podría proporcionar un método, en lugar de una propiedad, para recuperarlo]. Lamentablemente, no existen tales disposiciones.
supercat
2
Oh mi ... ¡esto hace que las estructuras mutables sean malditamente malvadas!
nawfal
1
Me gusta esta respuesta porque contiene información muy valiosa que no es obvia. Pero realmente, sin embargo, este no es un argumento contra estructuras mutables como algunos afirman. Sí, lo que vemos aquí es un "pozo de desesperación" como Eric lo habría dicho, pero la fuente de esta desesperación no es la mutabilidad. La fuente de la desesperación son los métodos de auto-mutación de la estructura . (En cuanto a por qué las matrices y las listas se comportan de manera diferente es porque uno es básicamente un operador que calcula una dirección de memoria y el otro es una propiedad. En general, todo queda claro una vez que comprende que una "referencia" es un valor de dirección .)
AnorZaken
18

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.

ThunderGr
fuente
Tener campos de clase de tipos de estructura a menudo puede ser un patrón muy útil, aunque es cierto que existen algunas limitaciones. El rendimiento se degradará si se usan propiedades en lugar de campos o usos 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 mutable int[]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.
supercat
13

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:

point.x = point.x + 1

comparado con:

point = Point(point.x + 1, point.y)

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.

Luis Masuelli
fuente
12

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

Hugo
fuente
3
Es cierto, pero en este caso la razón para usar una estructura es que hacerlo se impone sobre el diseño de la aplicación por restricciones externas (aquellas por el uso del código nativo). Todo lo demás que describa sobre estos objetos sugiere que claramente deberían ser clases en C # o VB.NET.
Jon Hanna
66
No estoy seguro de por qué algunas personas piensan que las cosas deberían ser objetos de clase. Si todos los espacios de la matriz se completan con instancias distintas de referencias, el uso de un tipo de clase agregará doce o veinticuatro bytes adicionales al requisito de memoria, y el acceso secuencial en una matriz de referencias de objetos de clase es mucho más lento que el acceso secuencial en Un conjunto de estructuras.
supercat
9

Cuando algo puede mutar, adquiere 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));

Como Persones 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 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 a un método un clon de Eric casi siempre será sorprendente. Cualquiera que quiera mutar Persondebe recordar pedir una referencia Persono estará haciendo lo incorrecto.

Si hace que el tipo sea inmutable, el problema desaparece; si no puedo modificar eric, no me importa si recibo erico si soy un clon eric. 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:

  • inmutable
  • tipos de referencia
  • seguro para pasar por valor

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 Personembargo, la intuición de un inmutable depende de lo que intentes hacer. Si Personsolo representa un conjunto de datos sobre una persona, no tiene nada de intuitivo; PersonLas variables realmente representan valores abstractos , no objetos. (En ese caso, probablemente sería más apropiado cambiarle el nombre PersonData). Si en Personrealidad 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 hacer Personun 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 ericy mutarlo), pero como eso no es idiomático en OOP, seguirá siendo poco intuitivo para cualquier otra persona que trabaje con su código.

Doval
fuente
3
Tu punto sobre la identidad es bueno; Vale la pena señalar que la identidad es relevante solo cuando existen múltiples referencias a algo. Si foocontiene 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 mutante foo.Xes semánticamente equivalente a fooseñalar un nuevo objeto que es exactamente como el que se refirió anteriormente, pero con Xmanteniendo 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.
Supercat
1
Si Thinges un tipo de clase mutable, una Thing[] se encapsulan las identidades de objetos - si uno quiere que o no - a menos que se puede asegurar que no hay Thingen 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. Si Thinges una estructura, Thing[]solo encapsula valores.
supercat
Para los objetos, su identidad proviene de su ubicación. Las instancias de los tipos de referencia tienen su identidad gracias a su ubicación en la memoria y solo pasa su identidad (una referencia), no sus datos, mientras que los tipos de valor tienen su identidad en el lugar exterior donde se almacenan. La identidad de su tipo de valor Eric proviene solo de la variable donde está almacenado. Si lo pasas, perderá su identidad.
IllidanS4 quiere que Mónica regrese el
6

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.

Bombe
fuente
3
Eso también es cierto si usa una clase como clave en un mapa.
Marc Gravell
6

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.

Miguel
fuente
1
¡Directo al grano! ¡Las estructuras son unidades organizativas sin restricciones de control de acceso! ¡Desafortunadamente, C # los ha hecho inútiles para este propósito!
ThunderGr
esto pierde completamente el punto ya que ambos ejemplos muestran estructuras mutables.
vidstige
C # los hizo inútiles para este propósito porque ese no es el propósito de las estructuras
Luiz Felipe
6

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.

  1. 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.

  2. 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.

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 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. . .

Ramesh Kadambi
fuente
44
No hay nada de malo en diseñar pequeñas estructuras opacas para comportarse como objetos de clase inmutables; Las pautas de MSDN son razonables cuando se intenta hacer algo que se comporte como un objeto . Las estructuras son apropiadas en algunos casos en los que se necesitan cosas livianas que se comporten como objetos, y en los casos en que se necesita un conjunto de variables pegadas con cinta adhesiva. Sin embargo, por alguna razón, muchas personas no se dan cuenta de que las estructuras tienen dos usos distintos, y que las pautas apropiadas para una no son apropiadas para la otra.
supercat
5

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.

Andru Luvisi
fuente
Las estructuras no tienen alias en C #. Cada asignación de estructura es una copia.
recursivo
@recursive: en algunos casos, esa es una gran ventaja de las estructuras mutables, y una que me hace cuestionar la noción de que las estructuras no deberían ser mutables. El hecho de que los compiladores a veces implícitamente copien estructuras no reduce la utilidad de las estructuras mutables.
supercat
1

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.

ShaunnyBwoy
fuente
A mi modo de ver, una estructura es en su núcleo un conjunto de variables pegadas con cinta adhesiva. En .NET es posible que una estructura finja ser algo más que un grupo de variables pegadas con cinta adhesiva, y sugeriría que, cuando sea práctico, un tipo que pretenda ser algo más que un grupo de variables pegadas con cinta adhesiva debe comportarse como un objeto unificado (lo que para una estructura implicaría inmutabilidad), pero a veces es útil pegar un conjunto de variables junto con cinta adhesiva. Incluso en el código de producción, consideraría mejor tener un tipo ...
supercat
... que claramente no tiene semántica más allá de "cada campo contiene lo último escrito en él", empujando toda la semántica en el código que usa la estructura, que intentar que una estructura haga más. Dado, por ejemplo, un Range<T>tipo con miembros Minimumy Maximumcampos de tipo Ty código Range<double> myRange = foo.getRange();, cualquier garantía sobre qué Minimumy qué Maximumcontenido debería provenir foo.GetRange();. Tener Rangeuna estructura de campo expuesto dejaría en claro que no va a agregar ningún comportamiento propio.
supercat