Objeto inmutable - Immutable object
En la programación funcional y orientada a objetos , un objeto inmutable ( objeto inmutable) es un objeto cuyo estado no se puede modificar después de su creación. Esto contrasta con un objeto mutable ( objeto modificable), que puede modificarse después de su creación. En algunos casos, un objeto se considera inmutable incluso si cambian algunos atributos utilizados internamente, pero el estado del objeto parece inalterable desde un punto de vista externo. Por ejemplo, un objeto que utiliza la memorización para almacenar en caché los resultados de cálculos costosos aún podría considerarse un objeto inmutable.
Las cadenas y otros objetos concretos se expresan normalmente como objetos inmutables para mejorar la legibilidad y la eficiencia del tiempo de ejecución en la programación orientada a objetos . Los objetos inmutables también son útiles porque son inherentemente seguros para subprocesos . Otros beneficios son que son más fáciles de entender y razonar y ofrecen mayor seguridad que los objetos mutables.
Conceptos
Variables inmutables
En la programación imperativa , los valores mantenidos en las variables del programa cuyo contenido nunca cambia se conocen como constantes para diferenciarlos de las variables que podrían alterarse durante la ejecución. Los ejemplos incluyen factores de conversión de metros a pies, o el valor de pi a varios lugares decimales.
Los campos de solo lectura se pueden calcular cuando se ejecuta el programa (a diferencia de las constantes, que se conocen de antemano), pero nunca cambian después de que se inicializan.
Inmutabilidad débil vs fuerte
A veces, se habla de que ciertos campos de un objeto son inmutables. Esto significa que no hay forma de cambiar esas partes del estado del objeto, aunque otras partes del objeto pueden ser cambiantes ( débilmente inmutables ). Si todos los campos son inmutables, entonces el objeto es inmutable. Si el objeto completo no puede ser extendido por otra clase, el objeto se llama fuertemente inmutable . Esto podría, por ejemplo, ayudar a hacer cumplir explícitamente ciertas invariantes sobre ciertos datos en el objeto que permanecen iguales durante la vida útil del objeto. En algunos lenguajes, esto se hace con una palabra clave (por ejemplo, const
en C ++ , final
en Java ) que designa el campo como inmutable. Algunos lenguajes lo invierten: en OCaml , los campos de un objeto o registro son por defecto inmutables, y deben marcarse explícitamente con mutable
para serlo.
Referencias a objetos
En la mayoría de los lenguajes orientados a objetos, se puede hacer referencia a los objetos mediante referencias . Algunos ejemplos de estos lenguajes son Java , C ++ , C # , VB.NET y muchos lenguajes de secuencias de comandos , como Perl , Python y Ruby . En este caso, importa si el estado de un objeto puede variar cuando los objetos se comparten mediante referencias.
Referenciar vs copiar objetos
Si se sabe que un objeto es inmutable, es preferible crear una referencia del mismo en lugar de copiar todo el objeto. Esto se hace para conservar memoria al prevenir la duplicación de datos y evitar llamadas a constructores y destructores; también da como resultado un aumento potencial en la velocidad de ejecución.
La técnica de copia de referencia es mucho más difícil de usar para objetos mutables, porque si cualquier usuario de una referencia de objeto mutable la cambia, todos los demás usuarios de esa referencia verán el cambio. Si este no es el efecto deseado, puede ser difícil notificar a los otros usuarios para que respondan correctamente. En estas situaciones, la copia defensiva de todo el objeto en lugar de la referencia suele ser una solución fácil pero costosa. El patrón de observador es una técnica alternativa para manejar cambios en objetos mutables.
Copiar en escrito
Una técnica que combina las ventajas de los objetos mutables e inmutables , y que se admite directamente en casi todo el hardware moderno, es la copia en escritura (COW). Con esta técnica, cuando un usuario le pide al sistema que copie un objeto, simplemente crea una nueva referencia que todavía apunta al mismo objeto. Tan pronto como un usuario intenta modificar el objeto a través de una referencia particular, el sistema hace una copia real, aplica la modificación y establece la referencia para hacer referencia a la nueva copia. Los otros usuarios no se ven afectados, porque todavía se refieren al objeto original. Por lo tanto, en COW, todos los usuarios parecen tener una versión mutable de sus objetos, aunque en el caso de que los usuarios no modifiquen sus objetos, se conservan las ventajas de ahorro de espacio y velocidad de los objetos inmutables. La copia en escritura es popular en los sistemas de memoria virtual porque les permite ahorrar espacio en la memoria sin dejar de manejar correctamente cualquier cosa que pueda hacer un programa de aplicación.
Pasantía
La práctica de usar siempre referencias en lugar de copias de objetos iguales se conoce como pasantía . Si se usa internación, dos objetos se consideran iguales si y solo si sus referencias, típicamente representadas como punteros o enteros, son iguales. Algunos lenguajes hacen esto automáticamente: por ejemplo, Python automáticamente pasa cadenas cortas . Si se garantiza que el algoritmo que implementa la internación lo hará en todos los casos en que sea posible, entonces comparar objetos para determinar la igualdad se reduce a comparar sus punteros, una ganancia sustancial de velocidad en la mayoría de las aplicaciones. (Incluso si no se garantiza que el algoritmo sea completo, todavía existe la posibilidad de una mejora del caso de ruta rápida cuando los objetos son iguales y usan la misma referencia). El internamiento generalmente solo es útil para objetos inmutables.
Seguridad del hilo
Los objetos inmutables pueden ser útiles en aplicaciones multiproceso. Varios subprocesos pueden actuar sobre los datos representados por objetos inmutables sin preocuparse de que otros subprocesos cambien los datos. Por lo tanto, los objetos inmutables se consideran más seguros para subprocesos que los objetos mutables.
Violar la inmutabilidad
La inmutabilidad no implica que el objeto almacenado en la memoria de la computadora no se pueda escribir. Más bien, la inmutabilidad es una construcción en tiempo de compilación que indica lo que un programador puede hacer a través de la interfaz normal del objeto, no necesariamente lo que puede hacer absolutamente (por ejemplo, eludiendo el sistema de tipos o violando la corrección constante en C o C ++ ).
Detalles específicos del idioma
En Python , Java y .NET Framework , las cadenas son objetos inmutables. Tanto Java como .NET Framework tienen versiones mutables de cadena. En Java, estos son StringBuffer
y StringBuilder
(versiones mutables de Java String
) y en .NET esto es StringBuilder
(versión mutable de .Net String
). Python 3 tiene una variante de cadena mutable (bytes), llamada bytearray
.
Además, todas las clases contenedoras primitivas en Java son inmutables.
Patrones similares son la interfaz inmutable y el envoltorio inmutable .
En lenguajes de programación puramente funcionales no es posible crear objetos mutables sin extender el lenguaje (por ejemplo, a través de una biblioteca de referencias mutables o una interfaz de función ajena ), por lo que todos los objetos son inmutables.
Ada
En Ada , cualquier objeto se declara variable (es decir, mutable; típicamente el valor predeterminado implícito) o constant
(es decir, inmutable) mediante la constant
palabra clave.
type Some_type is new Integer; -- could be anything more complicated
x: constant Some_type:= 1; -- immutable
y: Some_type; -- mutable
Parámetros de subprograma son inmutables en el en el modo y en el mutable en salir y salir modos.
procedure Do_it(a: in Integer; b: in out Integer; c: out Integer) is
begin
-- a is immutable
b:= b + a;
c:= a;
end Do_it;
C#
En C # puede imponer la inmutabilidad de los campos de una clase con la readonly
declaración. Al hacer cumplir todos los campos como inmutables, obtiene un tipo inmutable.
class AnImmutableType
{
public readonly double _value;
public AnImmutableType(double x)
{
_value = x;
}
public AnImmutableType Square()
{
return new AnImmutableType(_value * _value);
}
}
C ++
En C ++, una implementación const-correcta de Cart
permitiría al usuario crear instancias de la clase y luego usarlas como const
(inmutables) o mutables, según se desee, proporcionando dos versiones diferentes del items()
método. (Tenga en cuenta que en C ++ no es necesario, y de hecho imposible, proporcionar un constructor especializado para las const
instancias).
class Cart {
public:
Cart(std::vector<Item> items): items_(items) {}
std::vector<Item>& items() { return items_; }
const std::vector<Item>& items() const { return items_; }
int ComputeTotalCost() const { /* return sum of the prices */ }
private:
std::vector<Item> items_;
};
Tenga en cuenta que, cuando hay un miembro de datos que es un puntero o una referencia a otro objeto, entonces es posible mutar el objeto apuntado o referenciado solo dentro de un método no constante.
C ++ también proporciona inmutabilidad abstracta (a diferencia de bit a bit) a través de la mutable
palabra clave, que permite cambiar una variable miembro desde dentro de un const
método.
class Cart {
public:
Cart(std::vector<Item> items): items_(items) {}
const std::vector<Item>& items() const { return items_; }
int ComputeTotalCost() const {
if (total_cost_) {
return *total_cost_;
}
int total_cost = 0;
for (const auto& item : items_) {
total_cost += item.Cost();
}
total_cost_ = total_cost;
return total_cost;
}
private:
std::vector<Item> items_;
mutable std::optional<int> total_cost_;
};
D
En D , existen dos calificadores de tipo , const
y immutable
, para las variables que no se pueden cambiar. A diferencia de C ++ const
, Java final
y C # readonly
, son transitivos y se aplican de forma recursiva a cualquier cosa accesible a través de referencias de dicha variable. La diferencia entre const
y immutable
es a lo que se aplican: const
es una propiedad de la variable: pueden existir legalmente referencias mutables al valor referido, es decir, el valor realmente puede cambiar. Por el contrario, immutable
es una propiedad del valor referido: el valor y cualquier cosa que sea accesible transitivamente desde él no puede cambiar (sin romper el sistema de tipos, lo que lleva a un comportamiento indefinido ). Cualquier referencia de ese valor debe estar marcada const
o immutable
. Básicamente, para cualquier tipo no calificado T
, const(T)
es la unión disjunta de T
(mutable) y immutable(T)
.
class C {
/*mutable*/ Object mField;
const Object cField;
immutable Object iField;
}
Para un C
objeto mutable , mField
se puede escribir en. Para un const(C)
objeto, mField
no se puede modificar, hereda const
; iField
sigue siendo inmutable, ya que es la garantía más fuerte. Para an immutable(C)
, todos los campos son inmutables.
En una función como esta:
void func(C m, const C c, immutable C i)
{ /* inside the braces */ }
Dentro de las llaves, c
podría referirse al mismo objeto que m
, por lo que las mutaciones en también m
podrían cambiar indirectamente c
. Además,
c
puede hacer referencia al mismo objeto que i
, pero dado que el valor es inmutable, no hay cambios. Sin embargo, m
y i
legalmente no puede referirse al mismo objeto.
En el lenguaje de las garantías, mutable no tiene garantías (la función puede cambiar el objeto), const
es una garantía solo externa de que la función no cambiará nada y
immutable
es una garantía bidireccional (la función no cambiará el valor y el llamador debe no lo cambie).
Valores que son const
o immutable
deben ser inicializados por asignación directa en el punto de declaración o por un constructor .
Debido a que los const
parámetros olvidan si el valor era mutable o no, una construcción similar inout
actúa, en cierto sentido, como una variable para la información de mutabilidad. Una función de tipo const(S) function(const(T))
devuelve const(S)
valores escritos para argumentos mutables, constantes e inmutables. Por el contrario, una función del tipo inout(S) function(inout(T))
devuelve S
para mutables T
argumentos, const(S)
para const(T)
valores, y immutable(S)
para immutable(T)
valores.
La conversión de valores inmutables a mutable inflige un comportamiento indefinido ante el cambio, incluso si el valor original proviene de un origen mutable. La conversión de valores mutables a inmutables puede ser legal cuando no quedan referencias mutables después. "Una expresión puede convertirse de mutable (...) a inmutable si la expresión es única y todas las expresiones a las que se refiere transitivamente son únicas o inmutables". Si el compilador no puede probar la unicidad, la conversión se puede hacer explícitamente y depende del programador asegurarse de que no existan referencias mutables.
El tipo string
es un alias para immutable(char)[]
, es decir, una porción de memoria escrita de caracteres inmutables. Hacer subcadenas es barato, ya que solo copia y modifica un puntero y un archivo de longitud, y es seguro, ya que los datos subyacentes no se pueden cambiar. Los objetos de tipo const(char)[]
pueden referirse a cadenas, pero también a búferes mutables.
Hacer una copia superficial de un valor constante o inmutable elimina la capa externa de inmutabilidad: Copiar una cadena inmutable ( immutable(char[])
) devuelve una cadena ( immutable(char)[]
). El puntero y la longitud inmutables se están copiando y las copias son mutables. El dato referido no ha sido copiado y conserva su calificador, en el ejemplo immutable
. Puede eliminarse haciendo una copia depper, por ejemplo, utilizando la dup
función.
Java
Un ejemplo clásico de un objeto inmutable es una instancia de la String
clase
Java
String s = "ABC";
s.toLowerCase();
El método toLowerCase()
no modifica los datos "ABC" que s
contiene. En su lugar, se crea una instancia de un nuevo objeto String y se le dan los datos "abc" durante su construcción. El toLowerCase()
método devuelve una referencia a este objeto String . Para que la cadena s
contenga los datos "abc", se necesita un enfoque diferente:
s = s.toLowerCase();
Ahora String hace s
referencia a un nuevo objeto String que contiene "abc". No hay nada en la sintaxis de la declaración de la clase String que la imponga como inmutable; más bien, ninguno de los métodos de la clase String afecta los datos que contiene un objeto String, lo que lo hace inmutable.
La palabra clave final
( artículo detallado ) se usa para implementar tipos primitivos inmutables y referencias a objetos, pero no puede, por sí misma, hacer que los objetos en sí mismos sean inmutables. Vea los siguientes ejemplos:
Variables de tipo primitivo ( int
, long
, short
, etc.) pueden ser reasignados después de haber sido definido. Esto se puede prevenir usando final
.
int i = 42; //int is a primitive type
i = 43; // OK
final int j = 42;
j = 43; // does not compile. j is final so can't be reassigned
Los tipos de referencia no se pueden hacer inmutables simplemente usando la final
palabra clave. final
solo evita la reasignación.
final MyObject m = new MyObject(); //m is of reference type
m.data = 100; // OK. We can change state of object m (m is mutable and final doesn't change this fact)
m = new MyObject(); // does not compile. m is final so can't be reassigned
Envolturas primitivos ( Integer
, Long
, Short
, Double
, Float
, Character
, Byte
, Boolean
) también son inmutables. Las clases inmutables se pueden implementar siguiendo algunas pautas simples.
JavaScript
En JavaScript , todos los tipos primitivos (Indefinido, Nulo, Booleano, Número, BigInt, Cadena, Símbolo) son inmutables, pero los objetos personalizados son generalmente mutables.
function doSomething(x) { /* does changing x here change the original? */ };
var str = 'a string';
var obj = { an: 'object' };
doSomething(str); // strings, numbers and bool types are immutable, function gets a copy
doSomething(obj); // objects are passed in by reference and are mutable inside function
doAnotherThing(str, obj); // `str` has not changed, but `obj` may have.
Para simular la inmutabilidad en un objeto, se pueden definir propiedades como de solo lectura (se puede escribir: falso).
var obj = {};
Object.defineProperty(obj, 'foo', { value: 'bar', writable: false });
obj.foo = 'bar2'; // silently ignored
Sin embargo, el enfoque anterior aún permite agregar nuevas propiedades. Alternativamente, se puede usar Object.freeze para hacer que los objetos existentes sean inmutables.
var obj = { foo: 'bar' };
Object.freeze(obj);
obj.foo = 'bars'; // cannot edit property, silently ignored
obj.foo2 = 'bar2'; // cannot add property, silently ignored
Con la implementación de ECMA262 , JavaScript tiene la capacidad de crear referencias inmutables que no se pueden reasignar. Sin embargo, el uso de una const
declaración no significa que el valor de la referencia de solo lectura sea inmutable, solo que el nombre no se puede asignar a un nuevo valor.
const ALWAYS_IMMUTABLE = true;
try {
ALWAYS_IMMUTABLE = false;
} catch (err) {
console.log("Can't reassign an immutable reference.");
}
const arr = [1, 2, 3];
arr.push(4);
console.log(arr); // [1, 2, 3, 4]
El uso de estado inmutable se ha convertido en una tendencia creciente en JavaScript desde la introducción de React , que favorece los patrones de administración de estado similares a Flux como Redux .
Perl
En Perl , uno puede crear una clase inmutable con la biblioteca Moo simplemente declarando todos los atributos de solo lectura:
package Immutable;
use Moo;
has value => (
is => 'ro', # read only
default => 'data', # can be overridden by supplying the constructor with
# a value: Immutable->new(value => 'something else');
);
1;
La creación de una clase inmutable solía requerir dos pasos: primero, crear accesos (ya sea de forma automática o manual) que eviten la modificación de los atributos del objeto y, en segundo lugar, evitar la modificación directa de los datos de instancia de las instancias de esa clase (esto generalmente se almacenaba en un hash referencia, y podría bloquearse con la función lock_hash de Hash :: Util):
package Immutable;
use strict;
use warnings;
use base qw(Class::Accessor);
# create read-only accessors
__PACKAGE__->mk_ro_accessors(qw(value));
use Hash::Util 'lock_hash';
sub new {
my $class = shift;
return $class if ref($class);
die "Arguments to new must be key => value pairs\n"
unless (@_ % 2 == 0);
my %defaults = (
value => 'data',
);
my $obj = {
%defaults,
@_,
};
bless $obj, $class;
# prevent modification of the object data
lock_hash %$obj;
}
1;
O, con un descriptor de acceso escrito manualmente:
package Immutable;
use strict;
use warnings;
use Hash::Util 'lock_hash';
sub new {
my $class = shift;
return $class if ref($class);
die "Arguments to new must be key => value pairs\n"
unless (@_ % 2 == 0);
my %defaults = (
value => 'data',
);
my $obj = {
%defaults,
@_,
};
bless $obj, $class;
# prevent modification of the object data
lock_hash %$obj;
}
# read-only accessor
sub value {
my $self = shift;
if (my $new_value = shift) {
# trying to set a new value
die "This object cannot be modified\n";
} else {
return $self->{value}
}
}
1;
Pitón
En Python , algunos tipos integrados (números, valores booleanos, cadenas, tuplas, frozensets) son inmutables, pero las clases personalizadas son generalmente mutables. Para simular la inmutabilidad en una clase, se podría anular la configuración y eliminación de atributos para generar excepciones:
class ImmutablePoint:
"""An immutable class with two attributes 'x' and 'y'."""
__slots__ = ['x', 'y']
def __setattr__(self, *args):
raise TypeError("Can not modify immutable instance.")
__delattr__ = __setattr__
def __init__(self, x, y):
# We can no longer use self.value = value to store the instance data
# so we must explicitly call the superclass
super().__setattr__('x', x)
super().__setattr__('y', y)
Los ayudantes de biblioteca estándar collections.namedtuple
y typing.NamedTuple
, disponibles desde Python 3.6 en adelante, crean clases simples inmutables. El siguiente ejemplo es aproximadamente equivalente al anterior, más algunas características similares a tuplas:
from typing import NamedTuple
import collections
Point = collections.namedtuple('Point', ['x', 'y'])
# the following creates a similar namedtuple to the above
class Point(NamedTuple):
x: int
y: int
Introducido en Python 3.7, dataclasses
permite a los desarrolladores emular la inmutabilidad con instancias congeladas . Si una clase de datos congelada se construye, dataclasses
se anulará __setattr__()
y __delattr__()
elevar FrozenInstanceError
si se invoca.
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: int
y: int
Raqueta
Racket diverge sustancialmente de otras implementaciones de Scheme al hacer que su tipo de par de núcleos ("celdas de contras") sea inmutable. En lugar de ello, se proporciona un tipo de par mutable paralelo, a través de mcons
, mcar
, set-mcar!
etc. Además, muchos tipos inmutables son compatibles, por ejemplo, cadenas y vectores inmutables, y estos se utilizan ampliamente. Las nuevas estructuras son inmutables de forma predeterminada, a menos que un campo se declare específicamente mutable o toda la estructura:
(struct foo1 (x y)) ; all fields immutable
(struct foo2 (x [y #:mutable])) ; one mutable field
(struct foo3 (x y) #:mutable) ; all fields mutable
El lenguaje también admite tablas hash inmutables, implementadas funcionalmente y diccionarios inmutables.
Oxido
El sistema de propiedad de Rust permite a los desarrolladores declarar variables inmutables y pasar referencias inmutables. De forma predeterminada, todas las variables y referencias son inmutables. Las variables y referencias mutables se crean explícitamente con la mut
palabra clave.
Los elementos constantes en Rust son siempre inmutables.
// constant items are always immutable
const ALWAYS_IMMUTABLE: bool = true;
struct Object {
x: usize,
y: usize,
}
fn main() {
// explicitly declare a mutable variable
let mut mutable_obj = Object { x: 1, y: 2 };
mutable_obj.x = 3; // okay
let mutable_ref = &mut mutable_obj;
mutable_ref.x = 1; // okay
let immutable_ref = &mutable_obj;
immutable_ref.x = 3; // error E0594
// by default, variables are immutable
let immutable_obj = Object { x: 4, y: 5 };
immutable_obj.x = 6; // error E0596
let mutable_ref2 =
&mut immutable_obj; // error E0596
let immutable_ref2 = &immutable_obj;
immutable_ref2.x = 6; // error E0594
}
Scala
En Scala , cualquier entidad (estrictamente, un enlace) se puede definir como mutable o inmutable: en la declaración, se puede usar val
(valor) para entidades inmutables y var
(variable) para mutables. Tenga en cuenta que aunque un enlace inmutable no se puede reasignar, aún puede referirse a un objeto mutable y aún es posible llamar a métodos mutantes en ese objeto: el enlace es inmutable, pero el objeto subyacente puede ser mutable.
Por ejemplo, el siguiente fragmento de código:
val maxValue = 100
var currentValue = 1
define una entidad inmutable maxValue
(el tipo entero se infiere en tiempo de compilación) y una entidad mutable nombrada currentValue
.
De forma predeterminada, las clases de colección como List
y Map
son inmutables, por lo que los métodos de actualización devuelven una nueva instancia en lugar de mutar una existente. Si bien esto puede parecer ineficiente, la implementación de estas clases y sus garantías de inmutabilidad significan que la nueva instancia puede reutilizar los nodos existentes, lo que, especialmente en el caso de crear copias, es muy eficiente.
Ver también
Referencias
Este artículo contiene material del Libro de patrones de diseño de Perl
enlaces externos
- Objetos inmutables en C # mediante 3 sencillos pasos.
- Artículo Teoría y práctica de Java: ¿mutar o no mutar? por Brian Goetz , de IBM DeveloperWorks - copia guardada en Internet Archive por Brian Goetz , de IBM DeveloperWorks - copia guardada en Internet Archive
- Objetos inmutables de JavaPractices.com
- Objetos inmutables de Portland Pattern Repository
- Immutable.js por Facebook
- Estructuras inmutables en proyecto de código abierto C # en Codeplex
- Colecciones inmutables en la biblioteca oficial de .NET de Microsoft
- Objetos inmutables en C # por Tutlane.com