Actualizaciones

example workflow name

23 de mayo de 2021: Ahora disponible en indonesio gracias Ariandy/1kb.

2 de abril de 2021: Añadido enlace a BuyMeACoffee para aquellos a los que les gustaría invitarme a un café.

1 de febrero de 2021: ¡Ahora disponible en youtube! (en inglés) Dos meses después: completo el 1 de abril de 2021, 186 vídeos en total (un poco más de 23 horas).

22 de diciembre 2020: el libro en mdBook se puede encontrar aquí.

28 de noviembre de 2020: Ahora también disponible en chino simplificado ¡Gracias a kumakichi!

Introducción

Rust es un nuevo lenguaje de programación que ya tiene buenos libros de texto, pero a veces son difíciles ya que son para personas cuyo inglés es nativo. Muchas empresas y personas están aprendiendo Rust en estos días y pueden aprenderlo más rápido con un libro que use un inglés fácil1. Este libro es para que estas empresas y personas puedan aprender Rust con un español simple.

Rust es un lenguaje que bastante nuevo, pero ya es muy popular. Es popular porque te da la velocidad y control de C o C++, pero también ofrece protección del acceso a la memoria como otros lenguajes como Python. Esto lo hace usando nuevas ideas que son en parte diferentes a otros lenguajes. Lo que significa que hay que aprender cosas nuevas y no puedes simplemente "descubrirlo según vas avanzando". Rust es un lenguaje en el que tienes que pensar las cosas durante un tiempo para comprenderlo, pero que aún suena a familiar si conoces otros lenguajes y está diseñado para ayudarte a escribir código de calidad.

1

N.T.: esta traducción al español trata de seguir el mismo principio utilizando un español fácil.

¿Quién soy?

Soy un canadiense que vive en Korea y escribí Rust Fácil pensando en cómo hacer más fácil su uso para las empresas de aquí1. Espero que otras empresas que no usan el inglés como primer idioma puedan utilizarlo también.

1

N.T.: Soy un español que vive en Madrid, espero que esta traducción también facilite la comprensión de este lenguaje que tiene aspectos novedosos incluso para los que conocen C, C++, Java, Python, Ruby, Javascript o Typescript...

La escritura de Rust en inglés fácil

Rust en inglés fácil se escribió de julio a agosto de 2020 y ocupa unas 400 páginas. Puedes contactarme aquí o en LinkedIn o en Twitter si tienes cualquier pregunta. Si ves algo erróneo o tienes una petición de inserción (pull request) que hacer, adelante. Más de 20 personas ya han ayudado a corregir erratas y problemas en el código, por lo que tú también puedes[1^]. no soy el mejor experto de Rust del mundo, así que me gusta escuchar nuevas ideas o ver en qué puedo hacer el libro mejor.

1

N.T.: Esta traducción, Rust en español fácil, se escribió de julio a septiembre de 2021. Para cualquier asunto relacionado con ella, puedes contactarme en github o en LinkedIn.

Parte 1 - Rust en tu navegador

Este libro tiene dos partes. En la primera, aprenderás todo solo utilizando tu navegador. Puedes aprender casi todo lo que debes saber sin instalar Rust. Por eso esta primera parte es muy larga. En la segunda parte, mucho más corta, se habla de Rust en tu ordenador. Ahí aprenderás todo lo que necesitas conocer y que solo se puede hacer fuera del navegador. Algunos ejemplos son: trabajar con ficheros, obtener datos del usuario, gráficos y configuraciones personales. Espero que al finalizar la primera parte te guste Rust lo suficiente para que lo instales en tu equipo. Y si no, no hay problema, esta primera parte de enseña tanto que no te importará.

El entorno de pruebas (Playground) de Rust

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

Puede que no quieras instalarte aún Rust. No pasa nada. Puedes ir a https://play.rust-lang.org/ y comenzar a escribir Rust sin salir del navegador. Puedes escribir ahí el código y pulsar Run para ver los resultados. Puedes ejecutar la mayoría de los ejemplos de este libro en este entorno. Solamente al final del libro verás ejemplos que van más allá de lo que se puede hacer aquí (como abrir ficheros.)

Algunas recomendaciones de uso de este entorno de pruebas (Playground):

  • Ejecuta el código con Run
  • Cambia de Debug a Release si quieres que el código sea más rápdio. Debug (Depuración) hace que compile más rápido, se ejecute más lento y contenga información para la depuración de errores. Release (Producción) hace que compile más lento, se ejecute más rápido y se suprima la información para la depuración de errores.
  • Pulsa en Share (Compartir) para obtener un hiperenlace. Puedes usarlo para compartir tu código si necesitas ayuda. Después de pulsar compartir, puedes pulsar en Open a new thread in de Rust user forum(Abre un nuevo hilo en el foro de usuarios de Rust) para pedir ayuda.
  • Tools (Herramientas): Rustfmt formatea el código correctamente.
  • Tools (Herramientas): Clippy da información extra sobre cómo hacer mejor el código.
  • Config (Configuración): puedes cambiar aquí el tema a modo oscuro para que puedas trabajar mejor de noche y otras muchas opciones.

Si quieres instalar Rust, ve a https://www.rust-lang.org/tools/install y sigue las instrucciones. Normalmente, usarás rustup para instalar y actualizar Rust.

🚧 y ⚠️

En ocasiones, el código de los ejemplos del libro no funciona. Si un ejemplo no funciona, veras un 🚧 o un ⚠️ junto a él. 🚧 significa "en construcción", es decir, que el código no está completo. Rust necesita una fn main() (función principal) para ejecutarse, pero algunas veces solo queremos observar una parte del código por lo que no aparecerá el código fn main (). Estos ejemplos son correctos, pero necesitan de una función fn main() para ejecutarse. En algunos ejemplos el código mostrará un problema que tendrás que resolver. Estos ejemplos podrían tener una función fn main() pero generarán un error y por eso se acompañan de un símbolo ⚠️.

Comentarios

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

Los comentarios son para que los lean los programadores, no el ordenador. es bueno escribir comentarios para ayudar a otras personas a entender tu código. Es bueno, también, para ayudarte a entender tu propio código pasado un tiempo (Muchas personas escriben buen código, pero olvidan porqué lo escribieron). Para escribir comentarios en el código, en Rust se suele usar //:

fn main() {
    // Los programas Rust comienzan en fn main()
    // Pones el código en un bloque que comienza con { y termina con }
    let algun_numero = 100; // Aquí podemos esribir todo lo que queramos ya que el compilador no lo va mirar
}

Cuando lo haces así, el compilador no mirará nada que haya a la derecha de //.

Existe otra clase de comentario que se escribe con /* para iniciarlo y se termina con `*/. Este tipo de comentario es útil para escribirlo entre el código.

fn main() {
    let algun_numero/*: i16*/ = 100;
}

Para el compilador let algun_numero/*: i16*/ = 100; es igual que let algun_numero = 100;.

Esta forma /* */ también es útil para los comentarios de gran longitud que necesiten más de una línea. En este ejemplo se puede ver que necesitas escribir // en cada línea, pero si utilizas /*, el comentario dura hasta que lo finalices con */.

fn main() {
    let algun_numero = 100; /* Déjame contarte
    un poco sobre este número.
    Es el 100, que es mi número favorito.
    Se llama algun_numero pero realmente creo que...*/

    let algun_numero = 100; // Déjame contarte
    // un poco sobre este número.
    // Es el 100, que es mi número favorito.
    // Se llama algun_numero pero realmente creo que...
}

Tipos de dato

Rust tiene muchos tipos de dato que permiten trabajar con números, caracteres y otros. Algunos son simples, otros son más complicados e, incluso, puedes crear tus propios tipos de dato.

Tipos de dato primitivos

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

Rust tiene tipos simples que llaman tipos primitivos (primitivo = muy básico). Comenzaremos con los números enteros y los char (caracteres). Los enteros son números sin coma decimal. Existen dos tipos de enteros:

  • Enteros con signo.
  • Enteros sin signo.

"Con signo" significa que disponen de + (signo más) y - (signo menos), por lo que los enteros con signo pueden ser positivos o negativos (por ejemplo, +8, -8). Por el contratio, los enteros sin signo solo pueden ser positivos ya que no tienen signo.

Los enteros con signo son: i8, i16, i32, i64, i128, e isize.

Los enteros sin signo son: u8, u16, u32, u64, u128, e usize.

El número tras la i o la u indica el número de bits que se usan para el entero. Así, los números con más bits pueden ser mayores. 8 bits = un byte por lo que i8 ocupa un byte, y puede contener valores entre el -128 y el 127. Por lo tanto i64 ocupa 64 bits o, lo que es lo mismo, 8 bytes y puede representar números entre el -9223372036854775808 y el 9223372036854775807.

Los tipos numéricos con mayor tamaño pueden representar valores mayores. Por ejemplo, el tipo u8puede representar del 0 al 255, el tipo u16 puede representar del 0 al 65635, y el tipo u128 puede representar a un número entre el 0 y el 340282366920938463463374607431768211455.

¿Y qué representan isize y usize? El número de bits del tipo nativo del procesador de tu ordenador (El número de bits nativo de tu procesador se denomina la arquitectura de tu procesador). Así que isize y usize en un ordenador de 32-bits son equivalentes a i32 y u32. En un ordenador de 64-bits son equivalentes a i64 y u64.

Hay muchas razones para disponer de todos estos tipos de números enteros. Una razón es el rendimiento del ordenador: es más rápido procesar un número menor de bytes. Por ejemplo, el número -10 representado como un i8 es 11110110, pero como un i128 es 11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110110.

veamos otros usos:

Los caracters en Rust se denominan char. Todo char se representa por un número: la letra A es lel número 65, mientras que el carácter ("amigo" en chino) es el número 21451. La lista de estos números que representan a carateres se denomina "Unicode". Unicode usa números más pequeños para los caracteres que se usan más, como los de la A a la Z, los dígitos de 0 a 9 o el espacio.

fn main() {
    let primera_letra = 'A';
    let espacio = ' '; // Un espacio entre ' ' también es un char
    let char_en_otro_idioma = 'Ꮔ'; // Gracias a Unicode, otros lenguajes, como el Cherokee, también se visualizan bien
    let cara_gato = '😺'; // Emojis también son char
}

Los caracters que se utilizan más se representan or números menores al 256 y así pueden caber en un u8. Recuerda que un u8 permite números entre el 0 y el 255, lo que facilita la representación de 256 caracteres en total. Esto significa que Rust puede convertir de forma segura del tipo de datos u8 al char, utilizando la palabra reservada del lenguaje as (Considera el dato de este tipo u8 como si fuese char).

La conversión de tipos de datos utilizando as es muy útil ya que Rust es muy estrict. Siempre necesita conocer el tipo de dato y, además, no deja utilizar de forma conjunta dos tipos de datos diferentes incluso aunque sean de la misma familia (como los enteros). Por ejemplo, este código no funcionará:

fn main() { // main() es la función a partir de la que se 
            // inician los programas Rust. El código entre las llaves {}

    let mi_numero = 100; // No hemos indicado el tipo de datos entero
                         // que Rust debe utilizar,
                         // así que Rust elige i32. Rust siempre
                         // elige i32 para los enteros si no se le indica
                         // que utilice otro tipo de datos diferente

    println!("{}", mi_numero as char); // ⚠️
}

Esta es la razón:

error[E0604]: only `u8` can be cast as `char`, not `i32`
 --> src\main.rs:10:20
  |
3 |     println!("{}", mi_numero as char);
  |                    ^^^^^^^^^^^^^^^^^ invalid cast

Afortunadamente, podemos corregir esto fácilmente con as. No podemos convertir un i32 a char, pero podemos convertir un i32 a u8. Y después, podemos realizar la conversión de u8 a char. Po tanto, en una línea usamos as para convertir mi_numero a u8 y después lo convertimos a char. Ahora sí compilará:

fn main() { 
    let mi_numero = 100; 
    println!("{}", mi_numero as u8 as char);
}

Se imprime d porque es el char que está representado en Unicode con este número.

Existe una forma más fácil, sin embargo, de conseguir este resultado: indicarle a Rust que mi_numero es de tipo u8. Así:

fn main() { 
    let mi_numero: u8 = 100; // se indica de forma expresa que
                             // el tipo de la variable mi_numero es u8
    println!("{}", mi_numero as char);
}

Las anteriores, son dos de las razones para la existencia de todos estos tipos de datos numéricos en Rust. Hay otra razón más: usize es el tamaño que Rust utiliza para indexar (Indexar significa "conocer cual elemento va primero", "cual va segundo", etc.). usize es el mejor tamaño para el indexado porque:

  • Un índice no puede ser negativo, por lo que tiene que ser uno de los tipos de dato con una u.
  • Debe ser grande porque en muchas ocasiones necesitas indexar muchas cosas, pero
  • No puede ser un u64 porque los ordenadores de 32-bits no lo pueden manejar.

Por eso Rust usa usize para indexar y facilitar que tu ordenador pueda utilizar el tipo de datos mayor de que disponga.

Vamos a aprender algo más sobre char. Ya vimos que charsiempre es un carácter y utiliza '' en lugar de "".

Todos los char usan 4 bytes de memoria, puesto que son necesarios 4 bytes para contener cualquier clase de carácter:

  • Las letras y símbolos básicos suelen necesitar solo 1 de los 4 bytes: a b 1 2 + - = $ @
  • Otras letras como las diéresis y tildes necesitan 2 de los 4 bytes: ä ö ü ß è é à ñ
  • Los caracteres coreanos, japoneses o chinos necesitan 3 de los cuatro bytes: 国 안 녕

Cuando los caracteres se usan como parte de una cadena, esta se codifica para usar la menor cantidad de memoria necesaria para cada carácter1.

1 N.T.: Rust codifica las cadenas en UTF-8.

Para observar esto podemos usar .len():

fn main() {
    println!("Tamaño de un char: {}", std::mem::size_of::<char>()); // 4 bytes
    // .len() devuelve el tamaño de una cadena de texto en bytes
    println!("Tamaño de una cadena que contiene la 'a': {}", "a".len());
    println!("Tamaño de una cadena que contiene la 'ß': {}", "ß".len());
    println!("Tamaño de una cadena que contiene la '国': {}", "国".len());
    println!("Tamaño de una cadena que contiene la '𓅱': {}", "𓅱".len());
}

Lo que imprime:

Tamaño de un char: 4
Tamaño de una cadena que contiene la 'a': 1
Tamaño de una cadena que contiene la 'ß': 2
Tamaño de una cadena que contiene la '国': 3
Tamaño de una cadena que contiene la '𓅱': 4

Puedes ver que la a ocupa un byte, la 'ß' alemana ocupa dos, la japonesa ocupa tres, y el carácter del antiguo egipto 𓅱 ocupa cuatro bytes.

fn main() {
    let fragmento = "¡Hola!";
    println!("El fragmento ocupa {} bytes.", fragmento.len());
    let fragmento2 = "안녕!"; // Coreano de "hola"
    println!("El fragmento2 ocupa {} bytes.", fragmento2.len());
}

Que imprime:

El fragmento ocupa 7 bytes.
El fragmento2 ocupa 7 bytes.

El primer fragmento consta de seis caracteres y ocupa 7 bytes (la apertura de exclamación ocupa dos bytes). El segundo fragmento consta de tres caracteres y ocupa 7 bytes (los dos primeros caracteres ocupan tres bytes cada uno).

Si .len() devuelve el tamaño en bytes, ¿cómo se puede conocer el tamaño de una cadena de texto en caracteres? Aprenderemos esto más tarde, en este momento basta con recordar que se hace con .chars().count(). La primera función .chars() devuelve los caracteres separados y luego cuenta cuántos son.

fn main() {
    let fragmento = "¡Hola!";
    println!("El fragmento ocupa {} bytes y son {} caracteres.",
        fragmento.len(), fragmento.chars().count());
    let fragmento2 = "안녕!";
    println!("El fragmento2 ocupa {} bytes y son {} caracteres.",
        fragmento2.len(), fragmento2.chars().count());
}

Que imprime:

El fragmento ocupa 7 bytes y son 6 caracteres.
El fragmento2 ocupa 7 bytes y son 3 caracteres.

Inferencia de tipos de dato

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

La inferencia de tipos de datos consiste en que si no se le indica el tipo de datos al compilador, pero lo puede determinar por sí mismo, él decide que tipo utilizar. El compilador siempre necesita conocer el tipo de las variables, pero no siempre es necesario decírselo expresamente. En realidad, normalmente no necesitas indicárselo. Por ejemplo, en la sentencia let mi_numero = 8, mi_numero será de tipo i32. Esto se debe a que el compilador elige siempre el tipo i32 para los números enteros si no se le indica uno. Sin embargo, en la siguiente sentencia let mi_numero: u8 = 8, la variable mi_numero es de tipo u8 ya que así se le ha indicado.

Así que normalmente el compilador puede deducir el tipo de datos, pero en ocasiones será necesario indicárselo por una de las siguientes dos razones:

  1. Estás programando algo muy complejo y el compilador no puede deducir el tipo de datos que es necesario.
  2. Quieres usar un tipo de datos diferente (por ejemplo, quieres un i128, no el i32 que se usa por defecto).

Para especificar un tipo, se añaden dos puntos después del nombre de la variable seguido del tipo.

fn main() {
    let numerito: u8 = 10;
}

Para los números, se puede especificar el tipo después del número, no se necesita un espacio - solo teclearlo justo después del número.

fn main() {
    let numerito = 10u8; // 10u8 = 10 de tipo u8
}

También se puede añadir _ para añadir claridad a la lectura.

fn main() {
    let numerito = 10_u8; // Esto es más fácil de leer
    let numerazo = 100_000_000_i32; // 100 millones es de fácil lectura con _
}

El _ no modifica el número. Solo lo hace más fácil de leer. Y no importa el cuantos _ se utilizan.

fn main() {
    let numero = 0________u8;
    let numero2 = 1___6______2____4______i32;
    println!("{}, {}", numero, numero2);
}

Lo anterior imprime 0, 1624.

Números decimales

Los números decimales son aquellos que tienen coma decimal1. 5.5 es un número decimal y 6 es un número entero. 5.0 también es un número decimal e incluso 5. lo es.

1 N.T.: en español se usa una coma como carácter para separar la parte entera de un número de su parte decimal. En Rust, la coma decimal española se sustituye por el punto decimal que es el que se usa habitualmente en los lenguajes de programación.

fn main() {
    let mi_decimal = 5.; // Rust ve un . y sabe que es un decimal (float, en inglés)
}

Rust utiliza diversos tipos de dato para almacenar números decimales, son el f32 y el f64. Al igual que en los números enteros, el número tras f muestra el número de bits utilizados en cada caso para almacenar el dato. Si no se indica el tipo, Rust elige f64.

fn main() {
    let mi_decimal: f64 = 5.0; // Esta variable es de tipo f64
    let mi_otro_decimal: f32 = 8.5; // Esta es de tipo f32
    let tercer_decimal = mi_decimal + mi_otro_decimal; // ⚠️
}

Cuanto se intenta ejecutar el código anterior, Rust se queja diciendo:

error[E0308]: mismatched types
 --> src/main.rs:4:39
  |
5 |     let tercer_decimal = mi_decimal + mi_otro_decimal; // ⚠️
  |                                       ^^^^^^^^^^^^^^^ expected `f64`, found `f32`

El compilador indica "expected (tipo), found (type)" cuando se usa el tipo erróneo. Rust lee el código de esta forma:

fn main() {
    let mi_decimal: f64 = 5.0; // El compilador ve un f64
    let mi_otro_decimal: f32 = 8.5; // El compilador ve un f64. Es un tipo diferente.
    let tercer_decimal = mi_decimal + // Se quiere sumar mi_decimal, que es f64 a algún
                                      // otro número. Ahora espera otro f64...  
        mi_otro_decimal; // ⚠️ pero se encuentra un f32, no se pueden sumar.
}

Así que cuando veas que el compilador indica "expected (tipo), found (type)", debes buscar la causa por la que el compilador esperaba un tipo de datos diferente.

Con los números simples es fácil arreglarlo. Puedes convertir el f32 a f64 con un as.

fn main() {
    let mi_decimal: f64 = 5.0;
    let mi_otro_decimal: f32 = 8.5;
    // En la siguiente línea, se utiliza mi_otro decimal como un f64
    let tercer_decimal = mi_decimal + mi_otro_decimal as f64;
}

O simplemente, se pueden eliminar las declaraciones de tipo, Rust elegirá tipos que se puedan sumar entre sí.

fn main() {
    let mi_decimal = 5.0; // Rust elige f64
    let mi_otro_decimal = 8.5; // Rust elige de nuevo f64
    let tercer_decimal = mi_decimal + mi_otro_decimal;
}

El compilador de Rust es inteligente y no elegirá f64 si necesitas f32:

fn main() {
    let mi_decimal: f32 = 5.0; // Rust elige f64
    let mi_otro_decimal = 8.5; // Normalmente Rust elegiría f64
    // pero al conocer que lo vamos a sumar a un f32, elige un f32 para mi_otro_decimal
    let tercer_decimal = mi_decimal + mi_otro_decimal;
}

Imprimiendo '¡Hola, mundo!'

Puedes ver este capítulo en YouTube en inglés: Video 1, Video 2

Cuando creas un nuevo programa Rust, siempre contiene este código:

fn main() {
    println!("Hello, world!");
}
  • fn significa función.
  • main es el nombre de la función que inicia el programa.
  • () significa que en este caso no se le pasan variables a esta función.
  • {} es un bloque de código. Es donde se encuentra el código.
  • println! es una macro, que es como una función que sirve para escribir código por ti. Las macros siempre tienen un ! al final de su nombre. Por ahora, recuerda que ! significa que es una macro.

Para aprender lo que hace ;, crearemos otra función. Primero, en main imprimiremos el número 81.

1 N.T.: aprovechamos para cambiar el saludo a español. En Rust, cuando se utiliza la aplicación cargo para crear un programa, siempre se incorpora el código de main con "Hello, world!" de forma automática.

fn main() {
    println!("¡Hola, mundo número {}!", 8);
}

Las {} dentro de println! indican a Rust que "ponga la variable en este lugar". Este código imprime ¡Hola, mundo número 8!.

Podemos poner más cosas ampliando lo anterior.

fn main() {
    println!("¡Hola, mundos número {} y {}!", 8, 9);
}

El codigo anterior imprime ¡Hola, mundos número 8 y 9!.

Ahora vamos a crear una función.

fn numero() -> i32 {
    8
}

fn main() {
    println!("¡Hola, mundo número {}!", numero());
}

El código anterior también imprime ¡Hola, mundo número 8!. Cuando Rust encuentra numero() entiende que es una función. Esta función:

  • No toma ningún parámetro (porque tiene una llamada con ()).
  • Devuelve un i32. El símbolo de flecha -> muestra el tipo que devuelve la función.
  • La función en sí misma solo contiene un 8. Al no terminar en ; este valor es el que devuelve al terminar de ejecutarse. Si tuviera un ; detrás, la función no devolvería nada (devolvería un ()). Rust no compilaría si contuviera un ; al final ya que se ha indicado que (tras la flecha) debe devolver un valor de tipo i32 y con el ; se devuelve () que no es de tipo i32.
fn numero() -> i32 {
    8;
}

fn main() {
    println!("¡Hola, mundo número {}!", numero());
}
error[E0308]: mismatched types
 --> src/main.rs:1:16
  |
1 | fn numero() -> i32 {
  |    ------      ^^^ expected `i32`, found `()`
  |    |
  |    implicitly returns `()` as its body has no tail or `return` expression
2 |     8;
  |      - help: consider removing this semicolon

Esto significa que "me dijiste que numero() devuelve un i32, pero añadiste un ; por lo que esta función no devuelve nada". Así que el compilador sugiere que se elimine el punto y coma.

También se puede escribir lo siguiente return 8;, pero en Rust lo normal es simplemente eliminar el ; para ejecutar el return.

Cuando se quiere pasar variables a una función, se deben poner dentro de (). Hay que darles un nombre e indicar su tipo.

// Entran a la función dos i32s. Se llaman num_uno y num_dos
fn multiplicar(num_uno: i32, num_dos: i32) {
    let resultado = num_uno * num_dos;
    println!("{} por {} es {}", num_uno, num_dos, resultado);
}

fn main() {
    multiplicar(8, 9); // Pasamos unos números directamente
    let algun_numero = 10; // o podemos declarar dos variables
    let algun_otro_numero = 2;
    multiplicar(algun_numero, algun_otro_numero); // y pasarlos a la función
}

También se puede devolver un i32. Basta con poner la variable resultado como la última de la función sin ; al final.

fn multiplicar(num_uno: i32, num_dos: i32) -> i32 {
    let resultado = num_uno * num_dos;
    println!("{} por {} es {}", num_uno, num_dos, resultado);
    resultado // este es el valor i32 que se retorna
}

fn main() {
    // multiplicar() imprime el resultado y lo devuelve,
    // lo que permite asignarlo a resultado_mult
    let resultado_mult = multiplicar(8, 9);
}

La declaración de variables y los bloques de código

Se usa letpara declarar una variable (para decirle a Rust que construya una variable).

fn main() {
    let mi_numero = 8;
    println!("¡Hola, mundo número {}!", mi_numero);
}

Las variables existen dentro de un bloque de código {}. En el siguiente ejemplo mi_numero desaparece antes de llamar a println! porque se encuentra dentro de su propio código.

fn main() {
    {
        let mi_numero = 8; // mi_numero se crea aquí
                           // mi_numero se extingue aquí
    }
    println!("¡Hola, mundo número {}!", mi_numero);// ⚠️  mi_numero no existe y
                                                   // println!() no lo puede encontrar
}

Se puede usar un bloque de código para devolver un valor, como en el siguiente código.

fn main() {
    let mi_numero = {
        let segundo_num = 8;
        segundo_num + 9 // sin punto y coma, por lo que el
                        // bloque de código devuelve 8 + 9 = 17
    }; 
    println!("¡Hola, mundo número {}!", mi_numero);
}

Si se añadiera un punto y coma en la sentencia final del bloque, devolvería () (nada).

fn main() {
    let mi_numero = {
        let segundo_num = 8; // declara el segundo número
        segundo_num + 9;     // suma 9 con el segundo número
                             // pero no se devuelve
                             // segundo_num desaparece aquí
    }; 
    println!("¡Hola, mundo número {:?}!", mi_numero); // mi_numero es ()
}

Si has observado bien, hemos cambiado {} por {:?}. El motivo se verá en el siguiente capítulo.

La visualización y depuración

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

En Rust, las variables simples se pueden imprimir usyo {} en println!. Pero hay variables que no pueden imprimirse y es necesario usar la impresión de depuración. La impresión de depuración es para los programadores, porque habitualmente muestra más información. En ocasiones, esta impresión no es "bonita", no queda bien, porque muestra información extra para ayudarte.

¿Cómo puedes conocer si necesitas usar {:?} y no {}? El compilador te avesará. Por ejemplo:

fn main() {
    let no_imprime = ();
    println!("Esto no se imprimirá: {}", no_imprime); // ⚠️
}

Cuyo se compila el código anterior, el compilador se queja así:

error[E0277]: `()` doesn't implement `std::fmt::Desplay`
 --> src/main.rs:3:42
  |
3 |     println!("Esto no se imprimirá: {}", no_imprime); // ⚠️
  |                                          ^^^^^^^^^^ `()` cannot be formatted with el default formatter
  |
  = help: el trait `std::fmt::Desplay` es not implemented for `()`
  = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
  = note: required by `std::fmt::Desplay::fmt`
  = note: thes error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

Esto es mucha información, pero la parte importante es you may be able to use {:?} (or {:#?} for pretty-print) instead. Esto significa que puedes intentar usar {:?} o también {:#?}. La segunda opción, {:#?} se llama "impresión atractiva". Es igual que {:?} pero usyo un formateo diferente en más líneas.

En resumen, Desplay (vesualización) supone la impresión con {}, y Debug (depuración) supone la impresión con {:?}.

Una última cosa, puedes usar print! sin ln si no quieres que haya un salto de línea al final de la impresión.

fn main() {
    print!("Esto no imprimirá un salto de línea");
    println!(" así que esto estará en la mesma línea");
}

Que imprimirá Esto no imprimirá un salto de línea así que esto estará en la mesma línea.

El número menor y el número mayor

Si se quiere ver el menor y mayor número que se puede representar, se puede usar MIN y MAX. std es la "librería estándar del lenguaje" y contiene las funciones y otros elementos importantes del lenguaje Rust. Más adelante se explicarán elementos de la librería estyar. Mientras tanto, puedes recordar que esta es la forma de obtener los numeros menor y mayor de un tipo de datos.

fn main() {
    // pista: std::i8::MIN significa
    // "el valor de MIN de la sección i8 de la librería estandar"
    println!("El menor i8 es {} y el mayor i8 es {}.", std::i8::MIN, std::i8::MAX); 
    println!("El menor u8 es {} y el mayor u8 es {}.", std::u8::MIN, std::u8::MAX);
    println!("El menor i16 es {} y el mayor i16 es {}.", std::i16::MIN, std::i16::MAX);
    println!("El menor u16 es {} y el mayor u16 es {}.", std::u16::MIN, std::u16::MAX);
    println!("El menor i32 es {} y el mayor i32 es {}.", std::i32::MIN, std::i32::MAX);
    println!("El menor u32 es {} y el mayor u32 es {}.", std::u32::MIN, std::u32::MAX);
    println!("El menor i64 es {} y el mayor i64 es {}.", std::i64::MIN, std::i64::MAX);
    println!("El menor u64 es {} y el mayor u64 es {}.", std::u64::MIN, std::u64::MAX);
    println!("El menor i128 es {} y el mayor i128 es {}.", std::i128::MIN, std::i128::MAX);
    println!("El menor u128 es {} y el mayor u128 es {}.", std::u128::MIN, std::u128::MAX);
}

Que imprimirá:

El menor i8 es -128 y el mayor i8 es 127.
El menor u8 es 0 y el mayor u8 es 255.
El menor i16 es -32768 y el mayor i16 es 32767.
El menor u16 es 0 y el mayor u16 es 65535.
El menor i32 es -2147483648 y el mayor i32 es 2147483647.
El menor u32 es 0 y el mayor u32 es 4294967295.
El menor i64 es -9223372036854775808 y el mayor i64 es 9223372036854775807.
El menor u64 es 0 y el mayor u64 es 18446744073709551615.
El menor i128 es -170141183460469231731687303715884105728 y el mayor i128 es 170141183460469231731687303715884105727.
El menor u128 es 0 y el mayor u128 es 340282366920938463463374607431768211455.

Mutabilidad (cambio)

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

Cuando se declara una variable con `let, es inmutable (no se puede cambiar su valor).

El código siguiente no funciona.

fn main() {
    let mi_numero = 8;
    mi_numero = 10; // ⚠️
}

El compilador indica error[E0384]: cannot assign twice to immutable variable mi_numero.

error[E0384]: cannot assign twice to immutable variable `mi_numero`
 --> src/main.rs:3:5
  |
2 |     let mi_numero = 8;
  |         ---------
  |         |
  |         first assignment to `mi_numero`
  |         help: consider making this binding mutable: `mut mi_numero`
3 |     mi_numero = 10; // ⚠️
  |     ^^^^^^^^^^^^^^ cannot assign twice to immutable variable

Esto es porque las variables son inmutables si solo se escribe let.

Muchas veces, será necesario modificar la variable. Para ello, se debe añadir mut después de let:

fn main() {
    let mut mi_numero = 8;
    mi_numero = 10;
}

El código anterior funciona sin problema alguno.

Sin embargo, no se le puede cambiar el tipo. Esto no funciona:

fn main() {
    let mut mi_numero = 8; // La variable es i32. 
                           // El tipo no se puede cambiar.
    mi_numero = "¡Hola, mundo!"; // ⚠️
}

Si se intenta compilar el programa anterior, se obtendrá el mismo mensaje "expected" por parte del compilador: expected integer, found &str. &stres un tipo de cadena que aprenderemos pronto.

Ocultación (Shadowing)

La ocultación de una variable sucede cuando se usa let para declarar una nueva variable con el mismo nombre que otra. A primera vista se parece a la mutabilidad, pero es totalmente diferente. En el siguiente ejemplo, se oculta una variable:

fn main() {
    let mi_numero = 8; // Esta variable es i32
    println!("{}", mi_numero); // imprime 8
    let mi_numero = 9.2; // Esta variable es f64 y tiene el mismo nombre
    // pero es una variable nueva, completamente diferente.
    println!("{}", mi_numero) // imprime 9.2
}

Se dice que hemos "ocultado" mi_numerocon un nuevo "enlace".

¿Se ha destruido la anterior variable mi_numero? No, pero cuando se llama a mi_numero ahora se accede a la segunda variable de tipo f64. Y como ambas declaraciones se encuentran en el mismo bloque de código (mismo ámbito, mismo {}), se deja de tener acceso al mi_numero de tipo i32.

Si estuvieran en diferentes bloques de código, podríamos volver a acceder a la primera variable mi_numero. Por ejemplo:

fn main() {
    let mi_numero = 8; // Esta variable es i32
    println!("{}", mi_numero); // imprime 8
    {
        let mi_numero = 9.2; // Esta variable es f64 y tiene el mismo nombre
        // pero es una variable nueva, completamente diferente.
        println!("{}", mi_numero) // imprime 9.2
                                  // pero la nueva variable mi_numero
                                  // solo existe hasta aquí
                                  // la anterior ¡sigue viva!
    }
    println!("{}", mi_numero); // imprime 8
}

En resumen, cuando ocultas una variable, no la destruyes. La bloqueas.

Qué ventajas tiene el ocultar variables. Es una buena práctica cuando necesitas modificar una variable en muchas ocasiones. Imagina que quieres hacer un conjunto de cálculos matemáticos simples con una variable:

fn dos_veces(numero: i32) -> i32 {
    numero * 2
}

fn main() {
    let numero_final = {
        let y = 10;
        let x = 9; // x comienza con 9
        let x = dos_veces(x); // se oculta con el nuevo x: 18
        let x = x + y; // se oculta con el nuevo x: 28
        x // devuelve x: a numero_final se asigna este valor de x
    };
    println!("El número ahora es: {}", numero_final)
}

Sin ocultar las variables anteriores, habría sido necesario pensar diferentes nombres, incluso aunque no nos importen estos valores intermedios:

fn dos_veces(numero: i32) -> i32 {
    numero * 2
}

fn main() {
    // Ejemplo sin usar las capacidades de ocultar variables
    let numero_final = {
        let y = 10;
        let x = 9; // x comienza con 9
        let x2 = dos_veces(x); // segundo nombre para x
        let x2_y = x2 + y; // ¡tercer nombre para x!
        x2_y // qué pena no tener disponible la ocultación
             // habríamos podido usar solo una variable x
    };
    println!("El número ahora es: {}", numero_final)
}

En general, se usa la ocultación de variables en estos casos. Cuando se quiere usar una variable para un cálculo y luego otro más, sin tener mucho interés por los valores intermedios.

La pila, la memoria dinámica y los punteros

La pila ("stack" en inglés), la memoria dinámica ("heap" en inglés) y los punteros son elementos muy importantes en Rust.

La pila y la memoria dinámica son dos tipos de almacenamiento de los datos de un programa durante su ejecución. Sus diferencias más importantes son:

  • La pila es muy rápida, la memoria dinámica no lo es tanto. Tampoco es lenta, pero siempre es más rápido acceder a la pila. Aunque no es posible utilizar la pila siempre porque:
  • Rust necesita conocer el tamaño de una variable en durante su compilación para poder guardarla en la pila. Así, las variables simples como i32 van a la pila ya que se conoce su tamaño exacto. Se sabe que va a ocupar 4 bytes, 32 bits = 4 bytes. Por lo tanto, los datos de tipo i32 pueden ir siempre a la pila.
  • Algunos tipos no tienen un tamaño conocido en tiempo de compilación. No pueden guardarse en la pila. ¿Qué se puede hacer? En primer lugar, se pone la información en la memoria dinámica ya que esta puede contener datos de cualquier tamaño. En segundo lugar, se guarda un puntero en la pila. El tamaño de los punteros es conocid. Así, para recuperar un valor de una variable que está en la memoria dinámica, el ordenador va primero a la pila, obtiene el puntero y lo sigue hasta la memoria dinámica para localizr el dato que se busca.

Los punteros parecen complicados, pero no lo son. Son como una tabla de contenidos de un libro. Imagina este libro:

MI LIBRO

TABLA DE CONTENIDO

Capítulo                        Página
Capítulo 1: mi vida              1
Capítulo 2: mi gato              15
Capítulo 3: mi trabajo           23
Capítulo 4: mi familia           30
Capítulo 5: mis planes futuros   43

La tabla de contenido es como una "pila" que, en este caso, contiene cinco punteros. Puedes leerlos y encontrar la información sobre la que tratan. ¿Dónde está el capítulo sobre "mi vida"? Está en la página 1 (Apunta a la página 1). ¿Dónde está el capítulo sobre "mi trabajo"? Está en la página 23.

El puntero que se ve habitualmente en Rust se denomina referencia. Esto es lo importante que se debe saber: una referencia apunta a la memoria de otro valor. Una referencia supone que se tome prestado el valor, pero no se apropia de él. Es lo mismo que en el libro anterior: la tabla de contenidos no posee la información. Se encuentra en los capítulos que son los que la poseen. En Rust, las referencias llevan el símbolo & al principio de ellas. Así.

  • let mi_variable = 8 crea una variable normal, pero
  • let mi_referencia = &mi_variable crea una referencia. Se lee como "mi_referencia es una referencia a mi_variable" o como "mi_referencia se refiere a mi_variable".

Esto significa que mi_referencia solo mira a los datos de mi_variable. mi_variable sigue siendo propietaria de sus datos. También es posible tener una referencia que "apunte" a otra referencia. Hasta culquier número de referencias.

fn main() {
    let mi_numero = 15; // Esto es un i32
    let referencia_simple = &mi_numero; //  Esto es una &i32
    let referencia_doble = &referencia_simple; // Esto es una &&i32
    let referencia_quintuple = &&&&&mi_numero; // Esto es una &&&&&i32
}

Todos estos son tipos de dato diferentes, de la misma forma que "un amigo de un amigo" es diferente de "un amigo".

Más sobre impresión

En Rust se puede imprimir como se quiera. Por eso, interesa conocer algunas cosas más sobre este tema.

Si se añade \n se imprimirá una nueva línea. Si se añade \t se insertará un tabulador:

fn main() {
    // Observa: la función usada es print!, no println!
    print!("\tComienza con un tabulador\ny salta a una nueva línea");
}

El código anterior imprime lo siguiente:

        Comienza con un tabulador
y salta a una nueva línea

Dentro de "" se puede escribir en diferentes líneas, pero es necesario tener cuidado con los espacios:

fn main() {
    // Nota: después de la primera línea tienes que comenzar la siguiente línea
    // en la primera columna (pegado a la izquierda).
    println!("Dentro de comillas
se puede escribir
en muchas líneas
y se imprimirá correctamente.");
    // Si se escribe directamente bajo la sentencia, se añadirán los espacios
    // correspondientes de la izquierda
    println!("Si se olvida que hay que
    escribir pegado al lado izquierdo
    estos espacios se añadirán
    a la impresión.");
}

El código anterior imprime:

Dentro de comillas
se puede escribir
en muchas líneas
y se imprimirá correctamente.
Si se olvida que hay que
    escribir pegado al lado izquierdo
    estos espacios se añadirán
    a la impresión.

Si se necesitase imprimir caracteres como \n (caracteres de escape, como el del salto de línea y el tabulador que se han visto antes), se puede añadir un \ extra:

fn main() {
    println!("Se imprimen caracteres de escape, no inserta nueva línea y tabulador: \\n y \\t");
}

Lo que imprime:

Se imprimen caracteres de escape, no inserta nueva línea y tabulador: \n y \t

A veces se necesitan muchos " y caracteres de escape en el texto, por lo que Rust proporciona un método más simple para ignorarlos: se añade r# al comienzo y # al final de la cadena de caracteres.

fn main() {
    // En esta línea hemos usado \ cinco veces
    println!("Él dijo, \"Puedes encontrar el fichero en c:\\files\\my_documents\\file.txt\". Y así fue como lo encontré.");
    println!(r#"Él dijo, "Puedes encontrar el fichero en c:\files\my_documents\file.txt". Y así fue como lo encontré."#)
}

Ambas opciones imprimen lo mismo, pero el uso de r# lo hace más simple de entender.

Él dijo, "Puedes encontrar el fichero en c:\files\my_documents\file.txt". Y así fue como lo encontré.
Él dijo, "Puedes encontrar el fichero en c:\files\my_documents\file.txt". Y así fue como lo encontré.

Si se necesitara imprimir un carácter # en el texto, se puede usar r## al comienzo del texto y ## al final. Si se usaran más de dos consecutivos, se pueden seguir añadiendo # al comienzo y al final, hasta que no coincida con nada contenido en el texto.

fn main() {

    let my_string = "'Hola, mundo,' dijo."; // comilla simples
    let quote_string = r#""Hola, mundo," dijo."#; // comillas dobles
    // Contiene # se necesita al menos ##
    let hashtag_string = r##"El hasgtag #holamundo se ha hecho muy popular."##; 
    // Contiene ### se necesitan al menos ####
    let many_hashtags = r####""No se tiene que teclear ### para usar un hashtag. Solo hay que usar #.""####; 

    println!("{}\n{}\n{}\n{}\n", my_string, quote_string, hashtag_string, many_hashtags);
}

Este código imprimirá:

'Hola, mundo,' dijo.
"Hola, mundo," dijo.
El hasgtag #holamundo se ha hecho muy popular.
"No se tiene que teclear ### para usar un hashtag. Solo hay que usar #."

Existe otro uso para r#: usar palabras reservadas (como let, fn, etc.) como nombres de variable.

fn main() {
    let r#let = 6; // let como nombre de variable
    let mut r#mut = 10; // Esta variable se llama mut
}

Esta función de r# se introdujo porque las versiones más antiguas de Rust tenían menos palabras reservadas que ahora. Así se pueden evitar errores en código previo en el que se usaban nombres de variables que ahora son palabras reservadas.

Puede ser que por alguna razón realmente se necesite una función que se denomine como return. Así se puede escribir:

fn r#return() -> u8 {
    println!("Ahí va tu número.");
    8
}

fn main() {
    let mi_numero = r#return();
    println!("{}", mi_numero);
}

Que imprime:

Ahí va tu número.
8

Esto no se va a necesitar normalmente, pero si realmente hace falta, se puede usar.

Si se necesita imprimir los bytes de un &str o un char, basta con escribir la letra b delante de la cadena. Esto funciona para todos los caracteres ASCII. Estos son todos los caracteres ASCII.

☺☻♥♦♣♠♫☼►◄↕‼¶§▬↨↑↓→∟↔▲▼123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~

Así, este código:

fn main() {
    // N.T.: no se pueden poner tildes ya que en las 
    // vocales ya que las letras resultantes no forman
    // parte del conjunto de caracteres ASCII
    println!("{:?}", b"Esto aparece como una lista de numeros");
}

Da el siguiente resultado:

[69, 115, 116, 111, 32, 97, 112, 97, 114, 101, 99, 101, 32, 99, 111, 109, 111, 32, 117, 110, 97, 32, 108, 105, 115, 116, 97, 32, 100, 101, 32, 110, 117, 109, 101, 114, 111, 115]

Para un char esto se llama byte y para el tipo &str se llama byte de cadena de texto.

También existe un sistema de escape para insertar caracteres Unicode: \u{}. Así es posible imprimir cualquier carácter Unicode en una cadena de texto. Además, es posible formatear un número en hexadecimal usando {:X}. El siguiente ejemplo demuestra cómo imprimir el código hexadecimal que representa al carácter Unicode y cómo imprimirlo de nuevo como carácter.

fn main() {
    // Convierte el char a u32 para obtener su valor numérico
    println!("{:X}", '행' as u32); 
    println!("{:X}", 'H' as u32);
    println!("{:X}", '居' as u32);
    println!("{:X}", 'い' as u32);

    // Imprime los caracteres con el sistema de escape \u
    println!("\u{D589}, \u{48}, \u{5C45}, \u{3044}"); 
}

Ya se conoce que println! puede imprimir con {} (modo Display) t {:?} (para Depuración). Además de {:#?} para "impresión bonita". Pero existen otras muchas formas de imprimir.

Por ejempli, si se dispone de una referencia se puede usar {:p} para imprimir la dirección del puntero. Es decir, el lugar de la memoria del ordenador a la que apunta la referencia.

fn main() {
    let number = 9;
    let number_ref = &number;
    println!("{:p}", number_ref);
}

El código anterior imprime algo parecido a 0x7ffec9426f4c, dependerá de dónde se ejecute el programa y se almacene el número referenciado en la memoria del ordenador.

Los valores numéricos se pueden imprimir en binario, hexadecimal u octal:

fn main() {
    let number = 555;
    println!("Binario: {:b}, hexadecimal: {:x}, octal: {:o}", number, number, number);
}

Que imprime Binario: 1000101011, hexadecimal: 22b, octal: 1053.

También se pueden añadir números entre las llaves para indicar qué variable utilizar, teniendo en cuenta que la primera tiene como índice el 0, la segunda el 1 y así sucesivamente.

fn main() {
    let nombre_padre = "José Miguel";
    let nombre_hijo = "Víctor";
    let apellido = "González";
    println!("Este es {1} {2}, hijo de {0} {2}.", nombre_padre, nombre_hijo, apellido);
}

La variable nombre_padre está en la posición 0, nombre_hijoen la 1 y apellido está en la posición 2. Por eso, el código anterior imprime Este es Víctor González, hijo de José Miguel González..

Puede suceder que sea necesario imprimir una cadena de caracteres compleja con muchas variables dentro de las llaves. O puede que se necesite imprimir la misma variable dos o más veces. Para ello, se pueden añadir nombres a las llaves.

fn main() {
    println!(
        "{city1} está en {pais} y {city2} también está en {pais},
pero {city3} no está en {pais}.",
        city1 = "Seul",
        city2 = "Busan",
        city3 = "Tokio",
        pais = "Korea"
    );
}

Que imprime:

Seul está en Korea y Busan también está en Korea,
pero Tokio no está en Korea.

También es posible editar de forma compleja el formato de la impresión. Tiene esta forma:

{variable:relleno alineamiento mínimo.máximo}

Para entender esta sintaxis:

  1. ¿Se quiere usar un nombre de variable? Se escribe primero su nombre, como antes en {pais}. Lo siguiente será añadir (opcionalmente) : después si se quiere formatear de algún modo.
  2. ¿Se se necesita un carácter de relleno? Por ejemplo, 55 con tres "ceros de relleno" se imprimiría como 00055.
  3. ¿Que alineamiento se necesita para el relleno? izquierda, centro o derecha.
  4. ¿Se desea una longitud mínima? solo hay que indicar el número deseado.
  5. ¿Se desea una longitud máxima? solo hay que indicarla con un . delante.

Por ejemplo, si se quiere escribir la letra "a" con cinco caracteres - de relleno (a izquierda y derecha):

fn main() {
    let letra = "a";
    println!("{:-^11}", letra);
}

Que imprime -----a-----. El ordenador lo interpreta así:

  • ¿Hay un nombre de variable? No, ya que lo primero que aparece en {:-^11} son los dos puntos. No hay nombre de variable delante de estos dos puntos.
  • ¿Se pide un carácter de relleno? Sí, - está justo después de : y lo sigue un ^, lo que significa que el texto se inserta en el centro y el carácter de relleno se reparte en los espacios sobrantes a izquierda y derecha. Las otras dos posibilidades son: < que indica que el texto va a la izquierda y el carácter de relleno a la izquierda, y > que indica que el texto va a la derecha con el relleno a la izquierda.

A continuación, se muestran diversos ejemplos de tipos de formateo.

fn main() {
    let titulo = "NOTICIAS DE HOY";
    // sin variable, relleno con -, centrado, longitud de 30 caracteres
    println!("{:-^30}", titulo); 
    let barra = "|";
    // sin variable, relleno con espacios, 15 caracters cada uno, una barra a izquierda y otra a derecha
    println!("{: <15}{: >15}", barra, barra); 
    let a = "SEUL";
    let b = "TOKIO";
     // variables city1 y city2, relleno con -, a izquierda y a derecha
     println!("{city1:-<15}{city2:->15}", city1 = a, city2 = b);
}

El código anterior imprime:

-------NOTICIAS DE HOY--------
|                            |
SEUL---------------------TOKIO

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.

const y static

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

Además de let, existen dos maneras más de declarar valores. const y static. Para estas declaraciones, Rust no realiza la inferencia de los tipos: es necesario declarar el tipo de los valores. Existen vamores que no cambian (const significa constante). La diferencia entre ellos es que:

  • const se utiliza para los valores que no cambian y el nombre se reemplaza por su valor, allí donde se usa.
  • static define una posición fija en memoria que puede actuar como una variable global.

Por ello, son casi lo mismo. Los programadores de Rust casi siempre utilizan valores constantes con const.

Por convención, las constantes se suelen escribir con todas las letras en mayúsculas, normalmente están fuera del main para que existan en todo el programas.

Dos ejemplos:

const NUMERO_DE_MESES: u32 = 12;
static ESTACIONES: [&str; 4] = ["Primavera", "Verano", "Otoño", "Invierno"];

fn main() {
    println!("{}", NUMERO_DE_MESES);
    println!("{:?}", ESTACIONES);
}

Algo más sobre referencias

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

Las referencias son muy importantes en Rust. Las utiliza para asegurarse de que son seguros todos los accesos a la memoria. Ya se ha explicado anteriormente que para crear una referencia se utiliza & delante del valor:

fn main() {
    let pais = String::from("Austria");
    let ref_uno = &pais;
    let ref_dos = &pais;

    println!("{}", ref_uno);
}

El código anterior imprime Austria. pais es un String. Se crean dos referencias a pais. Estas referencias son de tipo &String, es decir son dos variables que son "referencias a String". Se pueden crear tantas referencias a paiscomo se quiera. Todas "apuntan" al mismo valor, pero son punteros diferentes.

A continuación se muestra un ejemplo sobre cómo Rust proteje el acceso a zonas de memoria erróneas:

fn return_str() -> &str {
    let pais = String::from("Austria");
    let pais_ref = &pais;
    pais_ref // ⚠️
}

fn main() {
    let pais = return_str();
}

La función return_str() crea un valor de tipo String, luego crea una referencia a dicho valor. Cuando se intenta devolver la referencia se produce un error:

error[E0106]: missing lifetime specifier
 --> src/main.rs:1:20
  |
1 | fn return_str() -> &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: consider using the `'static` lifetime
  |
1 | fn return_str() -> &'static str {
  |                    ^^^^^^^^

El valor de pais solo existe dentro de la función, al terminar de ejecutarse desaparece. Una vez la variable desaparece, el ordenador libera la memoria que ocupaba y la utiliza para otra cosa. Por eso, después de que la función termina, pais_ref apunta a una zona de memoria que ya no tiene el valor esperado y eso no es correcto. Rust previene este fallo en el código e impide que el programa compile.

Este es el efecto importante de la existencia de tipos de dato "con dueño" en Rust. En el código anterior, el valor String tiene como dueño a la variable pais, por eso se puede "prestar" a otras variables (referencias), pero cuando desaparece la variable dueña del valor, pais, la referencia también desaparece, la referencia tiene "prestado" el valor, por lo que no se puede pasar a otro "dueño".

Referencias modificables (mutables)

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

Si se necesita modificar un valor a través de una referencia, se debe indicar que la referencia sea modificable (mutable). Para ello, se utiliza &mut en lugar de &.

fn main() {
    // no hay que olvidar que hay que escribir mut en la variable original
    let mut mi_numero = 8;
    let num_ref = &mut mi_numero;
}

mi_numero es de tipo i32, y num_ref es de tipo &mut i32 (es una "referencia modificable/mutable a un valor ì32).

Si se desea usar esta referencia para sumar 10, no se puede usar num_ref += 10 ya que num_ref no es de tipo i32, es &i32. Para obtener el valor de la referencia, se debe usar * que significa que "no se necesita la referencia, sino el valor que al que representa". En otras palabras, * es lo opuesto a &, un * borra a un &.

fn main() {
    let mut mi_numero = 8;
    let num_ref = &mut mi_numero;
    *num_ref += 10; // Usa * para cambiar el valor i32
    println!("{}", mi_numero);

    let segundo_numero = 800;
    let triple_referencia = &&&segundo_numero;
    println!("segundo_numero = ¿triple_referencia? {}",
        segundo_numero == ***triple_referencia);
}

El código anterior, da como resultado:

18
segundo_numero = ¿triple_referencia? true

El uso del operador sobre una variable & se denomina "referenciar". El uso del operador sobre una variable de referencia * se denomina "desreferenciar".

Rust usa dos reglas para las referencias mutables e inmutables. Son muy importantes y fáciles de recordar porque tienen sentido.

  • Regla 1: Si solo existen referencias inmutables a un valor, se pueden tener tantas como se quiera.
  • Regla 2: Si existe una referencia mutable a un valor, solo puede existir una referencia. Esto último significa que no pueden existir a la vez referencias inmutables y mutables en el mismo momento.

Estas reglas son necesarias debido a que las referencias mutables pueden cambiar los datos. Sería problemático que se modificara un dato cuando otras referencias lo están usando.

Un forma de entenderlo es pensar en la creación de una presentación de Powerpoint 1.

1

No se trata de un ejemplo excesivamente bueno, ya que actualmente es posible editar simultáneamente en Office 365.

El primer caso representa el de la existencia de una referencia mutable: un empleado está editando una presentación de Powerpoint. Piode ayuda a su jefe. El empleado entrega sus credenciales de acceso a su jefe para que pueda editar el Powerpoint. Ahora el jefe tiene una "referencia mutable" a la presentación de su empleado. El jefe puede hacer los cambios que quiera y devolver el ordenador más tarde. Nadie más tiene acceso a la presentación, por lo que este caso no da problemas.

El segundo caso representa el de **la existencia únicamente de referencias inmutables": El empleado entrega la presentación a 100 personas. Todas ellas pueden verla. Tienen una "referencia inmutable" a la presentación ya que nadie puede modificarla, por lo que este caso no da problemas.

El tercer caso representa la situación problemática a evitar: El empleado entrega sus credenciales de acceso a su jefe, que a partir de aquí dispone de una "referencia mutable". Además, el empleado entrega la presentación a 100 personas. El jefe puede entrar a editar y estas modificaciones pueden verse o no por parte de las 100 personas, según el momento en que accedan sin que puedan controlar.

Se puede ver que el intento de usar una referencia mutable simultáneamente con una inmutable no es aceptado por el compilador:

fn main() {
    let mut numero = 10;
    let numero_ref = &numero;
    let numero_modif = &mut numero;
    *numero_modif += 10;
    println!("{}", numero_ref); // ⚠️
}

El compilador muestra un mensaje explicativo muy claro para mostrar el problema:

error[E0502]: cannot borrow `numero` as mutable because it is also borrowed as immutable
 --> src/main.rs:4:24
  |
3 |     let numero_ref = &numero;
  |                      ------- immutable borrow occurs here
4 |     let numero_modif = &mut numero;
  |                        ^^^^^^^^^^^ mutable borrow occurs here
5 |     *numero_modif += 10;
6 |     println!("{}", numero_ref); // ⚠️
  |                    ---------- immutable borrow later used here

Sin embargo, este código sí funciona. ¿Por qué?

fn main() {
    let mut numero = 10;
    let numero_modif = &mut numero; // referencia modificable
    *numero_modif += 10; // suma 10
    let numero_ref = &numero; // referencia inmutable
    println!("{}", numero_ref); // imprime el valor referenciado
}

Imprime 20 sin problemas. Funciona porque el compilador es suficientemente inteligente para comprender el código. Conoce que se ha cmabiado numero a través de la referencia numero_modif, pero después, esta referencia mutable no se vuelve a usar. Por eso no hay problema aquí. No se usan "a la vez" referencias inmutables y mutables (como sí pasaba en el caso anterior).

En versiones anteriores de Rust, este código sí daba error, pero actualmente no lo hace ya que el compilador es más inteligente. Puede entender no solo lo que se ha tecleado, sino como se usa todo.

Ocultación (shadowing) de nuevo

Es necesario recordar que el ocultamiento de variables no destruye sus valores, sino que los bloquea. Con el uso de las referencias esto se ve más claro.

fn main() {
    let pais = String::from("Austria");
    let pais_ref = &pais;
    let pais = 8;
    println!("{}, {}", pais_ref, pais);
}

¿Qué imprime este código? ¿Austria, 8 o 8, 8? Imprime Austria, 8. En la primera línea, se declara una variable String con el valor Austria y denominada pais. En la segunda línea se crea una referencia al valor de esta variable. Es decir, a la String que contiene Austria. Después, se oculta la variable pais con una nueva cuyo valor es 8 y que es de tipo i32. El primer elemento pais no se destruyó (lo hará al finalizar el ámbito en el que está, en la llave de cierre de este bloque), por lo que sigue accesible a través de la referencia pais_ref. Para mayor claridad, se vuelve a mostrar el código, ahora con comentarios sobre su comportamiento.

fn main() {
    let pais = String::from("Austria"); // String denominada pais
    let pais_ref = &pais; // pais_ref es una referencia al valor
    let pais = 8; // nueva variable pais con un valor 8. Sin relación con la anterior, ni con pais_ref
    println!("{}, {}", pais_ref, pais); // pais_ref sigue "apuntando" al dato Austria
}

Paso de referencias a funciones

Puedes ver este capítulo en Youtube en inglés: referencias inmutables y referencias mutables

Las referencias son muy útiles en las funciones. La regla de Rust para todos los valores es que un valor solo puede tener una variable propietario.

Este código no funcionará:

fn print_pais(pais_nombre: String) {
    println!("{}", pais_nombre);
}

fn main() {
    let pais = String::from("Austria");
    print_pais(pais); // Se imprime "Austria"
    print_pais(pais); // ⚠️ Se intenta de nuevo, pero no funciona
}

Devuelve el siguiente error:

error[E0382]: use of moved value: `pais`
 --> src/main.rs:8:16
  |
6 |     let pais = String::from("Austria");
  |         ---- move occurs because `pais` has type `String`, which does not implement the `Copy` trait
7 |     print_pais(pais); // Se imprime "Austria"
  |                ---- value moved here
8 |     print_pais(pais); // ⚠️ Se intenta de nuevo, pero no funciona
  |                ^^^^ value used here after move

La variable pais ya no existen en la última línea. El funcionamiento es el siguiente:

  • Paso 1: Se crea el valor de tipo String cuyo dueño es la variable pais.
  • Paso 2: Se pasa pais a la función print_pais. Es una función que no tiene -> en su declaración, por lo que no retorna ningún valor. Al hacer la llamada, la variable pais_nombre (parámetro) es la nueva dueña del valor. Después de que esta función finaliza, pais_nombre desaparece, como es la dueña del valor, este también se destruye.
  • Paso 3: Se intenta pasar por segunda vez pais a la función `print_pais, pero ya no existe ya que dejó de ser dueña del valor y el valor despareció dentro de la primera llamada a la función.

Se podría hacer que la función print_pais devolviera de nuevo el valor String, pero es poco ortodoxo.

fn print_pais(pais_nombre: String) -> String {
    println!("{}", pais_nombre);
    pais_nombre // se devuelve el valor
}

fn main() {
    let pais = String::from("Austria");
    let pais = print_pais(pais); // Es necesario crear una nueva variable para recuperar el valor
    print_pais(pais);
}

Ahora sí funciona e imprime:

Austria
Austria

Es mucho mejor evitar que la variable print_nombre, parámetro de la función, sea dueña del valor, solamente se "presta", sin que la variable pais de la función main deje de ser su dueño.

fn print_pais(pais_nombre: &String) {
    println!("{}", pais_nombre);
}

fn main() {
    let pais = String::from("Austria");
    print_pais(&pais); // Se imprime "Austria"
    print_pais(&pais); // Se intenta de nuevo y funciona correctamente
}

En este caso print_pais() toma una referencia a una String: &String. Cuando se llama print_pais() se pasa una referencia con &pais. Esto significa que "la función puede acceder al valor, pero no se hace dueña de él".

A continuación, se muestra un ejemplo similar para observar el comportamiento de las referencias modificables (mutables).

fn añade_hungria(pais_nombre: &mut String) { // se pasa una referencia mutable
    pais_nombre.push_str("-Hungría"); // push_str() añade un &str a un String
    println!("Ahora dice: {}", pais_nombre);
}

fn main() {
    // es importante que la variable se declare
    // como mutable para poder crear referencias mutables
    let mut pais = String::from("Austria");
    añade_hungria(&mut pais); // hay que pasar la referencia mutable.
    println!("Y el valor se ha modificado aquí: {}", pais);
}

Que imprime:

Ahora dice: Austria-Hungría
Y el valor se ha modificado aquí: Austria-Hungría

En resumen:

  • fn nombre_de_funcion(variable: String) toma un String y se hace dueño de él.
  • fn nombre_de_funcion(variable: &String) toma prestado un String y puede acceder a su valor.
  • fn nombre_de_funcion(variable: &mut String) toma prestado un String, puede acceder a su valor y modificarlo.

El siguiente ejemplo puede parecer similar, pero es muy diferente. Sirve para mostrar cómo quien se hace dueño de un objeto puede decidir que sea modificable, aunque anteriormente no lo fuese.

fn main() {
    let pais = String::from("Austria"); // pais no es mutable, ni referencia
    añadir_hungria(pais);
}

fn añadir_hungria(mut pais: String) { // añadir_hungria declara su parámetro como mutable
    pais.push_str("-Hungría");
    println!("{}", pais);
}

La función añadir_hungria se hace dueña del valor en su variable mut pais. A partir de ahí, puede hacer con este valor lo que quiera.

Si se recuerda el ejemplo anterior sobre el empleado, su jefe y la presentación en powerpoint, esta es la situación en la que el empleado le da el control completo al jefe. El empleado no puede volver a tocar la presentación y el jefe puede hacer lo que quiera con ella.