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, consten C ++ , finalen 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 mutablepara 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 StringBuffery 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 constantpalabra 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 readonlydeclaració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 Cartpermitirí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 constinstancias).

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 mutablepalabra clave, que permite cambiar una variable miembro desde dentro de un constmé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 , consty immutable, para las variables que no se pueden cambiar. A diferencia de C ++ const, Java finaly 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 consty immutablees a lo que se aplican: constes una propiedad de la variable: pueden existir legalmente referencias mutables al valor referido, es decir, el valor realmente puede cambiar. Por el contrario, immutablees 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 consto 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 Cobjeto mutable , mFieldse puede escribir en. Para un const(C)objeto, mFieldno se puede modificar, hereda const; iFieldsigue 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, cpodría referirse al mismo objeto que m, por lo que las mutaciones en también mpodrían cambiar indirectamente c. Además, cpuede hacer referencia al mismo objeto que i, pero dado que el valor es inmutable, no hay cambios. Sin embargo, my ilegalmente 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), constes una garantía solo externa de que la función no cambiará nada y immutablees una garantía bidireccional (la función no cambiará el valor y el llamador debe no lo cambie).

Valores que son consto immutabledeben ser inicializados por asignación directa en el punto de declaración o por un constructor .

Debido a que los constparámetros olvidan si el valor era mutable o no, una construcción similar inoutactú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 Spara mutables Targumentos, 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 stringes 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 dupfunción.

Java

Un ejemplo clásico de un objeto inmutable es una instancia de la Stringclase Java

String s = "ABC";
s.toLowerCase();

El método toLowerCase()no modifica los datos "ABC" que scontiene. 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 scontenga los datos "abc", se necesita un enfoque diferente:

s = s.toLowerCase();

Ahora String hace sreferencia 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 finalpalabra clave. finalsolo 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 constdeclaració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.namedtupley 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, dataclassespermite a los desarrolladores emular la inmutabilidad con instancias congeladas . Si una clase de datos congelada se construye, dataclassesse anulará __setattr__()y __delattr__()elevar FrozenInstanceErrorsi 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 mutpalabra 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 Listy Mapson 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