c# .net immutability value-type

c# - ¿Los tipos de valor son inmutables por definición?



.net immutability (12)

Con frecuencia leo que las struct deben ser inmutables, ¿no son por definición?

¿Consideras que int es inmutable?

int i = 0; i = i + 123;

Parece que está bien, obtenemos una nueva int y la asignamos a i . ¿Qué hay de esto?

i++;

Está bien, podemos pensarlo como un atajo.

i = i + 1;

¿Qué hay del struct Point ?

Point p = new Point(1, 2); p.Offset(3, 4);

¿Esto realmente muta el punto (1, 2) ? ¿No deberíamos pensar en ello como un atajo para lo siguiente con Point.Offset() devolviendo un nuevo punto?

p = p.Offset(3, 4);

El trasfondo de este pensamiento es este: ¿cómo puede un tipo de valor sin identidad ser mutable? Tienes que mirarlo al menos dos veces para determinar si cambió. Pero, ¿cómo puedes hacer esto sin una identidad?

No quiero complicar el razonamiento sobre esto considerando los parámetros de ref y el boxeo. También soy consciente de que p = p.Offset(3, 4); expresa la inmutabilidad mucho mejor que p.Offset(3, 4); hace. Pero la pregunta sigue siendo: ¿no son los tipos de valores inmutables por definición?

ACTUALIZAR

Creo que hay al menos dos conceptos involucrados: la mutabilidad de una variable o campo y la mutabilidad del valor de una variable.

public class Foo { private Point point; private readonly Point readOnlyPoint; public Foo() { this.point = new Point(1, 2); this.readOnlyPoint = new Point(1, 2); } public void Bar() { this.point = new Point(1, 2); this.readOnlyPoint = new Point(1, 2); // Does not compile. this.point.Offset(3, 4); // Is now (4, 6). this.readOnlyPoint.Offset(3, 4); // Is still (1, 2). } }

En el ejemplo, tenemos campos a, uno mutable e inmutable. Como un campo de tipo de valor contiene el valor completo, un tipo de valor almacenado en un campo inmutable también debe ser inmutable. Todavía estoy bastante sorprendido por el resultado: no esperaba que el campo de solo lectura permanezca sin modificaciones.

Las variables (además de las constantes) son siempre mutables, por lo tanto, no implican ninguna restricción en la mutabilidad de los tipos de valores.

La respuesta parece no ser tan directa así que volveré a formular la pregunta.

Dado lo siguiente.

public struct Foo { public void DoStuff(whatEverArgumentsYouLike) { // Do what ever you like to do. } // Put in everything you like - fields, constants, methods, properties ... }

¿Puede dar una versión completa de Foo y un ejemplo de uso, que puede incluir parámetros de ref y boxeo, para que no sea posible reescribir todas las ocurrencias de

foo.DoStuff(whatEverArgumentsYouLike);

con

foo = foo.DoStuff(whatEverArgumentsYouLike);


¿no son los tipos de valores inmutables por definición?

No, no lo son: si System.Drawing.Point estructura System.Drawing.Point por ejemplo, tiene un setter y un getter en su propiedad X

Sin embargo, puede ser cierto decir que todos los tipos de valores se deben definir con API inmutables.


No quiero complicar el razonamiento sobre esto considerando los parámetros de ref y el boxeo. También soy consciente de que p = p.Offset(3, 4); expresa la inmutabilidad mucho mejor que p.Offset(3, 4); hace. Pero la pregunta sigue siendo: ¿no son los tipos de valores inmutables por definición?

Bueno, entonces realmente no estás operando en el mundo real, ¿verdad? En la práctica, la propensión de los tipos de valores a hacer copias de sí mismos mientras se mueven entre funciones se combina bien con la inmutabilidad, pero no son inmutables a menos que los conviertas en inmutables, ya que, como dijiste, puedes usar referencias a ellos solo como cualquier otra cosa


Un objeto es inmutable si su estado no cambia una vez que el objeto ha sido creado.

Respuesta corta: No, los tipos de valores no son inmutables por definición. Tanto las estructuras como las clases pueden ser mutables o inmutables. Las cuatro combinaciones son posibles. Si una estructura o clase tiene campos públicos que no son de solo lectura, propiedades públicas con definidores o métodos que establecen campos privados, es mutable porque puede cambiar su estado sin crear una nueva instancia de ese tipo.

Respuesta larga: En primer lugar, la cuestión de la inmutabilidad solo se aplica a las estructuras o clases con campos o propiedades. Los tipos más básicos (números, cadenas y nulo) son intrínsecamente inmutables porque no hay nada (campo / propiedad) que cambiar sobre ellos. A 5 es un 5 es un 5. Cualquier operación en el 5 solo devuelve otro valor inmutable.

Puede crear estructuras mutables como System.Drawing.Point . Tanto X como Y tienen setters que modifican los campos de la estructura:

Point p = new Point(0, 0); p.X = 5; // we modify the struct through property setter X // still the same Point instance, but its state has changed // it''s property X is now 5

Algunas personas parecen confundir la inmutabilidad con el hecho de que los tipos de valores se pasan por valor (de ahí su nombre) y no por referencia.

void Main() { Point p1 = new Point(0, 0); SetX(p1, 5); Console.WriteLine(p1.ToString()); } void SetX(Point p2, int value) { p2.X = value; }

En este caso Console.WriteLine() escribe " {X=0,Y=0} ". Aquí p1 no se modificó porque SetX() modificó p2 que es una copia de p1 . Esto sucede porque p1 es un tipo de valor , no porque sea inmutable (no lo es).

¿Por qué los tipos de valores deben ser inmutables? Muchas razones ... Ver esta pregunta . Principalmente se debe a que los tipos de valores mutables conducen a todo tipo de errores no tan obvios. En el ejemplo anterior, el programador podría haber esperado que p1 sea (5, 0) después de llamar a SetX() . O imagine ordenar por un valor que luego puede cambiar. Entonces su colección ordenada ya no será ordenada como se esperaba. Lo mismo aplica para diccionarios y hashes. El fabuloso Eric Lippert ( blog ) ha escrito una serie completa sobre inmutabilidad y por qué cree que es el futuro de C #. Aquí hay uno de sus ejemplos que le permite "modificar" una variable de solo lectura.

ACTUALIZACIÓN: su ejemplo con:

this.readOnlyPoint.Offset(3, 4); // Is still (1, 2).

es exactamente a lo que Lippert se refirió en su publicación sobre la modificación de variables de solo lectura. Offset(3,4) realmente modificó un Point , pero era una copia de readOnlyPoint , y nunca se asignó a nada, por lo que se pierde.

Y es por eso que los tipos de valores mutables son malvados: te permiten pensar que estás modificando algo, cuando a veces estás modificando una copia, lo que lleva a errores inesperados. Si Point era inmutable, Offset() tendría que devolver un nuevo Point , y no podría haberlo asignado a readOnlyPoint . Y luego dices "Ah, claro, es de solo lectura por una razón. ¿Por qué estaba tratando de cambiarlo? Lo bueno es que el compilador me detuvo ahora".

ACTUALIZACIÓN: Acerca de su solicitud reformulada ... Creo que sé a qué se dirige. En cierto modo, puede "pensar" que las estructuras son internamente inmutables, que modificar una estructura es lo mismo que reemplazarla con una copia modificada. Incluso podría ser lo que el CLR hace internamente en la memoria, por lo que sé. (Así es como funciona la memoria flash. No puede editar solo unos pocos bytes, necesita leer un bloque entero de kilobytes en la memoria, modificar los pocos que desee y volver a escribir todo el bloque). Sin embargo, incluso si fueran "internamente inmutables" ", que es un detalle de implementación y para nosotros los desarrolladores como usuarios de estructuras (su interfaz o API, si lo desea), se pueden cambiar. No podemos ignorar ese hecho y "pensar en ellos como inmutables".

En un comentario dijiste "no puedes tener una referencia al valor de campo o variable". Está asumiendo que cada variable de estructura tiene una copia diferente, de modo que modificar una copia no afecta a las demás. Eso no es del todo cierto. Las líneas marcadas a continuación no son reemplazables si ...

interface IFoo { DoStuff(); } struct Foo : IFoo { /* ... */ } IFoo otherFoo = new Foo(); IFoo foo = otherFoo; foo.DoStuff(whatEverArgumentsYouLike); // line #1 foo = foo.DoStuff(whatEverArgumentsYouLike); // line #2

Las líneas # 1 y # 2 no tienen los mismos resultados ... ¿Por qué? Porque foo y otherFoo refieren a la misma instancia en caja de Foo. Lo que sea que cambie en foo en la línea n. ° 1 se refleja en otherFoo . La línea # 2 reemplaza foo con un nuevo valor y no le hace nada a otherFoo (suponiendo que DoStuff() devuelve una nueva instancia IFoo y no modifica foo ).

Foo foo1 = new Foo(); // creates first instance Foo foo2 = foo1; // create a copy (2nd instance) IFoo foo3 = foo2; // no copy here! foo2 and foo3 refer to same instance

La modificación de foo1 no afectará a foo2 o foo3 . La modificación de foo2 se reflejará en foo3 , pero no en foo1 . La modificación de foo3 se reflejará en foo2 pero no en foo1 .

¿Confuso? Se adhiere a los tipos de valores inmutables y elimina la necesidad de modificar cualquiera de ellos.

ACTUALIZACIÓN: error de escritura fijo en la primera muestra de código


Creo que la confusión es que si tienes un tipo de referencia que debería actuar como un tipo de valor, es una buena idea hacerlo inmutable. Una de las principales diferencias entre los tipos de valores y los tipos de referencia es que un cambio realizado a través de un nombre en un tipo de referencia puede aparecer en el otro nombre. Esto no sucede con los tipos de valor:

public class foo { public int x; } public struct bar { public int x; } public class MyClass { public static void Main() { foo a = new foo(); bar b = new bar(); a.x = 1; b.x = 1; foo a2 = a; bar b2 = b; a.x = 2; b.x = 2; Console.WriteLine( "a2.x == {0}", a2.x); Console.WriteLine( "b2.x == {0}", b2.x); } }

Produce:

a2.x == 2 b2.x == 1

Ahora, si tiene un tipo que le gustaría tener una semántica de valor, pero no quiere realmente convertirlo en un tipo de valor, tal vez porque el almacenamiento que requiere es demasiado o lo que sea, debe considerar que la inmutabilidad es parte de el diseño. Con un tipo de referencia inmutable, cualquier cambio realizado en una referencia existente produce un nuevo objeto en lugar de cambiar el existente, de modo que obtienes el comportamiento del tipo de valor de que cualquier valor que tengas no se puede cambiar por otro nombre.

Por supuesto, la clase System.String es un excelente ejemplo de dicho comportamiento.


El año pasado escribí una publicación en un blog sobre los problemas que pueden surgir al no hacer que las estructuras sean inmutables.

La publicación completa se puede leer aquí

Este es un ejemplo de cómo las cosas pueden salir terriblemente mal:

//Struct declaration: struct MyStruct { public int Value = 0; public void Update(int i) { Value = i; } }

Muestra de código:

MyStruct[] list = new MyStruct[5]; for (int i=0;i<5;i++) Console.Write(list[i].Value + " "); Console.WriteLine(); for (int i=0;i<5;i++) list[i].Update(i+1); for (int i=0;i<5;i++) Console.Write(list[i].Value + " "); Console.WriteLine();

El resultado de este código es:

0 0 0 0 0 1 2 3 4 5

Ahora hagamos lo mismo, pero sustituyamos la matriz por una List<> genérica List<> :

List<MyStruct> list = new List<MyStruct>(new MyStruct[5]); for (int i=0;i<5;i++) Console.Write(list[i].Value + " "); Console.WriteLine(); for (int i=0;i<5;i++) list[i].Update(i+1); for (int i=0;i<5;i++) Console.Write(list[i].Value + " "); Console.WriteLine();

El resultado es:

0 0 0 0 0 0 0 0 0 0

La explicación es muy simple. No, no es un boxeo / unboxing ...

Al acceder a los elementos de una matriz, el tiempo de ejecución obtendrá los elementos de la matriz directamente, por lo que el método Update () funciona en el elemento de la matriz en sí. Esto significa que las estructuras en el conjunto se actualizan.

En el segundo ejemplo, usamos una List<> genérica List<> . ¿Qué sucede cuando accedemos a un elemento específico? Bueno, se llama a la propiedad del indexador, que es un método. Los tipos de valores siempre se copian cuando los devuelve un método, por lo que esto es exactamente lo que sucede: el método del indizador de la lista recupera la estructura de una matriz interna y la devuelve a la persona que llama. Debido a que se trata de un tipo de valor, se realizará una copia y se llamará al método Update () en la copia, que por supuesto no tiene ningún efecto en los elementos originales de la lista.

En otras palabras, siempre asegúrese de que sus estructuras sean inmutables, porque nunca está seguro de cuándo se realizará una copia. La mayoría de las veces es obvio, pero en algunos casos realmente puede sorprenderte ...


La mutabilidad y los tipos de valores son dos cosas separadas.

La definición de un tipo como tipo de valor indica que el tiempo de ejecución copiará los valores en lugar de una referencia al tiempo de ejecución. La mutación, por otro lado, depende de la implementación, y cada clase puede implementarla como lo desee.


No, ellos no son. Ejemplo:

Point p = new Point (3,4); Point p2 = p; p.moveTo (5,7);

En este ejemplo moveTo() es una operación in situ . Cambia la estructura que se esconde detrás de la referencia p . Puedes ver eso mirando p2 : su posición también habrá cambiado. Con estructuras inmutables, moveTo() debería devolver una nueva estructura:

p = p.moveTo (5,7);

Ahora, Point es inmutable y cuando crea una referencia en cualquier parte de su código, no obtendrá sorpresas. Veamos i :

int i = 5; int j = i; i = 1;

Esto es diferente. No soy inmutable, 5 es. Y la segunda asignación no copia una referencia a la estructura que contiene i pero copia el contenido de i . De modo que detrás de escena ocurre algo completamente diferente: se obtiene una copia completa de la variable en lugar de solo una copia de la dirección en la memoria (la referencia).

Un equivalente con objetos sería el constructor de copias:

Point p = new Point (3,4); Point p2 = new Point (p);

Aquí, la estructura interna de p se copia en un nuevo objeto / estructura y p2 contendrá la referencia a ella. Pero esta es una operación bastante costosa (a diferencia de la asignación de enteros anterior) que es la razón por la cual la mayoría de los lenguajes de programación hacen la distinción.

A medida que las computadoras se vuelven más poderosas y obtienen más memoria, esta distinción desaparecerá porque causa una enorme cantidad de errores y problemas. En la próxima generación, solo habrá objetos inmutables, cualquier operación estará protegida por una transacción e incluso un int será un objeto completo. Al igual que la recolección de basura, será un gran paso adelante en la estabilidad del programa, causará mucho dolor en los primeros años, pero permitirá escribir software confiable. Hoy en día, las computadoras simplemente no son lo suficientemente rápidas para esto.


No, los tipos de valores no son inmutables por definición.

Primero, debería haber hecho la pregunta "¿Los tipos de valores se comportan como tipos inmutables?" en lugar de preguntar si son inmutables, supongo que esto causó mucha confusión.

struct MutableStruct { private int state; public MutableStruct(int state) { this.state = state; } public void ChangeState() { this.state++; } } struct ImmutableStruct { private readonly int state; public MutableStruct(int state) { this.state = state; } public ImmutableStruct ChangeState() { return new ImmutableStruct(this.state + 1); } }

[Continuará...]


Para definir si un tipo es mutable o inmutable, uno debe definir a qué se refiere ese "tipo". Cuando se declara una ubicación de almacenamiento de tipo de referencia, la declaración simplemente asigna espacio para mantener una referencia a un objeto almacenado en otro lugar; la declaración no crea el objeto real en cuestión.No obstante, en la mayoría de los contextos en los que se habla de tipos de referencia particulares, uno no estará hablando de una ubicación de almacenamiento que contenga una referencia , sino más bien el objeto identificado por esa referencia . El hecho de que uno pueda escribir en un lugar de almacenamiento con una referencia a un objeto implica que el objeto en sí no puede ser modificado.

Por el contrario, cuando se declara una ubicación de almacenamiento de tipo de valor, el sistema asignará dentro de esa ubicación de almacenamiento ubicaciones de almacenamiento anidadas para cada campo público o privado mantenido por ese tipo de valor. Todo sobre el tipo de valor se mantiene en esa ubicación de almacenamiento. Si uno define una variable foode tipo Pointy sus dos campos, Xy Y, mantenga 3 y 6 respectivamente. Si uno define la "instancia" de Pointin foocomo el par de campos , esa instancia será mutable si y solo si fooes mutable. Si se define una instancia de Pointcomo los valores contenidos en esos campos (por ejemplo, "3,6"), dicha instancia es, por definición, inmutable, ya que cambiar uno de esos campos provocaríaPoint para tener una instancia diferente.

Creo que es más útil pensar en un tipo de valor "instancia" como los campos, en lugar de los valores que contienen. Según esa definición, cualquier tipo de valor almacenado en una ubicación de almacenamiento mutable, y para el que exista cualquier valor no predeterminado, siempre será mutable, independientemente de cómo se declare. Una instrucción MyPoint = new Point(5,8)construye una nueva instancia de Point, con fields X=5y Y=8, y luego muta MyPointreemplazando los valores en sus campos con los de los recién creados Point. Incluso si una estructura no proporciona ninguna forma de modificar ninguno de sus campos fuera de su constructor, no hay forma de que un tipo de estructura pueda proteger una instancia para que no se sobrescriban todos sus campos con el contenido de otra instancia.

Incidentalmente, un ejemplo simple donde una estructura mutable puede lograr una semántica que no se puede lograr por otros medios: Asumiendo que myPoints[]es una matriz de un solo elemento que es accesible para múltiples hilos, tenga veinte hilos simultáneamente ejecute el código:

Threading.Interlocked.Increment(myPoints[0].X);

Si myPoints[0].Xcomienza igual a cero y veinte hilos realizan el código anterior, ya sea simultáneamente o no, myPoints[0].Xserá igual a veinte. Si uno intentara imitar el código anterior con:

myPoints[0] = new Point(myPoints[0].X + 1, myPoints[0].Y);

luego, si cualquier hilo se leía myPoints[0].Xentre el momento en que otro hilo lo leyó y escribió el valor revisado, los resultados del incremento se perderían (con la consecuencia de que myPoints[0].Xpodría terminar arbitrariamente con cualquier valor entre 1 y 20).


Puede escribir estructuras que son mutables, pero es una buena práctica hacer que los tipos de valor sean inmutables.

Por ejemplo, DateTime siempre crea nuevas instancias al realizar cualquier operación. El punto es mutable y se puede cambiar.

Para responder a su pregunta: No, no son inmutables por definición, depende del caso si deben ser mutables o no. Por ejemplo, si deberían servir como claves del diccionario, deberían ser inmutables.


Si lleva su lógica lo suficientemente lejos, entonces todos los tipos son inmutables. Cuando modifica un tipo de referencia, puede argumentar que realmente está escribiendo un nuevo objeto en la misma dirección, en lugar de modificar algo.

O podría argumentar que todo es mutable, en cualquier idioma, porque de vez en cuando la memoria que previamente se había utilizado para una cosa, será sobrescrita por otra.

Con suficientes abstracciones e ignorando suficientes características de idioma, puede llegar a cualquier conclusión que desee.

Y eso pasa por alto el punto. Según la especificación .NET, los tipos de valores son mutables. Puedes modificarlo

int i = 0; Console.WriteLine(i); // will print 0, so here, i is 0 ++i; Console.WriteLine(i); // will print 1, so here, i is 1

pero sigue siendo el mismo yo. La variable i solo se declara una vez. Todo lo que le sucede después de esta declaración es una modificación.

En algo así como un lenguaje funcional con variables inmutables, esto no sería legal. El ++ i no sería posible. Una vez que se ha declarado una variable, tiene un valor fijo.

En .NET, ese no es el caso, no hay nada que me impida modificar la i después de que ha sido declarada.

Después de pensarlo un poco más, aquí hay otro ejemplo que podría ser mejor:

struct S { public S(int i) { this.i = i == 43 ? 0 : i; } private int i; public void set(int i) { Console.WriteLine("Hello World"); this.i = i; } } void Foo { var s = new S(42); // Create an instance of S, internally storing the value 42 s.set(43); // What happens here? }

En la última línea, de acuerdo con su lógica, podríamos decir que realmente construimos un nuevo objeto y sobrescribimos el anterior con ese valor. ¡Pero eso no es posible! Para construir un nuevo objeto, el compilador tiene que establecer la variable i en 42. ¡Pero es privado! Solo se puede acceder a través de un constructor definido por el usuario, que explícitamente no permite el valor 43 (configurándolo a 0 en su lugar), y luego a través de nuestro método de set , que tiene un desagradable efecto secundario. El compilador no tiene forma de crear un nuevo objeto con los valores que le gustan. La única forma en que se puede establecer en 43 es modificando el objeto actual llamando a set() . El compilador no puede hacer eso, porque cambiaría el comportamiento del programa (se imprimiría en la consola)

Entonces, para que todas las estructuras sean inmutables, el compilador tendría que hacer trampa y romper las reglas del lenguaje. Y, por supuesto, si estamos dispuestos a romper las reglas, podemos probar cualquier cosa. Podría probar que todos los enteros son iguales también, o que definir una nueva clase hará que tu computadora se incendie. Mientras nos mantengamos dentro de las reglas del lenguaje, las estructuras son mutables.


Los objetos / estructuras son inmutables cuando se pasan a una función de tal manera que los datos no se pueden cambiar, y la estructura devuelta es una newestructura. El ejemplo clásico es

String s = "abc";

s.toLower();

si la toLowerfunción está escrita para que se devuelva una nueva cadena que reemplace "s", es inmutable, pero si la función va letra por letra reemplazando la letra dentro de "s" y nunca declarando una "nueva cadena", es mutable.