4. Préstamos y cadenas
Ejemplo completo: examples/l04_borrowing.rs — cargo run --example l04_borrowing.
Tomar prestado en lugar de mover
Sección titulada «Tomar prestado en lugar de mover»La lección 3 terminó con una función que «robaba» su argumento. La mayoría de las veces solo quieres consultar un valor: pasa una referencia con &.
fn word_count(text: &str) -> usize { text.split_whitespace().count()}
let title = String::from("the rust programming language");println!("{} words", word_count(&title)); // 4 wordsprintln!("{title}"); // title sigue siendo utilizableUna referencia toma prestado el valor: el propietario conserva la propiedad, y el préstamo debe terminar antes de que el propietario desaparezca.
Dos tipos de referencias
Sección titulada «Dos tipos de referencias»| Sintaxis | Cuántas a la vez | Puede modificar | |
|---|---|---|---|
| Referencia compartida | &T |
cualquier cantidad | no |
| Referencia mutable | &mut T |
exactamente una, y ninguna compartida | sí |
fn shout(text: &mut String) { text.make_ascii_uppercase(); text.push('!');}
let mut message = String::from("hello");shout(&mut message);println!("{message}"); // HELLO!Quien llama escribe &mut en el punto de la llamada — como con la palabra clave ref de C#, la mutación es visible donde ocurre. Java no tiene equivalente: cualquier método que tenga una referencia puede modificar el objeto.
La regla: compartida XOR mutable
Sección titulada «La regla: compartida XOR mutable»En cada momento puedes tener o bien muchos lectores o bien un único escritor — nunca ambos. El compilador lo comprueba; es lo que se llama el borrow checker (verificador de préstamos).
let mut names = vec![String::from("Ada")];let first = &names[0];names.push(String::from("Grace"));println!("{first}");error[E0502]: cannot borrow `names` as mutable because it is also borrowed as immutable --> e_borrow_mut.rs:4:5 |3 | let first = &names[0]; | ----- immutable borrow occurs here4 | names.push(String::from("Grace")); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here5 | println!("{first}"); | ----- immutable borrow later used hereNo es pedantería. push puede reasignar el buffer del vector, lo que dejaría a first apuntando a memoria liberada. En C#/Java el GC mantiene vivo el objeto antiguo, así que este error concreto no provoca un fallo — pero la misma regla atrapa un error que conoces bien.
Ya te has encontrado con este error
Sección titulada «Ya te has encontrado con este error»// Javafor (Integer n : numbers) { numbers.add(n * 2); // ConcurrentModificationException en tiempo de ejecución}// C#foreach (var n in numbers) { numbers.Add(n * 2); // InvalidOperationException: Collection was modified}En Rust, no pasa del compilador:
let mut numbers = vec![1, 2, 3];for n in &numbers { numbers.push(n * 2);}error[E0502]: cannot borrow `numbers` as mutable because it is also borrowed as immutable --> e_iter_mutate.rs:4:9 |3 | for n in &numbers { | -------- | | | immutable borrow occurs here | immutable borrow later used here4 | numbers.push(n * 2); | ^^^^^^^^^^^^^^^^^^^ mutable borrow occurs hereLa solución es la misma que en C#/Java — terminar de leer y luego escribir:
let mut numbers = vec![1, 2, 3];let doubled: Vec<i32> = numbers.iter().map(|n| n * 2).collect();numbers.extend(doubled);println!("{numbers:?}"); // [1, 2, 3, 2, 4, 6]Sin referencias colgantes
Sección titulada «Sin referencias colgantes»Una referencia nunca puede vivir más que aquello a lo que apunta:
fn longest_line() -> &str { let text = String::from("line one\nline two"); text.lines().next().unwrap()}error[E0106]: missing lifetime specifier --> e_dangling.rs:1:22 |1 | fn longest_line() -> &str { | ^ expected named lifetime parameter | = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from…help: instead, you are more likely to want to return an owned value |1 - fn longest_line() -> &str {1 + fn longest_line() -> String {text se libera cuando la función retorna, así que una referencia a su contenido quedaría colgando. Devuelve en su lugar un String en propiedad. (Los tiempos de vida (lifetimes) — la sintaxis 'a que menciona el error — tienen su propia lección, la 9.)
Un slice toma prestada una parte contigua de una colección sin copiarla:
let mut scores = vec![90, 72, 85];scores.push(60);let top_two = &scores[..2]; // &[i32]println!("top two: {top_two:?}"); // top two: [90, 72]Piensa en Span<T> / ReadOnlySpan<T> en C#, o en List.subList en Java — pero comprobado en tiempo de compilación, de modo que nunca puede vivir más que el vector.
String frente a &str
Sección titulada «String frente a &str»Este es el tropiezo más habitual, y los slices lo explican:
String |
&str |
|
|---|---|---|
| Qué es | un buffer UTF-8 en propiedad que puede crecer | un slice prestado de texto UTF-8 |
| Dónde viven los bytes | en el montón, propiedad de este valor | en cualquier parte: un String, el binario (literales), … |
| Puede crecer | sí (push_str, push) |
no |
| Analogía en C# | un StringBuilder que te pertenece |
ReadOnlySpan<char> / un string que no te pertenece |
| Uso típico | campos de structs, valores de retorno | parámetros de funciones |
- Los literales de cadena como
"hello"son&str(&'static str: viven en el binario). &Stringse convierte automáticamente en&str, así que los parámetros normalmente deberían ser&str— entonces aceptan ambos:
fn first_word(text: &str) -> &str { text.split_whitespace().next().unwrap_or("")}
println!("{}", word_count("a literal works too")); // 4println!("first word: {}", first_word(&title)); // first word: theLas cadenas son UTF-8, no arrays de caracteres
Sección titulada «Las cadenas son UTF-8, no arrays de caracteres»En C# y Java, s[0] / s.charAt(0) devuelve una unidad UTF-16. Rust se niega a indexar una cadena por posición:
let word = String::from("cafe");let c = word[0];error[E0277]: the type `str` cannot be indexed by `{integer}` --> e_index_str.rs:3:18 |3 | let c = word[0]; | ^ string indices are ranges of `usize` | = help: the trait `SliceIndex<str>` is not implemented for `{integer}` = note: you can use `.chars().nth()` or `.bytes().nth()`Como los caracteres ocupan de 1 a 4 bytes en UTF-8, «el n-ésimo carácter» es un recorrido O(n), y Rust lo hace explícito:
let word = "café";println!("{} bytes, {} chars, first 3 bytes: {}", word.len(), word.chars().count(), &word[..3]);// 5 bytes, 4 chars, first 3 bytes: cafCortar por un rango de bytes funciona, pero entra en pánico si cortas por la mitad de un carácter:
thread 'main' panicked at e_slice_boundary.rs:3:25:byte index 4 is not a char boundary; it is inside 'é' (bytes 3..5) of `café`Construir cadenas
Sección titulada «Construir cadenas»let mut log = String::new();for (i, s) in ["alpha", "beta"].iter().enumerate() { log.push_str(&format!("{i}:{s} "));}println!("{}", log.trim_end()); // 0:alpha 1:betaformat! funciona como string.Format / String.format (y como la interpolación de C#, con {name} dentro del literal).
Puntos clave
Sección titulada «Puntos clave»&Ttoma prestado para leer,&mut Ttoma prestado para escribir; el propietario conserva la propiedad.- Muchos préstamos compartidos o un único préstamo mutable — la regla que también detecta «colección modificada durante la iteración» en tiempo de compilación.
- Las referencias nunca pueden quedar colgando; devuelve valores en propiedad cuando los datos se crean dentro de una función.
- Recibe
&stren los parámetros y guardaStringen los structs; las cadenas son UTF-8, así que itera con.chars()en lugar de indexar.
Ejercicios
Sección titulada «Ejercicios»- Corrige la firma para que esto compile sin clonar, y explica por qué tu versión es más flexible:
fn is_shouting(text: String) -> bool { text.chars().any(|c| c.is_alphabetic()) && text == text.to_uppercase()}
let msg = String::from("HELLO");if is_shouting(msg) { println!("{msg} is shouting"); }Solución
fn is_shouting(text: &str) -> bool { text.chars().any(|c| c.is_alphabetic()) && text == text.to_uppercase()}
let msg = String::from("HELLO");if is_shouting(&msg) { println!("{msg} is shouting"); }Tomar prestado deja msg en propiedad de quien llama, y &str también acepta literales (is_shouting("hi")) y slices.
- Esto compila en C# y se ejecuta sin problemas. ¿Por qué Rust rechaza el equivalente y cómo lo corriges?
var names = new List<string> { "Ada" };var first = names[0];names.Add("Grace");Console.WriteLine(first);Solución
En C#, first guarda una referencia al objeto cadena, que el GC mantiene vivo aunque la lista se reasigne. En Rust, &names[0] apunta dentro del buffer del vector, que push puede reasignar, así que el borrow checker prohíbe la mutación mientras el préstamo siga vivo (E0502). Soluciones: usar first antes del push, o tomar una copia en propiedad con let first = names[0].clone();.
- Escribe
fn initials(full_name: &str) -> Stringque devuelva"A.L."para"Ada Lovelace", manejando correctamente los nombres que empiezan por letras no ASCII como"Émile Zola".
Solución
fn initials(full_name: &str) -> String { full_name .split_whitespace() .filter_map(|word| word.chars().next()) .map(|c| format!("{c}.")) .collect()}
assert_eq!(initials("Ada Lovelace"), "A.L.");assert_eq!(initials("Émile Zola"), "É.Z.");chars().next() toma el primer carácter, no el primer byte, así que É (2 bytes en UTF-8) se maneja correctamente.