|
| 1 | +# Mutabilidad Interna |
| 2 | + |
| 3 | +Cuando vimos `Box`, entendimos cómo Rust controla la propiedad de un valor en el |
| 4 | +heap. |
| 5 | +El siguiente paso es entender cómo Rust nos permite mutar valores incluso cuando |
| 6 | +no tenemos acceso mutable directo: eso es lo que llamamos mutabilidad interna. |
| 7 | + |
| 8 | +En Rust, el sistema de tipos y el sistema de préstamos (borrowing) son tan |
| 9 | +poderosos que nos permiten tener mutabilidad incluso cuando el valor no es |
| 10 | +mutable en sí mismo. |
| 11 | + |
| 12 | +Puede parecer contradictorio, pero es una característica intencional del |
| 13 | +lenguaje que nos permite escribir código seguro y concurrente sin sacrificar la |
| 14 | +flexibilidad. |
| 15 | + |
| 16 | +En Go seria imposible mutar un valor a través de una referencia inmutable, pero en Rust |
| 17 | + |
| 18 | +En Go esto no existe como concepto formal. En Go uno simplemente pasa un puntero |
| 19 | +y puede mutar el valor, porque el lenguaje permite `aliasing libre` y no impone |
| 20 | +restricciones de préstamo o propiedad, no tenemos `trait bounds` que nos ayuden |
| 21 | +a controlar el acceso a los datos, ni delimitar las posibilidades de un objeto. |
| 22 | + |
| 23 | +## Aliasing |
| 24 | + |
| 25 | +```go |
| 26 | +package main |
| 27 | +import "fmt" |
| 28 | +func main() { |
| 29 | + x := 10 |
| 30 | + p := &x // p apunta a x |
| 31 | + q := &x // q también apunta a x |
| 32 | + *p = 20 |
| 33 | + fmt.Println(x, *q) // imprime 20 20 |
| 34 | +} |
| 35 | +``` |
| 36 | + |
| 37 | +Aquí `x` tiene aliasing, porque `p` y `q` son dos referencias que pueden mutar |
| 38 | +el mismo valor. |
| 39 | + |
| 40 | +Es decir el termino `aliasing` se refiere a que el lenguaje permite tener varias |
| 41 | +referencias mutables o inmutables al mismo tiempo, sin restricciones. |
| 42 | + |
| 43 | +En Go, puedes tener tantos punteros como quieras apuntando al mismo valor y |
| 44 | +mutarlos a voluntad. |
| 45 | + |
| 46 | +Esto es flexible, pero también arriesgado, porque puedes crear condiciones de |
| 47 | +carrera si accedes desde múltiples threads sin locks u otros tipos de problemas |
| 48 | +con respecto a la consistencia. |
| 49 | + |
| 50 | +### ¿Cómo maneja esto Rust? |
| 51 | + |
| 52 | +Rust no permite aliasing libre para referencias mutables |
| 53 | + |
| 54 | +Solo una referencia mutable (`&mut T`) puede existir a la vez, o varias |
| 55 | +referencias inmutables (`&T`) pueden coexistir, pero no puedes combinarlas con |
| 56 | +una mutable como hemos visto en capítulos anteriores. |
| 57 | + |
| 58 | +Pero hay casos en donde quizás podamos necesitar un mecanismo similar al |
| 59 | +aliasing, no exactamente lo mismo porque no queremos que sea tan libre, pero |
| 60 | +sí que nos permita cierta flexibilidad para que algunos casos, como caches, |
| 61 | +contadores, o estructuras de datos complejas, en esos casos deberíamos poder |
| 62 | +mutar su estado interno incluso cuando no tenemos acceso mutable directo. |
| 63 | + |
| 64 | +Quizás queremos implementar un comportamiento en que el usuario de nuestra |
| 65 | +estructura no necesite preocuparse por si es mutable o no, y que internamente |
| 66 | +podamos cambiar su estado. |
| 67 | + |
| 68 | +Es aquí donde conceptos previamente vistos como `Smart Pointers` y |
| 69 | +`trait bounds` nos ayudan a implementar este patrón de `mutabilidad interna`. |
| 70 | + |
| 71 | +Esto es algo que Go resuelve "liberando" la mutabilidad, mientras que Rust lo |
| 72 | +resuelve "restringiendo" y pidiendo al compilador que controle la seguridad. |
| 73 | + |
| 74 | +## Dos nuevos Smart Pointers |
| 75 | + |
| 76 | +Cuando exploramos `Box`, entendimos que Rust cuida celosamente quién es el dueño |
| 77 | +de un valor en el `heap`. Esa propiedad nos da seguridad, nadie puede tocar lo |
| 78 | +que no le pertenece. Pero la vida real, y nuestros programas, no siempre son tan |
| 79 | +rígidos. A veces necesitamos un poco de flexibilidad, queremos cambiar cosas |
| 80 | +aunque no tengamos permiso explícito, al menos en apariencia. |
| 81 | + |
| 82 | +### Cell |
| 83 | + |
| 84 | +Rust nos ofrece esta ventana de libertad cuidadosamente medida. Primero, con |
| 85 | +`Cell`, un contenedor pequeño y sencillo. Nos permite alterar valores simples, |
| 86 | +incluso cuando la variable que los contiene parece inmutable. Es como si Rust |
| 87 | +nos dijera: "Sí, puedes tocar esto, pero solo si sabes lo que haces y no |
| 88 | +compartes el juguete con otros al mismo tiempo". |
| 89 | + |
| 90 | +Rust no nos deja hacer esto con cualquier cosa. `Cell` es para tipos que son |
| 91 | +`Copy`, es decir, tipos simples como números o booleanos. Si intentamos usarlo |
| 92 | +con algo más complejo, Rust nos detendrá en seco, también nos prohibe de usarlo |
| 93 | +en contextos concurrentes, porque ahí la seguridad es aún más crítica. |
| 94 | + |
| 95 | +Rust logra esto nuevamente utilizando su sistema de tipos y `trait bounds`. |
| 96 | +`Cell` implementa el `!Sync` trait, lo que significa que no puede ser compartido |
| 97 | +entre threads. Esto nos protege de condiciones de carrera y otros problemas que |
| 98 | +podrían surgir si varios threads intentaran mutar el mismo valor al mismo |
| 99 | +tiempo. |
| 100 | + |
| 101 | +Cuando veamos más adelante el capítulo de concurrencia, entenderemos mejor esto. |
| 102 | + |
| 103 | +Veamos un ejemplo simple: |
| 104 | + |
| 105 | +```rust |
| 106 | +use std::cell::Cell; |
| 107 | + |
| 108 | +fn main() { |
| 109 | + let x = Cell::new(5); |
| 110 | + x.set(10); |
| 111 | + println!("{}", x.get()); // imprime 10 |
| 112 | +} |
| 113 | +``` |
| 114 | + |
| 115 | +Como veremos de forma similar que con `Box` debemos de importarlo desde el |
| 116 | +módulo y luego podemos crear una `Cell` con un valor inicial, nuevamente |
| 117 | +`Cell` es un `Smart Pointer`, el valor se almacena en el heap y `Cell` mismo |
| 118 | +controlara la propiedad y el acceso a ese valor, es por eso que una vez |
| 119 | +inicializado `x` es inmutable, pero internamente podemos cambiar su valor con |
| 120 | +el método `set`, y obtener su valor con `get`, el que validara si es posible |
| 121 | +modificar el valor o no sera el propio compilador. |
| 122 | + |
| 123 | +Esta estructura es considera de una abstracción de coste cero |
| 124 | +(zero-cost abstraction), porque el compilador optimiza el código para que no |
| 125 | +haya sobrecarga en tiempo de ejecución, es decir, el código generado es tan |
| 126 | +eficiente como si hubiéramos manipulado el valor directamente. |
| 127 | + |
| 128 | +### RefCell |
| 129 | + |
| 130 | +La realidad nos demuestra que rara vez la mutabilidad que buscamos se reduce a |
| 131 | +tipos de datos simples como enteros o booleanos. Nosotros muchas veces |
| 132 | +buscaremos mutar estructuras más complejas, para estructuras más complejas, |
| 133 | +surge `RefCell`, que nos da un poco más de libertad: podemos pedir prestadas |
| 134 | +referencias mutables a datos incluso si el contenedor parece inmutable. Rust ya |
| 135 | +no puede verificar todo en compile time, así que hace un chequeo en runtime: si |
| 136 | +intentamos ser demasiado ambiciosos y pedir mutaciones conflictivas, Rust nos |
| 137 | +detiene antes de que causemos estragos. |
| 138 | + |
| 139 | +```rust |
| 140 | +use std::cell::RefCell; |
| 141 | + |
| 142 | +fn main() { |
| 143 | + let data = RefCell::new(vec![1, 2, 3]); |
| 144 | + match data.try_borrow_mut() { |
| 145 | + Ok(mut_ref) => { |
| 146 | + mut_ref.push(4); |
| 147 | + } |
| 148 | + Err(_) => { |
| 149 | + println!("No se pudo obtener una referencia mutable"); |
| 150 | + } |
| 151 | + } |
| 152 | + let borrowed = data.try_borrow(); |
| 153 | + match borrowed { |
| 154 | + Ok(ref_ref) => { |
| 155 | + println!("{:?}", ref_ref); // imprime [1, 2, 3, 4] |
| 156 | + } |
| 157 | + Err(_) => { |
| 158 | + println!("No se pudo obtener una referencia inmutable"); |
| 159 | + } |
| 160 | + } |
| 161 | +} |
| 162 | +``` |
| 163 | + |
| 164 | +Aquí, `RefCell` nos permite mutar un vector incluso cuando `data` es |
| 165 | +inmutable. Usamos `try_borrow_mut` para obtener una posible referencia mutable |
| 166 | +al vector y si nos lo permite el lenguaje podemos modificarlo. Si intentamos |
| 167 | +pedir prestadas múltiples referencias mutables al mismo tiempo, obtendremos un |
| 168 | +error. |
| 169 | + |
| 170 | +El comportamiento de `RefCell` se basa en el conteo de referencias en tiempo de |
| 171 | +ejecución. Mantiene un registro de cuántas referencias inmutables y mutables |
| 172 | +están activas. Si intentamos pedir prestada una referencia mutable mientras hay |
| 173 | +referencias inmutables activas, o viceversa, `RefCell` nos impedirá hacerlo, |
| 174 | +lanzando un pánico en tiempo de ejecución en el caso de usar `borrow_mut` o |
| 175 | +`borrow` o retornando un `Err` en el caso de usar `try_borrow_mut` o |
| 176 | +`try_borrow`. |
| 177 | + |
| 178 | +Rust permite ambos manejos de errores pero no nos permite ignorar el error, |
| 179 | +porque eso rompería la seguridad que Rust nos ofrece, es preferible mostrar |
| 180 | +un error en tiempo de ejecución que permitir un comportamiento indefinido los |
| 181 | +cuales suelen ser la causa de bugs muy difíciles de rastrear. |
| 182 | + |
| 183 | +El caso de arriba es solo una ejemplificación simple, pero veamos una |
| 184 | +abstracción más realista: |
| 185 | + |
| 186 | +{{#tabs }} |
| 187 | +{{#tab name="main.rs" }} |
| 188 | + |
| 189 | +```rust |
| 190 | +mod client; |
| 191 | +use client::Client; |
| 192 | +use std::thread::sleep; |
| 193 | + |
| 194 | +fn main() { |
| 195 | + let limiter = Client::new(); |
| 196 | + |
| 197 | + for i in 0..=5 { |
| 198 | + match limiter.execute_request() { |
| 199 | + Ok(_) => println!("Request {i} ejecutada con éxito"), |
| 200 | + Err(e) => println!("Error en request {i}: {e}"), |
| 201 | + } |
| 202 | + sleep(Duration::from_secs(1)); |
| 203 | + } |
| 204 | + |
| 205 | + println!("Uso actual: {}", limiter.current_usage()); |
| 206 | +} |
| 207 | +``` |
| 208 | + |
| 209 | +{{#endtab }} |
| 210 | +{{#tab name="client.rs" }} |
| 211 | + |
| 212 | +```rust |
| 213 | +use std::cell::RefCell; |
| 214 | +use std::time::{Duration, Instant}; |
| 215 | + |
| 216 | +struct Client { |
| 217 | + limit: i32, |
| 218 | + usage: RefCell<i32>, // mutabilidad interna |
| 219 | + last_request: RefCell<Option<Instant>>, // cooldown interno |
| 220 | +} |
| 221 | + |
| 222 | +impl Client { |
| 223 | + fn new() -> Self { |
| 224 | + Client { |
| 225 | + limit: 5, |
| 226 | + usage: RefCell::new(0), |
| 227 | + last_request: RefCell::new(None), |
| 228 | + } |
| 229 | + } |
| 230 | + |
| 231 | + fn execute_request(&self) -> Result<(), String> { |
| 232 | + // Intentamos actualizar el cooldown |
| 233 | + let Ok(mut last) = self.last_request.try_borrow_mut() else { |
| 234 | + return Err("No se pudo acceder al cooldown".to_string()); |
| 235 | + }; |
| 236 | + |
| 237 | + let now = Instant::now(); |
| 238 | + |
| 239 | + if let Some(prev) = *last { |
| 240 | + if now.duration_since(prev) < Duration::from_secs(1) { |
| 241 | + return Err("Cooldown activo, espera antes de enviar otra request".to_string()); |
| 242 | + } |
| 243 | + } |
| 244 | + |
| 245 | + // actualizamos el cooldown |
| 246 | + *last = Some(now); |
| 247 | + |
| 248 | + let Ok(mut usage) = self.usage.try_borrow_mut() else { |
| 249 | + return Err("No se pudo ejecutar la request".to_string()); |
| 250 | + }; |
| 251 | + |
| 252 | + if *usage < self.limit { |
| 253 | + *usage += 1; // incremento secreto |
| 254 | + // Se ejecuto |
| 255 | + Ok(()) |
| 256 | + } else { |
| 257 | + Err("Límite de requests alcanzado".to_string()) |
| 258 | + } |
| 259 | + } |
| 260 | + |
| 261 | + fn current_usage(&self) -> i32 { |
| 262 | + match self.usage.try_borrow() { |
| 263 | + Ok(u) => *u, |
| 264 | + Err(_) => { |
| 265 | + println!("No se puede leer usage ahora"); |
| 266 | + 0 |
| 267 | + } |
| 268 | + } |
| 269 | + } |
| 270 | +} |
| 271 | +``` |
| 272 | + |
| 273 | +{{#endtab }} |
| 274 | +{{#endtabs }} |
| 275 | + |
| 276 | +<div class="info play ferris-scientific"> |
| 277 | +<p>Link de ejemplo completo en <a href="https://www.rustexplorer.com/b/z00dc1">Rust Explorer</a></p> |
| 278 | +</div> |
| 279 | + |
| 280 | +En este ejemplo podemos ver cómo `Client` usa `RefCell` para manejar su estado |
| 281 | +interno de manera segura. Aunque `Client` es inmutable desde el exterior, puede |
| 282 | +mutar los atributos `usage` y `last_request` internamente gracias a `RefCell`. |
| 283 | + |
| 284 | +Es una forma elegante de encapsular la mutabilidad, permitiendo que el usuario |
| 285 | +de `Client` no tenga que preocuparse por si es mutable o no, mientras que |
| 286 | +internamente `Client` puede gestionar su estado de manera segura y controlada. |
| 287 | + |
| 288 | +Esta es una de las ventajas de usar Smart Pointers, podemos crear abstracciones |
| 289 | +más complejas y seguras, aprovechando el sistema de tipos y el control de |
| 290 | +préstamos de Rust. |
| 291 | + |
| 292 | +Así es como podemos mantener un estado interno mutable, sin requerir al usuario |
| 293 | +de nuestra estructura que tenga que lidiar con la mutabilidad directamente. |
| 294 | + |
| 295 | +## ¿Por qué es útil esto? |
| 296 | + |
| 297 | +Va a ser muy útil en varios escenarios como los mencionados antes, pero |
| 298 | +es realmente útil a la hora de hacer abstracciones más complejas, porque |
| 299 | +nos permite que el usuario no se preocupe que se modifica de la variable, |
| 300 | +si tuvieramos que declarar todo como mutable, el usuario final tendría que |
| 301 | +tener un mayor conocimiento de cómo funciona nuestra estructura, y eso |
| 302 | +rompería la encapsulación. |
| 303 | + |
| 304 | +En este caso nosotros no le pedimos nada al usuario, el usuario simplemente |
| 305 | +crea un `Client` y ejecutara las request y el `Client` internamente se encargara |
| 306 | +de gestionar su estado. |
| 307 | + |
| 308 | +Son abstracciones inteligentes que nos permiten escribir código más limpio y |
| 309 | +seguro. |
| 310 | + |
| 311 | +--- |
| 312 | + |
| 313 | +En el proximo capítulo veremos cómo Rust maneja la concurrencia, y veremos que |
| 314 | +estas abstracciones son aún más valiosas en ese contexto, tendremos otra |
| 315 | +abstracción que nos permitirá mutar datos de forma segura incluso en presencia |
| 316 | +de múltiples threads. |
| 317 | + |
| 318 | +Asegurándonos de que no haya condiciones de carrera y nuevamente manteniendo la |
| 319 | +consistencia de los datos. |
| 320 | + |
0 commit comments