Tipos de memoria
Stack
Aquí se guardan datos de la forma último en entrar, primero en salir
(LIFO).
Para guardar datos, se debe saber con antelación cuanto vamos a guardar y no
podrá cambiar de tamaño durante la ejecución del programa.
1fn foo() {
2 let var = 10; // push to the stack
3 // operations with `var`
4} // var goes out of scope, popped from de stack (data deleted)
Heap
Este tipo de memoria es completamente desorganizada. Se guardan los datos en medida que se vayan necesitando, de modo que podemos cambiar su tamaño.
Su funcionamiento es el siguiente:
- Se busca un sitio libre del tamaño requerido en memoria y se guardan los datos allí.
- Para poder acceder a esos datos, devuelve un puntero a esta memoria. Y como este es de tamaño fijo, podemos guardarlo en la stack, de forma que usamos ese puntero para leer/escribir los datos.
1int * var; // push to the stack a pointer to an int
2
3int main() {
4 var = new int; // allocate in the heap a int
5 // operations with var
6 delete var; // manually delete the memory on the heap
7} // var goes out of scope, popped from de stack (data deleted)
Stack vs Heap
Por lo general, la heap es mucho más lenta que la stack, porque para guardar datos primero tiene que buscar un sitio; y para leer tiene que seguir un puntero; pero sigue siendo necesaria por el hecho de almacenar datos de tamaño variable, pero en caso contrario, se debería evitar su uso.
Ownership and borrowing
Hay varias formas de manejar la memoria:
Garbage collector:
- Pros:
- (Casi) sin errores.
- Se escriben los programas más rápido.
- Cons:
- No se tiene control sobre la memoria.
- Rendimiento lento y impredecible.
- El tamaño del ejecutable es más grande.
- Pros:
Manual:
- Pros:
- Control total sobre la memoria.
- Mejor rendimiento.
- El tamaño del ejecutable es más pequeño.
- Cons:
- Da lugar a errores.
- Se escriben los programas muy despacio.
- Pros:
Ownership:
- Pros:
- Control sobre la memoria.
- (Casi) sin errores.
- Mejor rendimiento.
- El tamaño del ejecutable es más pequeño.
- Cons:
- Se escriben los programas muy lento y tiene una curva de aprendizaje difícil: luchando con el borrow checker.
- Pros:
Ownership
Esto es la asociación de un valor con un nombre (variable). Las reglas son las siguientes:
- Cada valor tiene una variable y se le llama su dueño.
- Solamente puede haber un dueño a la vez.
- Cuando el dueño se va
out of scope
, los datos son borrados.
Variable scope
1{ // s is not valid here, it's not yet declared
2 let s = "hello"; // s is valid from this point forward
3 // do stuff with s
4} // this scope is now over, and s is no longer valid
"hello"
se guarda en la stack porque es de tamaño fijo, pero no se puede
cambiar su valor. En cambio, si usamos String
, guardamos los datos en la heap
y entonces ya podremos modificarla.
1{ // s is not valid here, it's not yet declared
2 let s = String::from("hello"); // s is valid from this point forward
3 // do stuff with s
4} // this scope is now over, and s is no longer valid (popped from the stack
5 // and the data doesn't have an owner, so it gets freed automatically)
Interactivity
1let s1 = String::from("hello"); // allocate on the heap
2let s2 = s1; // move (all the data is moved from s1 to the variable s2. Now the
3 // owner of "hello" is s2)
4// cannot use s1 anymore, because it doesn't have any data.
5
6// SOLUTION
7let s2 = s1.clone(); // clone all the data (get a copy of each value and store
8 // it on new memory -very expensive-)
1fn main() {
2 let s = String::from("hello"); // allocate on the heap
3 foo(s); // -> some_string = s // move the ownership to some_string
4 // cannot use s anymore, because it doesn't have any data
5}
6fn foo(s1: String) {
7 // some operations
8} // some_string gets dropped ("hello" is removed)
9
10// SOLUTION (not recommended)
11fn main() {
12 let mut s = String::from("hello"); // allocate on the heap
13 s = foo(s);
14 // now we can use s here
15}
16fn foo(s1: String) -> String { // some_string = s // takes ownership
17 // some operations
18 some_string // s = some_string // gives back ownership
19}
20
21// SOLUTION (borrowing)
22fn main() {
23 let s = String::from("hello"); // allocate on the heap
24 foo(&s); // create a pointer to s
25 // now we can use s here
26}
27fn foo(s1: &String) { // gets ownership of the pointer, but not actual memory
28 // some operations
29} // the pointer gets dropped, but not the memory
Esto no ocurre con valores del stack, simplemente se hace una copia.
Borrowing
Este es un diagrama de la solución del último ejemplo.
Como ya vimos, podemos crear una referencia añadiendo &
delante.
Nota: Lo contrario a crear una referencia (al usar
&
) es desreferenciar, que se consigue usando*
.
Hay que tener en cuenta, que la sintaxis puede ser un poco diferente a otros
lenguajes: para indicar que el valor es una referencia, añadimos &
delante del
tipo de dato; y para crear la referencia, añadimos el &
delante del valor.
El uso de estas referencias es muy útil en casos donde no queramos tomar ownership de una variable, pero igualmente necesitamos su valor.
En casos donde es necesario modificar ese valor, debemos crear una referencia
mutable: &mut
.
Las reglas de las referencias son las siguientes:
- La referencia se borra una vez que se usó por última vez.
- Puedes tener, o una referencia mutable, o cualquier número de referencias inmutables; pero no una referencia mutable y varias inmutables a la vez.
- Las referencias deben ser siempre válidas.
Lifetimes
Algunos links de ayuda:
Todas las referencias en Rust tienen un lifetime, que es el scope por la cual la referencia es válida. Pero la mayoría del tiempo son implícitas por el compilador.
Las referencias deben de ser siempre válidas, si tenemos una referencia a un
valor borrado el compilador lo sabrá y dará un error. Esto se comprueba con el
borrow checker
.
Las normas que usa el compilador para asignar las lifetimes:
Si hay exactamente un input lifetime como parámetro, esta se asignará a todas las output lifetimes parameters
Si hay varias input lifetime parameters, pero una de ellas es
&self
o&mut self
, la lifetime de self se asigna a todas las output lifetime parameters'static
: es una lifetime para todo el programa (como string literals)
1let reference;
2{
3 let value = 10;
4 reference = &value;
5}
6// reference no es válida aquí (dangeling reference)
1let s1 = String::from("a");
2let s2 = String::from("abcdef");
3
4let result = longest(s1.as_str(), s2.as_str());
5println!(result);
6
7// &i32: referencia
8// &'a i32: referencia con lifetime
9// &'a mut i32: mut ref con lifetime
10fn longest<'a>(a: &'a str, b: &'a str) -> &'a str{ // sin la generic da error
11 if a.len() > b.len() {
12 a
13 } else {
14 b
15 }
16}
Smart Pointer
Se trata de un puntero, una variable que guarda una dirección de memoria, por lo que es una variable que contiene otra variable (y sus datos). Lo más común es la referencia.
Smart Pointers hacen un seguimiento de los punteros a una memoria en concreto, y cuando no hay más punteros, los datos se borran.
A diferencia de las referencias, los Smart Pointers son dueños de los datos de alguna forma. Los vectores y los strings son Smart Pointers: tienen metadatos, son dueños de los datos.
Smart Pointers implementan los traits Deref
y Drop
.
Deref
Este trait nos permite implementar el operador para desreferenciar un valor (*
)
en una estructura de datos creada por nosotros. Sin ella, el compilador solo
sabrá como desreferenciar una referencia, no un objecto creado por nosotros.
Para las referencia mutables, en cambio, debemos usar el trait DerefMut
1struct MyBox<T>(T); // Tuple Struct
2
3impl<T> MyBox<T> {
4 fn new(x: T) -> MyBox {
5 MyBox(x) // Para este ejemplo no es necesario guardar el valor en heap
6 }
7}
8
9impl<T> Deref for MyBox<T> {
10 type Target = T;
11
12 fn deref(&self) -> &T {
13 &self.0 // Devuelve una referencia al valor guardado.
14 // Se debe devolver una referencia, por el sistema de ownership que
15 // ya vimos. Y esta referencia podrá ser desreferenciada por el
16 // compilador como siempre.
17 }
18}
19
20fn main() {
21 let x = 5;
22 let y = MyBox::new(x);
23
24 assert_eq!(5, x);
25 assert_eq!(x, *y); // Equivalente a: *(y.deref())
26}
Dered Coercion
: En algunos casos, la desreferencia ya se llama automáticamente,
ya que ayuda a crear un código más legible:
1fn main() {
2 let m = MyBox::new(String::from("Rust"));
3 hello(&m); // La función toma un `&str`, pero le estamos pasando un `MyBox`
4 // Aquí se ha desreferenciado automaticametente.
5 // &MyBox<String> -> &String -> &str (String también implementa Deref)
6}
7
8fn hello(name: &str) {
9 println!("Hello, {}!", name);
10}
Estas son las reglas donde se usa Deref Coercion:
- desde
&T
a&U
dondeT: Deref<Target=U>
- desde
&mut T
a&mut U
dondeT: DerefMut<Target=U>
- desde
&mut T
a&U
dondeT: Deref<Target=U>
El caso donde no hay Deref Coercion es desde &T
a &mut U
, ya que esto va en
contra de las reglas de borrowing.
Drop
Este trait permite personalizar qué es lo que ocurre cuando un valor sale de
scope y debe ser eliminado. Funciona algo así como un destructor. En el caso de
Box
, el comportamiento que queremos es que se eliminen los datos guardados en
la heap.
Este método también se llama automáticamente, pero podemos llamarlo para
eliminar el valor prematuramente: drop(T)
(no se permite llamar directamente
.drop()
)
1struct Entity(i32);
2
3impl Drop for Entity {
4 fn drop(&mut self) {
5 // Aquí manejamos qué ocurre cuando se va a eliminar este objeto
6 println!("Borrando Entity con valor {}...", self.0);
7 }
8}
9
10fn main() {
11 let my_entity = Entity(10);
12 // ...
13} // Aquí se debe eliminar `my_entity`, por lo que se llama a `drop` y imprimirá
14 // "Borrando Entity con valor 10...".
Box
Box<T>
es un Smart Pointer que guarda datos en la heap, mientras que el propio
Box<>
se guarda en la stack. Esta es una dirección de memoria a los datos que
en tiene guardados en la heap.
1enum List<T> {
2 // Rust necesita saber cuanto espacio ocupa List, debemos hacer Box<List<T>>
3 Cons(T, Box<List<T>>),
4 Nul
5}
6
7use List::{Cons, Nul};
8let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nul))))));
Contando referencias - Shared Smart Pointer
En la mayoría de los casos, los valores tienen siempre un solo dueño, pero en otros, es necesario que sea un valor compartido.
Piensa en este ejemplo: una persona enciende una televisión y se pone a verla.
A medida que más personas vienen a ver la televisión, esta se mantiene
encendida. Cuando las personas se van marchando, la última es la encargada de
apagar la tele. Si se hubiese apagado mientras todavía hay personas viendo, el
resultado será panic
.
Puedes intercambiar a las personas por variables y la televisión por los datos:
si la primera persona al irse (fuera del scope
), apagase la tele (borrase los
datos); el resto no podría usarla.
Nota: Esto solo se puede usar en programas de un solo
thread
.
1use std::rc::Rc;
2
3fn main() {
4 let value = "ejemplo".to_string();
5
6 {
7 // Se crea `a`. Hay 1 referencia a `value`.
8 let a = Rc::new(value);
9
10 {
11 // Se clona `a` a `b`. Ahora hay dos referencias a `value`
12 let b = Rc::clone(&a);
13
14 // Los dos son iguales
15 assert_eq!(a, b);
16
17 // Podemos usar los valores normalmente
18 println!("{}", a);
19 } // Se borra `b`. Hay una referencia a `value`
20 } // Se borra `a`. No hay ninguna referencia a `value`, también se borra.
21
22 // No se puede usar `value`, porque se ha movido dentro del `Rc` y porque se
23 // ha eliminado al salir este de scope.
24}
El método Rc::clone(Rc<T>)
no creará una copia tal cual del valor (ya que es
no es eficiente ni necesario), si no que simplemente aumentará el número de
referencias al valor. Cuando se eliminen todas las referencias, el valor se
eliminará.
Podemos saber cuantas referencias tiene usando el método
Rc::strong_count(&Rc<T>)
.
Interior Mutability
Interior Mutability
nos permite mutar datos incluso cuando tenemos referencias
inmutables a esos datos. Esto se consigue usando código no seguro en una
estructura de datos, llamada RefCell
.
Se trata de código que no está comprobado por el compilador siguiendo las reglas
convencionales de ownership, borrowing, etc. Esto se hace para incrementar la
flexibilidad. Y, en lugar de comprobar las reglas en el momento de compilación,
se hace en el momento de ejecución. En caso de error, panic!
.
Por norma general, es mejor comprobar estas reglas al compilar, además de que no afectan al rendimiento. Sin embargo, en algunos casos, comprobar las reglas en el momento de ejecución pueden permitir acciones que el compilador no. El ejemplo más famoso es el Problema de la Parada, que no se puede determinar haciendo un análisis estático.
Nota: Esto solo se puede usar en programas de un solo
thread
.
Cell
std::cell::Cell
nos permite tener varias referencias mutables al mismo valor,
siempre y cuando este sea un valor de la Stack (que implemente Copy), entonces
no habrá problema el tener dos referencias mutables.
1let x = Cell::new(0);
2let y = &x;
3let z = &x;
4
5x.set(1); // x = 1, y = 1, z = 1
6x.set(1); // x = 2, y = 2, z = 2
7x.set(1); // x = 3, y = 3, z = 3
Esto es equivalente a (pero no compila)
1let x = 0;
2let y = &mut x;
3let z = &mut x;
4
5x = 1 // x = 1, y = 1, z = 1
6*y = 2; // x = 2, y = 2, z = 2
7*z = 3; // x = 3, y = 3, z = 3
Técnicamente, no hay coste en tiempo de ejecución, pero deberíamos considerar, a
la hora de copiar un gran struct solo por un solo campo, hacer ese campo un
Cell
en lugar de todo el struct.
RefCell
Funciona de forma similar a Cell
, pero también se puede usar con datos en la
Heap, sin embargo, sí que tiene coste en tiempo de ejecución.
1let a = 5;
2let b = &mut a; // Error: `a` no es mutable
3
4let mut c = 5;
5let d = &c;
6*d = 2; // Error: `d` no es mutable
Recapitulación
Memoria | Varios dueños | Inmutable | Mutable | Comprobación | |
---|---|---|---|---|---|
Box | Heap | No | Sí | Sí | Compilar |
Rc | Heap | Sí | Sí | No | Compilar |
Cell | Stack | No | Sí | Sí | Compilar |
RefCell | ? | No | Sí | Sí | Ejecución |