Pregunta

Siguiendo las discusiones aquí sobre SO, ya leí varias veces la observación de que las estructuras mutables son & # 8220; evil & # 8221; (como en la respuesta a esta pregunta ).

¿Cuál es el problema real con la mutabilidad y las estructuras en C #?

¿Fue útil?

Solución

Las estructuras son tipos de valores, lo que significa que se copian cuando se pasan.

Entonces, si cambia una copia, solo está cambiando esa copia, no el original y ninguna otra copia que pueda estar alrededor.

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)

Otros consejos

Dónde comenzar ;-p

El blog de Eric Lippert es siempre es bueno para una cita:

  

Esta es otra razón por la cual mutable   Los tipos de valor son malos. Intenta siempre   hacer que los tipos de valor sean inmutables.

Primero, tiende a perder los cambios con bastante facilidad ... por ejemplo, sacando cosas de una lista:

Foo foo = list[0];
foo.Name = "abc";

¿Qué cambió eso? Nada útil ...

Lo mismo con las propiedades:

myObj.SomeProperty.Size = 22; // the compiler spots this one

forzá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 cadena , un DateTime y un bool , puede muy rápidamente quemar mucha memoria. Con una clase, varias personas que llaman pueden compartir una referencia a la misma instancia (las referencias son pequeñas).

No diría evil pero la mutabilidad es a menudo un signo 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 necesario , el programador simplemente piensa que podría estar 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.

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 el uso de una estructura inmutable en estos casos de uso perfectamente válidos "evil".

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 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 valores, que se copian de forma predeterminada. necesita una referencia si no desea copiarlos " o " intente trabajar con estructuras de solo lectura primero " .

Las estructuras con campos o propiedades mutables públicas no son malas.

Métodos de estructura (a diferencia de los establecedores de propiedades) que mutan " this " son algo malvados, solo porque .net no proporciona un medio para distinguirlos de métodos que no lo hacen. Estructurar métodos que no mutan " esto " debe ser invocable incluso en estructuras de solo lectura sin necesidad de copia defensiva. Métodos que mutan " esto " no debe ser invocable en absoluto en estructuras de solo lectura. Dado que .net no quiere prohibir métodos de estructura que no modifiquen " esto " de ser invocado en estructuras de solo lectura, pero no quiere permitir que se muten estructuras de solo lectura, copia defensivamente estructuras en contextos de solo lectura, posiblemente obteniendo lo peor de ambos mundos.

A pesar de los problemas con el manejo de métodos de auto mutación en contextos de solo lectura, sin embargo, 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;};
class PointyClass {public int x,y,z;};

void Method1(PointyStruct foo);
void Method2(ref PointyStruct foo);
void Method3(PointyClass foo);

Para cada método, responda las siguientes preguntas:

  1. Suponiendo que el método no utiliza ningún "inseguro" código, ¿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:
 & # 8195; Método1 () : no (intención clara)
 & # 8195; Método2 () : sí (intención clara)
 & # 8195; Método3 () : sí (intención incierta)
 Pregunta 2:
 & # 8195; Método1 () : sin
 & # 8195; Method2 () : no (a menos que no sea seguro)
 & # 8195; Método3 () : sí

Método1 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 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 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 regresar, o puede dar una referencia a otro hilo que podría mutarlo de alguna manera arbitraria en algún momento futuro arbitrario. La única forma de limitar lo que el Método 3 podría hacerle a un objeto de clase mutable que se le pasara sería encapsular el objeto mutable en un contenedor de solo lectura, que es feo y engorroso.

Las matrices de estructuras ofrecen una semántica maravillosa. Dado RectArray [500] de tipo Rectángulo, es claro y obvio cómo, p. copie el elemento 123 al elemento 456 y luego, un tiempo después, ajuste el ancho del elemento 123 a 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 es una clase con los mismos campos que Rectangle y se desea 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 asume que la matriz contiene instancias que no van a cambiar, por lo que el código apropiado sería más como "RectClassArray [321] = RectClassArray [456]; ... RectClassArray [321] = Nueva 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 (e.

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.

Hay otros casos de esquina que podrían conducir a un comportamiento impredecible desde el punto de vista de los programadores. Aquí un par de ellos.

  1. 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);
    }
}

  1. Tipos de valores mutables 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 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);

Entonces, las estructuras mutables no son malas siempre y cuando tú y el resto del equipo entiendan claramente lo que estás haciendo. Pero hay demasiados casos en los que el comportamiento del programa sería diferente del esperado, lo que podría conducir a errores sutiles difíciles de producir y difíciles de entender.

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 a los estúpidos 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 convertido en malas. 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.

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 hilo de código nativo pueda acceder simultáneamente a la matriz (tal vez 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, admitamos 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

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 muchas personas profundamente acostumbradas 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 del namedtuple de Python), 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

en comparación 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 adecuado y, por lo tanto, no tienen identidad), y los datos pueden modificarse. En otros idiomas, record (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 desde archivos, modificados y descargados en archivos (ese es el uso principal 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, tenía que implementarlo 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 adecuada 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.

Cuando algo puede ser mutado, 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));

Debido a que Persona es mutable, es más natural pensar en cambiar la posición de Eric que clonar 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 a Persona debe recordar pedir una referencia a Persona o estarán haciendo lo incorrecto.

Si hace que el tipo sea inmutable, el problema desaparece; si no puedo modificar eric , no me importa si recibo eric o un clon de eric . En términos más generales, un tipo es seguro para 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.

La intuición de una Persona inmutable depende de lo que intente hacer. Si Person solo representa un conjunto de datos sobre una persona, no tiene nada de intuitivo; Las variables Persona representan verdaderamente valores abstractos, no objetos. (En ese caso, probablemente sería más apropiado cambiarle el nombre a PersonData ). Si Person realmente está modelando a una persona, la idea de crear y mover clones constantemente es una tontería incluso si has evitado la trampa de pensar que estás modificando el original. En ese caso, probablemente sería más natural simplemente hacer de Persona 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 tiene beneficios (nadie puede guardar en secreto una referencia a 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.

No tiene nada que ver con estructuras (y tampoco con C #), pero en Java puede tener problemas con objetos mutables cuando son, p. llaves 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.

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 el uso válido de estructuras que se utilizan para organizar variables públicas, si quisiera control de acceso usaría una clase.

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 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 Pulse cualquier tecla para continuar . . .

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 ofrece algunos ejemplos.

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 simulacros 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 realice ajustes, ya que esa es la naturaleza de su prueba (es decir, duplicación, luego modificación). No importa lo que suceda con la primera instancia de Eric si solo usamos 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.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top