Convertir y parsear texto: de y hacia String
Pasar de vista a dueño con to_string, String::from y to_owned; construir cadenas con format!; y convertir texto en tipos con parse y su Result. Por qué parsear devuelve un Result y cómo manejar el error en la frontera entre el mundo sin tipos y el mundo tipado.
El texto es la frontera de casi todo programa: entra por teclado, ficheros y red como una ristra de bytes sin estructura, y hay que convertirlo en los tipos fuertes con los que de verdad trabajas —números, fechas, direcciones— y viceversa. Rust trata ese cruce con dos herramientas simétricas: convertir de vista a dueño (to_string, String::from) para producir cadenas, y parsear (parse) para leer un tipo desde texto. Y como el texto de entrada nunca es de fiar, parsear devuelve un Result: el fallo es un valor que estás obligado a mirar.
- Convertir
&strenStringconto_string,String::fromyto_owned, y saber qué cuesta. - Construir cadenas con
format!,push_strypush. - Parsear texto a un tipo con
parse::<T>()y el turbofish. - Manejar el
Resultde un parseo con?,matchy combinadores.
De vista a dueño: tres caminos que copian
Convertir un &str en un String significa reservar un buffer propio y copiar los bytes: cruzas de mirar a poseer, y eso siempre cuesta una asignación en el heap. Hay varias funciones para el mismo salto, y conviene saber cuál dice mejor tu intención.
let vista: &str = "Ada";
let a = vista.to_string(); // vía el trait Display; la más habitual y legible
let b = String::from(vista); // conversión directa, sin pasar por formateo
let c = vista.to_owned(); // vía ToOwned: "dame la versión en propiedad de esta vista"
let d: String = vista.into(); // conversión genérica por el trait Into
Las cuatro producen el mismo String y todas reservan. to_string() es la más común y legible; String::from y to_owned() son marginalmente más directas para el caso &str a String porque no pasan por la maquinaria de Display. La diferencia es de estilo, no de resultado: elige la que deje más claro que estás tomando propiedad de un texto que solo mirabas.
to_string() no es exclusivo de las cadenas: lo tiene todo tipo que implemente Display. 42.to_string(), 3.14.to_string(), true.to_string() funcionan porque esos tipos saben describirse como texto. Es, de hecho, el inverso de parse: Display va del tipo al texto, FromStr va del texto al tipo. Los verás siempre como una pareja.
Construir cadenas: format! y compañía
Para fabricar texto nuevo, la herramienta central es format!: toma una plantilla con huecos y devuelve un String recién reservado, sin imprimir nada. Es println! que en vez de escribir en pantalla te devuelve la cadena.
let nombre = "Turing";
let anio = 1936;
let frase = format!("{nombre} publicó en {anio}"); // String nuevo: "Turing publicó en 1936"
let mut acc = String::new(); // String vacío, sin reservar aún
acc.push_str("hola"); // añade un &str al final
acc.push(' '); // añade un solo char
acc.push_str("mundo");
push_str añade una vista al final; push añade un único char. Para acumular en bucle, prefiere push_str sobre encadenar con +, porque el operador + sobre cadenas consume el String de la izquierda (toma su propiedad) y solo acepta un &str a la derecha, una asimetría que sorprende. Cuando compongas texto a partir de muchas piezas, format! o un String con push_str son más claros y a menudo más eficientes.
parse: de texto a tipo, con Result
La operación inversa —leer un valor tipado desde texto— es parse. Como el mismo texto podría convertirse a muchos tipos distintos, parse necesita saber a qué tipo apuntas, y hay dos formas de decírselo: el turbofish ::<T> o una anotación en la variable destino.
let por_turbofish = "42".parse::<i32>(); // Result<i32, ParseIntError>
let por_anotacion: Result<i32, _> = "42".parse();
let n: i32 = "42".parse().unwrap(); // 42, si estás seguro de que es válido
let x = "3.14".parse::<f64>().unwrap(); // 3.14
La clave está en el tipo de retorno: parse no devuelve el número, devuelve un Result<T, T::Err>. Porque "42" es un número pero "cuarenta y dos" no lo es, y esa diferencia solo se conoce en ejecución, examinando los bytes. El sistema de tipos, incapaz de saber de antemano si el texto será válido, te entrega el resultado envuelto en un Result y te obliga a contemplar el fracaso.
Detrás de parse está el trait FromStr: cualquier tipo que lo implemente se vuelve parseable, y parse no es más que azúcar sobre FromStr::from_str. Eso significa que tus propios tipos pueden leerse desde texto con la misma sintaxis que un i32, sin más que implementar el trait:
use std::str::FromStr;
struct Punto { x: i32, y: i32 }
impl FromStr for Punto {
type Err = String;
fn from_str(s: &str) -> Result<Punto, String> {
let (a, b) = s.split_once(',').ok_or("falta la coma")?;
let x = a.trim().parse().map_err(|_| "x inválida")?;
let y = b.trim().parse().map_err(|_| "y inválida")?;
Ok(Punto { x, y })
}
}
let p: Punto = "3, 4".parse().unwrap(); // usa tu from_str: misma sintaxis que un número
Manejar el error de parseo
Nunca trates el Result de parse con un unwrap reflejo salvo en un prototipo: un unwrap sobre una entrada malformada es un panic que tumba el programa. La forma robusta es propagar con ?, ramificar con match, o dar un valor por defecto con un combinador.
use std::num::ParseIntError;
fn duplicar(texto: &str) -> Result<i32, ParseIntError> {
let n: i32 = texto.parse()?; // si falla, `?` devuelve el Err y sale de la función
Ok(n * 2)
}
fn main() {
match duplicar("21") {
Ok(v) => println!("resultado: {v}"), // resultado: 42
Err(e) => println!("entrada inválida: {e}"),
}
let puerto: u16 = "no-es-numero".parse().unwrap_or(8080); // valor por defecto ante el fallo
println!("puerto: {puerto}"); // 8080
}
El operador ? es el idioma dominante: cuando tu propia función devuelve Result, ? extrae el Ok o corta la ejecución devolviendo el Err al llamador, encadenando parseos sin una pirámide de match. Y cuando un fallo tiene una respuesta sensata —un puerto por defecto, un cero— los combinadores unwrap_or, unwrap_or_else o unwrap_or_default la expresan en una línea sin desviar el flujo.
Cuando propagas un error de parseo a través de una función cuyo tipo de error es distinto, map_err traduce en el sitio: convierte el ParseIntError concreto en tu tipo de error antes de que ? lo devuelva. Y para transformar el valor bueno sin ramificar, los combinadores de Result —map, and_then, ok— encadenan sobre el Ok dejando el Err intacto.
fn edad_valida(texto: &str) -> Result<u8, String> {
texto.parse::<u8>()
.map_err(|e| format!("no es una edad: {e}")) // traduce el error concreto
.and_then(|n| if n < 150 { Ok(n) } else { Err("fuera de rango".into()) })
}
Todo programa tiene una frontera: por un lado, el exterior —ficheros, red, argumentos, teclado— habla en el único idioma universal que existe, bytes y texto, sin tipos ni garantías; por el otro, tu lógica quiere trabajar con u16, IpAddr, DateTime, tipos fuertes cuyos valores son todos válidos por construcción. parse es la aduana entre ambos mundos, y que devuelva Result es la consecuencia inevitable de que el exterior no es de fiar: un &str que dice ser un número podría ser cualquier cosa, y el único momento honesto para descubrirlo es al cruzar la frontera. De aquí sale una de las máximas de diseño más valiosas de Rust, “parse, don’t validate”: en lugar de arrastrar un String por todo el programa comprobando una y otra vez ¿será un número válido?, lo conviertes una sola vez, en el borde, a un tipo que ya no puede ser inválido, y a partir de ahí el sistema de tipos garantiza que nadie manejará un puerto que no quepa en un u16 ni una fecha imposible. El parseo concentra toda la incertidumbre en un punto —la aduana— y devuelve, del otro lado, valores en los que el compilador puede confiar. Así, el Result que parse te obliga a manejar no es una molestia burocrática: es el lenguaje forzándote a decidir, en el único sitio donde puedes, qué hacer cuando el mundo exterior no cumple lo que prometía. Empujar esa decisión al borde, y no diseminarla por dentro, es lo que separa un programa robusto de uno que se cae con la primera entrada rara.
De vista a dueño (to_string, String::from, to_owned) siempre reserva y copia: cruzar de mirar a poseer cuesta una asignación. format! fabrica un String nuevo a partir de una plantilla. parse::<T>() hace el camino inverso, de texto a tipo, y devuelve Result porque la entrada podría no ser válida. Maneja ese Result con ? para propagar, match para ramificar o unwrap_or para un valor por defecto, y parsea en el borde del programa para no arrastrar texto sin validar por dentro.
- Convierte
"hola"aStringde tres formas distintas y explica qué tienen en común en el heap. - Escribe
fn etiqueta(id: u32) -> Stringconformat!y comprueba que devuelve propiedad, no una vista. - Parsea
"255"au8y luego"256"au8; observa que el segundo devuelveErrpor desbordamiento. - Escribe una función que sume dos números venidos como
&strusando?, y decide su tipo de retorno. - Usa
unwrap_oryunwrap_or_elsepara dar un valor por defecto a un parseo que falla, y razona cuándo prefieres cada uno.