Borrowing: Referencias Seguras en Rust
El borrowing (préstamo) permite usar valores sin tomar su ownership. Las referencias nos permiten referir a un valor sin ser responsables de liberarlo, resolviendo muchos problemas que surgen con el sistema de ownership.
¿Qué es Borrowing?
Borrowing es crear referencias a un valor sin tomar su ownership. Es como “pedir prestado” el valor - puedes usarlo, pero debes devolverlo intacto.
fn main() {
let s1 = String::from("hola");
// Crear una referencia (borrowing)
let longitud = calcular_longitud(&s1);
// s1 sigue siendo válido porque no transferimos ownership
println!("La longitud de '{s1}' es {longitud}");
}
fn calcular_longitud(s: &String) -> usize {
s.len()
} // s sale de scope, pero no elimina el valor porque no es propietario
Referencias Inmutables
Por defecto, las referencias son inmutables:
fn main() {
let s = String::from("hola mundo");
// Múltiples referencias inmutables están permitidas
let r1 = &s;
let r2 = &s;
let r3 = &s;
println!("r1: {r1}");
println!("r2: {r2}");
println!("r3: {r3}");
// Todas las referencias pueden coexistir
println!("Original: {s}");
// Pasar referencias a funciones
imprimir_string(&s);
analizar_string(r1);
}
fn imprimir_string(texto: &String) {
println!("Imprimiendo: {texto}");
}
fn analizar_string(texto: &String) {
println!("Longitud: {}", texto.len());
println!("¿Está vacío? {}", texto.is_empty());
// texto.push_str("!!!"); // ERROR: no se puede modificar
}
Referencias Mutables
Para modificar un valor a través de una referencia, necesitas una referencia mutable:
fn main() {
let mut s = String::from("hola");
// Crear referencia mutable
cambiar_string(&mut s);
println!("Después del cambio: {s}");
}
fn cambiar_string(texto: &mut String) {
texto.push_str(", mundo!");
}
Restricciones de Referencias Mutables
Solo puede haber una referencia mutable a un valor en un scope dado:
fn main() {
let mut s = String::from("hola");
let r1 = &mut s;
// let r2 = &mut s; // ERROR: cannot borrow `s` as mutable more than once
println!("{r1}");
// r1 ya no se usa después de esto, así que podemos crear otra
let r2 = &mut s;
println!("{r2}");
}
Reglas de Borrowing
La Regla Fundamental
No puedes tener referencias mutables e inmutables al mismo tiempo:
fn main() {
let mut s = String::from("hola");
let r1 = &s; // OK - referencia inmutable
let r2 = &s; // OK - otra referencia inmutable
// let r3 = &mut s; // ERROR: cannot borrow as mutable while borrowed as immutable
println!("{r1} y {r2}");
// r1 y r2 ya no se usan después de este punto
let r3 = &mut s; // OK - ahora podemos tener una referencia mutable
println!("{r3}");
}
Lifetime de Referencias
Las referencias tienen un “lifetime” - deben ser válidas mientras se usan:
fn main() {
let mut s = String::from("hola");
{
let r = &s; // OK - r vive hasta el final de este bloque
println!("{r}");
} // r sale de scope aquí
// Ahora podemos crear una referencia mutable
let r_mut = &mut s;
r_mut.push_str(" mundo");
println!("{r_mut}");
}
Tipos de Referencias
Referencias a Diferentes Tipos
fn main() {
// Referencias a tipos primitivos
let numero = 42;
let ref_numero = №
println!("Número: {numero}, Referencia: {ref_numero}");
// Referencias a arrays
let array = [1, 2, 3, 4, 5];
let ref_array = &array;
println!("Primer elemento: {}", ref_array[0]);
// Referencias a tuplas
let tupla = (10, "hola");
let ref_tupla = &tupla;
println!("Segundo elemento: {}", ref_tupla.1);
}
Referencias vs Punteros
fn main() {
let x = 5;
let ref_x = &x;
// Dereferencing con *
println!("Valor de x: {x}");
println!("Valor a través de referencia: {}", *ref_x);
println!("Referencia (automática): {ref_x}"); // Rust desreferencia automáticamente
// Con strings
let s = String::from("hola");
let ref_s = &s;
println!("Longitud directa: {}", s.len());
println!("Longitud por referencia: {}", ref_s.len()); // Desreferenciado automático
println!("Longitud explícita: {}", (*ref_s).len()); // Desreferenciado manual
}
Slices: Referencias a Porciones
Los slices son referencias a una porción contigua de una colección:
String Slices
fn main() {
let s = String::from("hola mundo");
// Crear slices
let hola = &s[0..4]; // "hola"
let mundo = &s[5..10]; // "mundo"
let completo = &s[..]; // "hola mundo"
let desde_inicio = &s[..5]; // "hola "
let hasta_final = &s[5..]; // "mundo"
println!("Original: {s}");
println!("Hola: {hola}");
println!("Mundo: {mundo}");
println!("Completo: {completo}");
// Función que retorna un slice
let primera_palabra = obtener_primera_palabra(&s);
println!("Primera palabra: {primera_palabra}");
}
fn obtener_primera_palabra(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..] // Si no hay espacios, retorna toda la string
}
Mejorando con &str
fn main() {
let s = String::from("hola mundo");
// Esta función funciona con String y &str
let palabra1 = primera_palabra(&s);
let palabra2 = primera_palabra("hola rust");
println!("Palabra 1: {palabra1}");
println!("Palabra 2: {palabra2}");
}
// Mejor: usar &str en lugar de &String
fn primera_palabra(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
s
}
Array Slices
fn main() {
let array = [1, 2, 3, 4, 5];
// Diferentes slices del array
let slice_completo = &array;
let slice_parcial = &array[1..4]; // [2, 3, 4]
let primeros_tres = &array[..3]; // [1, 2, 3]
println!("Array completo: {:?}", slice_completo);
println!("Slice parcial: {:?}", slice_parcial);
println!("Primeros tres: {:?}", primeros_tres);
// Pasar slices a funciones
imprimir_slice(slice_parcial);
let suma = sumar_slice(&array[2..]);
println!("Suma desde índice 2: {suma}");
}
fn imprimir_slice(slice: &[i32]) {
println!("Imprimiendo slice: {:?}", slice);
}
fn sumar_slice(slice: &[i32]) -> i32 {
slice.iter().sum()
}
Patrones Comunes con Borrowing
Funciones que Solo Leen
fn main() {
let texto = String::from("Rust es genial");
let numeros = vec![1, 2, 3, 4, 5];
// Funciones que solo leen no necesitan ownership
mostrar_info(&texto);
estadisticas(&numeros);
// Los valores originales siguen disponibles
println!("Texto original: {texto}");
println!("Números originales: {:?}", numeros);
}
fn mostrar_info(s: &String) {
println!("Texto: '{s}'");
println!("Longitud: {}", s.len());
println!("¿Contiene 'Rust'? {}", s.contains("Rust"));
}
fn estadisticas(nums: &Vec<i32>) {
println!("Números: {:?}", nums);
println!("Cantidad: {}", nums.len());
println!("Suma: {}", nums.iter().sum::<i32>());
println!("Promedio: {:.2}", nums.iter().sum::<i32>() as f64 / nums.len() as f64);
}
Funciones que Modifican
fn main() {
let mut mensaje = String::from("Hola");
let mut puntuaciones = vec![85, 90, 78];
// Modificar a través de referencias mutables
agregar_exclamacion(&mut mensaje);
agregar_bonus(&mut puntuaciones, 5);
println!("Mensaje modificado: {mensaje}");
println!("Puntuaciones con bonus: {:?}", puntuaciones);
}
fn agregar_exclamacion(s: &mut String) {
s.push('!');
}
fn agregar_bonus(puntuaciones: &mut Vec<i32>, bonus: i32) {
for puntuacion in puntuaciones.iter_mut() {
*puntuacion += bonus;
}
}
Builder Pattern con References
struct Configuracion {
nombre: String,
debug: bool,
max_conexiones: u32,
}
impl Configuracion {
fn nueva() -> Self {
Configuracion {
nombre: String::from("default"),
debug: false,
max_conexiones: 100,
}
}
// Métodos que toman &mut self para modificar
fn set_nombre(&mut self, nombre: &str) -> &mut Self {
self.nombre = nombre.to_string();
self
}
fn set_debug(&mut self, debug: bool) -> &mut Self {
self.debug = debug;
self
}
fn set_max_conexiones(&mut self, max: u32) -> &mut Self {
self.max_conexiones = max;
self
}
// Método que toma &self para leer
fn mostrar(&self) {
println!("Configuración:");
println!(" Nombre: {}", self.nombre);
println!(" Debug: {}", self.debug);
println!(" Max conexiones: {}", self.max_conexiones);
}
}
fn main() {
let mut config = Configuracion::nueva();
// Chaining methods que usan referencias mutables
config
.set_nombre("MiApp")
.set_debug(true)
.set_max_conexiones(200);
config.mostrar();
}
Ejercicios Prácticos
Ejercicio 1: Analizador de Texto
fn main() {
let texto = String::from("Rust es un lenguaje de programación rápido y seguro");
// Análisis usando referencias
let info = analizar_texto(&texto);
println!("Texto: '{texto}'");
println!("Palabras: {}", info.0);
println!("Caracteres: {}", info.1);
println!("Palabra más larga: '{}'", info.2);
// Modificar el texto
let mut texto_mut = texto;
agregar_emoji(&mut texto_mut);
println!("Con emoji: {texto_mut}");
}
fn analizar_texto(texto: &str) -> (usize, usize, &str) {
let palabras: Vec<&str> = texto.split_whitespace().collect();
let num_palabras = palabras.len();
let num_chars = texto.chars().count();
let palabra_mas_larga = palabras
.iter()
.max_by_key(|palabra| palabra.len())
.unwrap();
(num_palabras, num_chars, palabra_mas_larga)
}
fn agregar_emoji(texto: &mut String) {
texto.push_str(" 🦀");
}
Ejercicio 2: Manipulador de Lista
fn main() {
let mut numeros = vec![3, 1, 4, 1, 5, 9, 2, 6];
println!("Lista original: {:?}", numeros);
// Operaciones que no modifican
mostrar_estadisticas(&numeros);
let pares = obtener_pares(&numeros);
println!("Números pares: {:?}", pares);
// Operaciones que modifican
duplicar_valores(&mut numeros);
println!("Después de duplicar: {:?}", numeros);
filtrar_mayores_que(&mut numeros, 10);
println!("Mayores que 10: {:?}", numeros);
}
fn mostrar_estadisticas(lista: &Vec<i32>) {
let suma: i32 = lista.iter().sum();
let promedio = suma as f64 / lista.len() as f64;
let max = lista.iter().max().unwrap();
let min = lista.iter().min().unwrap();
println!("Estadísticas:");
println!(" Suma: {suma}");
println!(" Promedio: {promedio:.2}");
println!(" Máximo: {max}");
println!(" Mínimo: {min}");
}
fn obtener_pares(lista: &Vec<i32>) -> Vec<i32> {
lista.iter()
.filter(|&&x| x % 2 == 0)
.cloned()
.collect()
}
fn duplicar_valores(lista: &mut Vec<i32>) {
for valor in lista.iter_mut() {
*valor *= 2;
}
}
fn filtrar_mayores_que(lista: &mut Vec<i32>, limite: i32) {
lista.retain(|&x| x > limite);
}
Ejercicio 3: Parser de Configuración Simple
fn main() {
let config_text = "nombre=MiApp\ndebug=true\nport=8080\nmax_users=1000";
let config = parsear_config(config_text);
mostrar_config(&config);
// Modificar configuración
let mut config_mut = config;
actualizar_puerto(&mut config_mut, 3000);
mostrar_config(&config_mut);
}
#[derive(Debug)]
struct Config {
nombre: String,
debug: bool,
port: u32,
max_users: u32,
}
fn parsear_config(texto: &str) -> Config {
let mut nombre = String::new();
let mut debug = false;
let mut port = 8080;
let mut max_users = 100;
for linea in texto.lines() {
if let Some((clave, valor)) = parsear_linea(linea) {
match clave {
"nombre" => nombre = valor.to_string(),
"debug" => debug = valor == "true",
"port" => port = valor.parse().unwrap_or(8080),
"max_users" => max_users = valor.parse().unwrap_or(100),
_ => println!("Clave desconocida: {clave}"),
}
}
}
Config { nombre, debug, port, max_users }
}
fn parsear_linea(linea: &str) -> Option<(&str, &str)> {
let partes: Vec<&str> = linea.split('=').collect();
if partes.len() == 2 {
Some((partes[0], partes[1]))
} else {
None
}
}
fn mostrar_config(config: &Config) {
println!("Configuración:");
println!(" Nombre: {}", config.nombre);
println!(" Debug: {}", config.debug);
println!(" Puerto: {}", config.port);
println!(" Max usuarios: {}", config.max_users);
}
fn actualizar_puerto(config: &mut Config, nuevo_puerto: u32) {
config.port = nuevo_puerto;
println!("Puerto actualizado a: {nuevo_puerto}");
}
Errores Comunes con Borrowing
Error 1: Referencias Colgantes (Dangling References)
// ¡ESTO NO COMPILA!
// fn crear_referencia_colgante() -> &String {
// let s = String::from("hola");
// &s // ERROR: returns a reference to data owned by the current function
// }
// Solución: retornar el valor, no una referencia
fn crear_string_correcta() -> String {
String::from("hola")
}
fn main() {
let s = crear_string_correcta();
println!("{s}");
}
Error 2: Múltiples Referencias Mutables
fn main() {
let mut s = String::from("hola");
// ¡ESTO NO COMPILA!
// let r1 = &mut s;
// let r2 = &mut s;
// println!("{r1} {r2}");
// Solución: usar las referencias en diferentes scopes
{
let r1 = &mut s;
r1.push_str(" mundo");
} // r1 sale de scope aquí
let r2 = &mut s; // Ahora está bien
r2.push('!');
println!("{r2}");
}
Puntos Clave para Recordar
- Las referencias no tienen ownership del valor que referencian
& crea referencias inmutables, &mut crea referencias mutables
- Solo una referencia mutable OR múltiples inmutables a la vez
- Las referencias deben ser válidas durante su uso (no dangling)
- Los slices son referencias a porciones de colecciones
&str es más flexible que &String para parámetros
- Rust desreferencia automáticamente en muchos contextos
- Las reglas de borrowing previenen data races y use-after-free
Próximo Paso
En el siguiente capítulo exploraremos las Structs, que nos permiten agrupar datos relacionados y crear tipos de datos personalizados, combinando ownership y borrowing de manera efectiva.