Cadenas de caracteres

Puedes ver este capítulo en YouTube en inglés

Rust tiene dos tipos de cadenas de caracteres: String y &str. ¿Cuál es la diferencia?

  • &str es una cadena de caracteres simple que reside en la pila. Cuando se escribe let mi_variable = "¡Hola, mundo!" se crea una &str. Este tipo es muy rápido.
  • String es un tipo de dato más complejo. Es un poco más lento, pero tiene más funciones. Una String es un puntero que almacena los datos en la memoria dinámica.

Hay que destacar que &str tiene & como primer carácter debido a que es necesaria una referencia para utilizar el tipo str. Esto es por la razón que vimos anteriormente: la pila necesita conocer el tamaño, así que se le da una referencia, las referencias siempre tienen el mismo tamaño. Otro tema a tener en cuenta es que al utilizar & una referencia para interactuar con el tipo str, nunca se es dueño del tipo. Por el contrario, Stringes un tipo con dueño. Más adelante se mostrará la importancia de esta distinción.

Ambos tipos, &str y String son UTF-8. Por ejemplo, se puede escribir el siguiente código:

fn main() {
    // Nombre en coreano. No da problemas, ya que &str es UTF-8
    let nombre = "서태지";
    // Ț y ș no son un problema en UTF-8.
    let otro_nombre = String::from("Adrian Fahrenheit Țepeș");
}

En el código anterior se observa que se puede construir de forma fácil una String de una &str. Los dos tipos están muy relacionados, aunque son muy diferentes.

Gracias a UTF-8, incluso se pueden escribir emojis.

fn main() {
    let nombre = "😂";
    println!("Mi nombre real es {}", nombre);
}

Si se ejecuta este código en el terminal de comandos de tu propio ordenador tiene que aparecer Mi nombre real es 😂 a menos el terminal de comandos presente limitaciones y no lo pueda imprimir. En cuyo caso imprimirá algo así Mi nombre real es �. En todo caso, Rust es capaz de manejar todos los caracteres Unicode.

La razón para utilizar una referencia &para el tipo str es que str es un tipo de datos de tamaño dinámico, su tamaño puede ser diferente. Por ejmplo, los nombres "서태지" y "Adrian Fahrenheit Țepeș" no son del mismo tamaño:

fn main() {

    // std::mem::size_of::<Type>() devuelve el tamaño en bytes de un tipo
    println!("Una String siempre ocupa {:?} bytes. Es de tamaño fijo.",
        std::mem::size_of::<String>()); 
    println!("Y un i8 siempre ocupa {:?} bytes. Es de tamaño fijo.", 
        std::mem::size_of::<i8>());
    println!("Y un f64 siempre ocupa {:?} bytes. Es de tamaño fijo.", 
        std::mem::size_of::<f64>());
     // std::mem::size_of_val() devuelve el tamaño en bytes de una variable
    println!("¿Y un &str? Puede ocupar cualquier tamaño. '서태지' ocupa {:?} bytes. No es de tamaño fijo.",
        std::mem::size_of_val("서태지"));
    println!("Y 'Adrian Fahrenheit Țepeș' ocupa {:?} bytes. No es de tamaño fijo.",
        std::mem::size_of_val("Adrian Fahrenheit Țepeș"));
}

Lo que da como resultado:

Una String siempre ocupa 24 bytes. Es de tamaño fijo.
Y un i8 siempre ocupa 1 bytes. Es de tamaño fijo.
Y un f64 siempre ocupa 8 bytes. Es de tamaño fijo.
¿Y un &str? Puede ocupar cualquier tamaño. '서태지' ocupa 9 bytes. No es de tamaño fijo.
Y 'Adrian Fahrenheit Țepeș' ocupa 25 bytes. No es de tamaño fijo.

Por eso es necesario usar &, porque así se construye un puntero (tipo de tamaño fijo) que puede almacenarse en la pila. Si se escribiera str, Rust no sabría qué hacer al no conocer su tamaño.

Hay muchas formas de construir un elemento de tipo String. Algunas de ellas son:

  • String::from("Esta es una cadena de texto"); - String::from() es un método de Strgin que crea un String a partir de una cadena de texto.
  • "Esta es una cadena de texto".to_string() - "".to_string() es un método de &str que crea un String.
  • La macro format! - Es como println! excepto que crea un String en lugar de imprimir el texto.

A continuación se muestran algunos ejemplos:

fn main() {
    let my_name = "Billybrobby";
    let my_country = "USA";
    let my_home = "Korea";

    let together = format!(
        "Soy {} y vengo de {}, pero vivo en {}.",
        my_name, my_country, my_home
    );
}

Así se construye un objeto String denominado together, pero no se ha impreso.

Otra forma adicional para crear un String es con la función into(), pero esta forma es algo diferente ya que no solo sirve para crear String. Algunos tipos se pueden convertir de forma fácil en otros utilizando from y into(). Si el tipo tiene from, tiene también into(). from resulta más claro ya que con él conoces los tipos: al usarlo String::from("Cadena de texto") se sabe que se crea una String de &str. Sin embargo, con .into() el compilador, a veces, no lo conoce:

fn main() {
    let my_string = "Intento construir un String".into(); // ⚠️
}

Rust no conoce el tipo al que se quiere convertir la cadena de texto, porque se pueden crear muchos tipos diferentes a partir de un &str. Rust se queja: puedo convertir &str en muchos tipos diferentes. ¿Cuál es el que quieres?

error[E0282]: type annotations needed
 --> src/main.rs:2:9
  |
2 |     let my_string = "Intento construir un String".into(); // ⚠️
  |         ^^^^^^^^^ consider giving `my_string` a type

Por lo que se puede corregir así:

fn main() {
    let my_string: String = "Intento construir un String".into();
}

Y ahora sí se ha creado un objeto String.