Strings en Rust – Aprende Rust 12/x

strings en rust

Hasta ahora hemos trabajado con Strings y lo hemos mencionado muy por encima, tal vez donde más relevancia ha tomado es en el caso de ownership. Ahora veremos un poco más en detalle los Strings en Rust.

Crear un string en Rust

En el capítulo de vectores vimos el tipo Vec<T>, y este tenía la característica de que guardaba un valor junto a otro en memoria, bueno pues String también lo hace y además agrega más opciones.

let mut s = String::new();

Esto crearía un String vacío.

Podemos usar el método to_string() para obtener el valor o usar directamente cuando creamos un String con un valor predeterminado:

let data = "initial contents";
println!("{data}");

let s = data.to_string();
println!("{s}");

let s = "initial contents".to_string();
println!("{s}");

También podemos usar lo que ya hemos visto:

let s = String::from("initial contents");

Básicamente String::from("...) y to_string hacen lo mismo.

Todos los strings en Rust son codificados en utf-8, por lo que puedes escribir directamente los caracteres o emojis 😀:

let hello = String::from("السلام عليكم");
let hello = String::from("Dobrý den");
let hello = String::from("Hello");
let hello = String::from("שָׁלוֹם");
let hello = String::from("नमस्ते");
let hello = String::from("こんにちは");
let hello = String::from("안녕하세요");
let hello = String::from("你好");
let hello = String::from("Olá");
let hello = String::from("Здравствуйте");
let hello = String::from("Hola 🥳");

Modificar un string en Rust

Dijimos que String usar algo como Vec<T>, y en este tipo de dato podemos agregar elementos al vector mediante el método push. En los strings usaremos push_str.

let mut s = String::from("foo");
s.push_str("bar");
println!("{s}");

La salida de esto será foobar. OJO, siempre respetando la regla del cambio. Usamos mut porque s va a cambiar.

Existe también el método push, solo que este en lugar de agregar una cadena, permite agregar solo un caracter:

let mut s = String::from("gatito");
s.push('s');
println!("{s}");

Concatenar strings en Rust

Lo primero que se nos ocurre acá es usar el operador + para “pegar” dos cadenas y pues si, lo podemos usar:

let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2;

El resultado sería el esperado: Hello, world!.

Recordemos que por temas de ownership, s1 estaría siendo “tomado” en s3, pero s2 seguiría siendo válido, porque le estamos pasando su referencia.

¿Qué pasa si queremos modificar la línea de s3 con esto: let s3 = s1 + s2? Es decir, queremos juntar dos String. Fallará horriblemente.

Esto es porque el operador + usa el método add, cuya declaración es así:

fn add(self, s: &str) -> String {

Esta función se recibe a sí mismo y un parámetro &str.

Primero, en el ejemplo que funciona, estamos agregando una referencia de &s2 a s1. Pero debemos notar que &s2 no es &str sino &String. ¿Por qué funciona? Rust tiene algo que se llama deref coercion que básicamente convierte tipos que implementan un trait llamado Deref, no entraremos en detalle, pero esto permite que en este ámbito &String pueda convertirse a &str que es el que necesitamos en la función add.

Luego, tenemos que add recibe self no &self, o sea, no toma la referencia, sino que se hace con el ownership de s1. Es por ello que en nuestro ejemplo, la parte de let s3 = s1 + s2, la variable s1 se envía sin & y por esa misma razón s1 ya no es válida después de esa línea. Después de eso, podemos enviar cualquier cantidad de &str o &String, es decir, todos deberán ser referencias. No puedes hacer lo siguiente:

let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");

let s = s1 + "-" + s2 + "-" + &s3;
println!("{s}");

El error:

let s = s1 + "-" + s2 + "-" + &s3;
                   ^^ expected `&str`, found `String`

El compilador nos indica que el tipo que le enviamos como segundo parámetro no es el correcto.

Si hacemos esto, sí funciona:

let s = s1 + "-" + &(s2 + "-" + &s3);

Es porque lo que está entre paréntesis se está evaluando primero y toma las mismas reglas, es decir, el primer operador es el “valor” y el segundo es una referencia. Incluso sigue las mismas reglas de ownership, porque si queremos imprimir el valor de s1 o s2, el compilador nos tira el error error[E0382]: borrow of moved value: s2. En este punto las dos variables no serían válidas.

Rust con esto quiere hacer eficiente el uso de memoria. ¿Cómo? Pues cuando tomamos el ownership, estamos “matando” la variable anterior y no necesitamos una copia completa de su valor, simplemente agregamos en esa misma posición de memoria, más datos.

Indexes en Strings en Rust

En algunos lenguajes, puedes acceder a cada elemento de una cadena con una notación parecida a los arrays, es decir:

let s1 = String::from("hello");
let h = s1[0];

El error:

`String` cannot be indexed by `{integer}`

Rust no soporta esto.

En Rust, un String es una representación de Vec<u8>.

let s1 = String::from("hello");
println!("{}", s1.len());

Aquí el resultado será obviamente 5, porque la longitud de la cadena en bytes es de 5 bytes, o sea, cada letra “pesa” 1 byte.

let s1 = String::from("Здравствуйте");
println!("{}", s1.len());

Aquí es resultado podría ser 12, pero no, es 24. Esto es porque, como ya dijimos, la codificación de las cadenas en Rust es utf-8, por lo que codificar esos caracteres en ese formato nos toma 24 bytes, cada una de esas letras ocupado 2 bytes de espacio.

¿Y entonces? Pues no, no se puede, Rust no lo permite. Recordemos que al ser un vector, estos guardan sus elementos en memoria de forma continua, y como algunos caracteres necesitan más de un byte, es posible que el resultado no sea el esperado.

Y sin embargo, podemos iterar sobre los propios caracteres o sobre los bytes, por ejemplo:

fn main() {
    for c in "Зд".chars() {
        println!("{c}");
    }

    for b in "Зд".bytes() {
        println!("{b}");
    }
}

En nuestro primer for, el resultado es que imprimirá З y д, mientras que en el segundo el resultado es:

208
151
208
180

O sea, el З necesita el 208y 151 para guardarse correctamente, si tomaras cualquiera de ellos, para Rust no es válido por sí mismo, es por ello que los resultados, haciendo s1[0] el resultado serían inciertos.

Con esto creo que podemos finalizar los Strings en Rust, son algo complejos, la verdad.

Para entender más de colecciones y Strings en Rust puedes ir acá.

Gracias por leer.


Posted

in

, , ,

by