rust-strings-string-vs-str

Strings en Rust: diferencias clave entre String y &str

  • 6 min

Las cadenas en Rust son secuencias de texto UTF-8 representadas como String propietario o &str prestado.

Si vienes de JavaScript, Python o C#, esto te va a doler un poco al principio.

En esos lenguajes, solo hay un tipo “String”. Haces s = "hola", luego s += " mundo", y si quieres la primera letra haces s[0]. Fácil.

En Rust, intenta hacer eso y el compilador te lanzará tres errores distintos. Rust tiene dos tipos principales de cadenas: String y &str. Y para colmo, no te deja acceder a las letras por índice.

¿Por qué Rust complica tanto algo tan básico? La respuesta corta es: Gestión de Memoria y UTF-8. Hoy vamos a entenderlo de una vez por todas.

Los dos tipos principales: String y &str

Para entender la diferencia, tenemos que pensar en Ownership (Propiedad) y memoria.

String: el dueño

  • Dónde vive: Los metadatos en el Stack, el contenido en el Heap.
  • Propiedad: Es dueño de sus datos (Owner).
  • Capacidad: Puede crecer y encogerse cuando disponemos de acceso mutable.
  • Coste: Crear contenido propio suele requerir reservar memoria mediante el asignador y copiar bytes.

Es el equivalente a un StringBuilder en Java o un std::string en C++. Es un vector de bytes (Vec<u8>) disfrazado.

// Creamos un String vacío que puede crecer
let mut s = String::new();
s.push_str("Hola");
s.push('!');
Copied!

&str: una vista prestada

  • Dónde vive: Es una referencia (&) a algún lugar (puede ser al Heap, al Stack o a la memoria estática del ejecutable).
  • Propiedad: No es dueño, solo pide prestado (Borrowed).
  • Capacidad: Tamaño fijo. No puedes añadirle texto.
  • Coste: Copiar la propia referencia es barato: su representación contiene un puntero y una longitud.

Se le conoce como String Slice. Es una ventana para mirar texto que pertenece a otro.

La analogía de la foto

  • String es la casa original. Puedes pintar las paredes, construir una habitación extra o demolerla. Tú tienes las escrituras.
  • &str es una ventana desde la que mirar una parte de la casa. No puedes reformarla desde esa referencia, y Rust impide que la ventana sobreviva a la casa.

¿Cuándo usar cuál?

La recomendación práctica para tus funciones es esta:

  • Si necesitas guardar el texto en un Struct para usarlo más tarde: Usa String.
  • Si solo necesitas leer el texto para analizarlo o imprimirlo: Usa &str.
// ✅ Bien: Aceptamos &str, así nos pueden pasar Strings y literales
fn saludar(nombre: &str) {
    println!("Hola, {}", nombre);
}

fn main() {
    let nombre_string = String::from("Luis");
    let nombre_literal = "Juan";

    saludar(&nombre_string); // Rust convierte &String a &str automáticamente
    saludar(nombre_literal);
}
Copied!

Conversiones

Pasar de uno a otro es el pan de cada día en Rust.

De &str a String

Cuando tienes una referencia y quieres ser el dueño para modificarla o guardarla, debes reservar memoria y copiar los bytes.

let slice = "hola";
let s1 = slice.to_string(); // Opción A (Más común)
let s2 = String::from(slice); // Opción B
let s3 = slice.to_owned(); // Opción C (Idiomática si vienes de Cow)
Copied!

De String a &str

Es muy barato.

let s = String::from("hola");
let slice = &s; // Deref coercion: &String se comporta como &str
let slice2 = &s[..]; // Slice explícito de todo el string
Copied!

El problema de s[0] y UTF-8

Intenta compilar esto:

let s = String::from("Hola");
let letra = s[0]; // ❌ Error: `String` cannot be indexed by `{integer}`
Copied!

¿Por qué Rust no me deja leer la primera letra? ¿Acaso no es un array de caracteres?

No. Un String es un búfer de bytes (u8) que siempre contiene UTF-8 válido, no un array donde cada posición equivalga a un carácter.

En UTF-8, un carácter puede ocupar entre 1 y 4 bytes.

  • 'a' ocupa 1 byte.
  • 'ñ' ocupa 2 bytes.
  • '€' ocupa 3 bytes.
  • '😀' ocupa 4 bytes.

Si Rust te dejara hacer s[0] en la palabra "España", todo iría bien. Pero si lo haces en "Ñu", s[0] te devolvería la mitad de la Ñ, lo cual no es un carácter válido. Rust prefiere no compilar a darte basura.

¿Cómo iterar entonces?

Si quieres acceder a los caracteres, debes decirle a Rust explícitamente cómo quieres interpretar los bytes.

Como bytes (.bytes()) Si quieres los datos crudos.

for b in "Ñu".bytes() {
    println!("{}", b); // Imprime 195, 145, 117
}
Copied!

Como valores Unicode (.chars()) Si quieres recorrer valores escalares Unicode.

for c in "Ñu".chars() {
    println!("{}", c); // Imprime Ñ, u
}
Copied!

.chars() no devuelve siempre lo que una persona percibe como una letra completa. Por ejemplo, un grafema con marcas combinadas puede contener varios valores Unicode. Para segmentar grafemas hacen falta herramientas específicas.

Concatenación de Strings

Unir cadenas también tiene su truco por culpa del Ownership.

Usando el operador +

let s1 = String::from("Hola, ");
let s2 = String::from("mundo!");

// Nota: s1 se MUEVE y deja de existir. s2 se pasa por referencia.
let s3 = s1 + &s2;
Copied!

El operador + en realidad llama a un método add(self, s: &str). Toma posesión del primero, le añade el contenido del segundo (sin copiar el segundo, solo leyéndolo) y devuelve el resultado. Es eficiente, pero hace que s1 muera.

Usando la macro format!

Si quieres concatenar sin perder variables y de forma más legible, usa format!. Funciona igual que println! pero devuelve un String en lugar de imprimirlo.

let s1 = String::from("Tic");
let s2 = String::from("Tac");

let s3 = format!("{}-{}", s1, s2); // Crea un String nuevo. s1 y s2 siguen vivos.
Copied!

Esta opción crea un String nuevo y suele ser más fácil de leer al combinar varias partes. Su coste exacto depende de las cadenas y de la capacidad que haya que reservar.