Ir al contenido

4. Préstamos y cadenas

Ejemplo completo: examples/l04_borrowing.rscargo run --example l04_borrowing.

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 words
println!("{title}"); // title sigue siendo utilizable

Una referencia toma prestado el valor: el propietario conserva la propiedad, y el préstamo debe terminar antes de que el propietario desaparezca.

Sintaxis Cuántas a la vez Puede modificar
Referencia compartida &T cualquier cantidad no
Referencia mutable &mut T exactamente una, y ninguna compartida
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.

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 here
4 | names.push(String::from("Grace"));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
5 | println!("{first}");
| ----- immutable borrow later used here

No 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.

// Java
for (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 here
4 | numbers.push(n * 2);
| ^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here

La 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]

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.

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).
  • &String se 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")); // 4
println!("first word: {}", first_word(&title)); // first word: the

Las 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: caf

Cortar 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é`
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:beta

format! funciona como string.Format / String.format (y como la interpolación de C#, con {name} dentro del literal).

  • &T toma prestado para leer, &mut T toma 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 &str en los parámetros y guarda String en los structs; las cadenas son UTF-8, así que itera con .chars() en lugar de indexar.
  1. 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.

  1. 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();.

  1. Escribe fn initials(full_name: &str) -> String que 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.