Introducción a Rust

0
588
Introducción a Rust

En esta introducción a Rust vamos a ver los rasgos principales del lenguaje.

Entorno

Esta introducción está escrita en la versión 1.41.1 de rustc  en MacOS Catalina 10.15.3.

¿Qué es Rust?

Rust es un lenguaje que promete excelente rendimiento gracias a su bajo nivel, sin sacrificar en ergonomía o en fiabilidad.

Sus objetivos de diseño son:

  • Concurrencia segura por defecto.
  • Acceso seguro de memoria (sin punteros nulos o colgantes).
  • Eficiencia sin recolector de basura.

Recursos para aprender Rust

Los mejores recursos para aprender Rust son los dos libros oficiales y los ejercicios prácticos:

Yo recomiendo empezar por el primer libro The Rust Programming Language. Explica de principio a fin todo lo que tiene que ver con el lenguaje.

Instalación

En Linux o MacOs:

 

En Windows:
Instalar rustup-init.exe.

Existen otros métodos de instalación en la guía de instalación.

Para comprobar que la instalación ha funcionado, revisar la versión con:

Si tienes problemas en MacOS, prueba a ejecutar este comando:

Plugin de Rust para IDE

Para facilitar el trabajo en Rust, existen plugins para los IDE más usados.

 

He probado ambas y he tenido mejor resultado con InteliJ IDEA. Pero ninguna es infalible y no es capaz de autocompletar en todos los casos.

Hola Mundo

Para crear un proyecto de Rust, el comando cargo new  crea un nuevo proyecto de tipo aplicación:

Esto creará el directorio hello_world, con los archivos:

  • cargo.toml: Archivo que define el proyecto. Piensa pom.xml o package.json. ¿Qué es .toml?
  • src/main.rs: Archivo de entrada de la aplicación.
  • .gitignore: Configura Git para ignorar la carpeta /target.

 

El contenido de main.rs es el siguiente, parece que nuestro trabajo está hecho:

Para compilar el código, usamos cargo build:

Para comprobar que el código compila, usamos cargo check. Es más rápido que build:

Para ejecutar la aplicación, usamos cargo run:

Stack y Heap

Para usar Rust correctamente, debemos conocer cómo es su gestión de memoria.
En Rust, la memoria se divide en stack y heap.

Stack

Guarda la memoria en el orden en el que entra.
Esto se llama último dentro, primero fuera. Imagina una pila de platos, solo puedes quitar el último que has añadido.

Toda lo almacenado en el stack debe tener un tamaño fijo, si no sabemos el tamaño de algo, debemos guardarlo en el heap.

Heap

Almacena la memoria de una forma menos organizada.
Cuando guardamos algo en el heap, el sistema operativo busca un hueco disponible, y guarda la dirección de esa memoria en el stack.

De esta forma, tenemos acceso a una memoria de tamaño desconocido desde el stack.

 

Stack y Heap

Variables y Mutabilidad

Para declarar una variable, usamos let:

Ahora, si intentamos modificar la variable:

Vamos a obtener un error con este aspecto:

Esto se debe a que en Rust las variables son inmutables por defecto.

Si queremos que una variable sea mutable, debemos usar mut.

Documentación sobre variables y mutabilidad.

Tipos primitivos

Escalares

En Rust existen muchos tipos primitivos escalares:

  • Enteros con signo: i8, i16, i32, i64, i128, isize.
  • Enteros sin signo: u8, u16, u32, u64, u128, usize
  • Punto flotante: f32, f64.
  • char para valores como 'a', 'b', 'c'.
  • bool puede ser true o false.
  • El tipo unidad (), una tupla vacía. Parecido a un void en otros lenguajes.

Tuplas

Una tupla es una colección de valores con distintos tipos. Se construyen con los paréntesis.

El compilador de Rust puede extraer los tipos en situaciones normales como esta, por eso no necesitamos especificar los tipos así:

También podemos extraer los valores de la tupla de esta manera:

Arrays

Un array es una colección de elementos del mismo tipo que se van a guardar en memoria de manera continua. Se crean usando los corchetes y es necesario especificar su tamaño:

En muchos casos el compilador podrá extraer el tipo del array en tiempo de compilación. Basta con:

Documentación sobre Tipos primitivos.

Structs

Con struct podemos crear tuplas con nombre:

Estructuras que almacenan datos en campos nombrados:

Si queremos que los campos sean accesibles desde fuera, usamos pub.

Más adelante veremos cómo adherir métodos a un struct con impl.

Documentación sobre structs.

Enums

Un enum permite la creación de un tipo que puede ser una de las variantes disponibles.
El valor de un enum puede contener datos, como una tupla o un struct.

Documentación sobre enums.

El enum Option

En Rust no existe el concepto de null.

Pero sí existe un enum que representa el concepto de un valor existente o absente.
Este es Option, y está dentro de la librería estándar del lenguaje. No hace falta importarlo.

Un Option puede ser dos cosas:

  • None, indica que no tiene valor real.
  • Some(value), una tupla que contiene el valor real de tipo T.

Ahora vamos a intentar hacer una operación con un Option:

Si intentamos ejecutar este código, obtendremos el error:

Esto se debe a que Option es un wrapper que contiene el valor real. Para sacar el valor podemos usar la función unwrap(). Esto nos permitirá «desenvolver» el valor real.

Pero que pasa si intentamos hacer unwrap() de un valor None:

Llamar directamente a unwrap() puede resultar en un error de ejecución. Una forma sencilla de solventar este error es usar unwrap_or(default), que devolverá un valor por defecto si Option es None.

Documentación sobre Option.

Pattern Matching

Rust tiene un mecanismo de control de búsqueda de patrones con el operador match.

Permite comparar un valor contra una serie de patrones y ejecutar código en el patrón que se cumpla. El compilador no nos dejará continuar si no cubrimos todas las ramas posibles.

Pero match no solo sirve para devolver un valor, podemos ejecutar código dentro de las ramas.

Documentación sobre Pattern Matching.

Pattern Matching con Option

Otra forma de utilizar el valor real dentro de un Option es con match.

También podemos utilizar match como anteriormente hemos usado unwrap_or()

Otra sintaxis disponible es if let, que nos permite contrastar un único patrón de manera más concisa.
Pero el compilador no nos obligará a cumplir todas las ramas.

Funciones

En Rust las funciones se escriben con la palabra fn:

Si la función tiene parámetros:

Si la función devuelve un valor, el valor debe estar en la última línea sin punto y coma.

En muchas ocasiones no será necesario especificar el tipo del resultado, ya que el compilador puede interpretar la función.

Documentación sobre funciones.

Métodos

Los métodos son funciones asociadas a una estructura. En Rust se utiliza impl para crear la implementación de un tipo.

Si queremos crear un método que tenga acceso a la instancia del objeto actual, usamos &self:

Si queremos modificar un campo de la instancia usamos &mut self:

Documentación sobre métodos.

Closures

Una closure, también conocida como expresiones lambda, es una función que puede capturar el entorno que la rodea. Esto significa que tiene acceso a las variables del contexto.

Tiene algunas diferencias con las funciones:

  • Utiliza || en lugar de () para las variables de entrada.
  • El cuerpo {} es opcional en expresiones únicas.
  • Puede capturar su entorno.

Documentación sobre closures.

Ownership

Las reglas de ownership dominan Rust con estas normas:

  • Todo valor tiene una propiedad llamada su dueño.
  • Solo puede tener un dueño en todo momento.
  • Cuando el dueño sale del contexto, el valor también lo es.

Estas reglas se encargan del control de la memoria en Rust sin necesitar un colector de basura.

Vamos a ver las reglas en acción.

Este código es bastante aparente. Se inicializa x a 5 y se inicializa y a x.

Es correcto presuponer que ambas variables van a tener valor 5. Pero esto se debe a que son de tipo entero, y tienen un tamaño fijo. Por lo que se guardan en el stack.

Ahora vamos a usar el tipo String, que es de un tamaño indeterminado y se guarda en el heap.

Parece que el funcionamiento será el mismo, pero no es así. Vamos a ver cómo se reparte el tipo String entre el stack y el heap.

 

String en Stack y Heap

Cuando se crea un String, se introducen en el stack la longitud, la capacidad y una referencia de memoria al heap, en el que se guardan el valor del String.

Cuando instanciamos string2 a partir de string1, se añade al stack una nueva variable con longitud, capacidad y la referencia apunta a la misma memoria que string1. Mientras que string1 se marca como invalido.

String en Stack y Heap

 

Si queremos mantener las dos variables válidas, debemos usar clone(). Hay que tener en cuenta que esta operación es mucho más cara que la anterior.

Ownership en las funciones

Si ejecutamos el siguiente ejemplo:

Recibiremos este error.

Esto se debe a una de las reglas que vimos anteriormente.

  • Todo valor tiene un único dueño.

Cuando se pasa s por parámetro a takes_ownership, la función pasa a ser su dueño.
Y cuando la función termina y sale del contexto, s pasa a ser invalido.

Por esto no podemos acceder a s después de takes_ownership.

En el caso de que necesitemos una operación parecida a la anterior, debemos usar referencias.

Referencias

Para marcar referencias usamos &.

Si queremos modificar una referencia, usamos &mut. Es importante saber que, si queremos modificar una referencia, debemos asegurarnos que el valor también es mutable.

Con las referencias vienen varias reglas que debemos seguir:

  • Puedes tener o una referencia mutable o múltiples referencias inmutables.
  • Las referencias siempre deben ser válidas.

Documentación sobre Ownership.

Testing

Rust viene con herramientas de testing automático de serie.

Para ejecutar los test, comando es cargo test:

Documentación sobre testing.

Conclusiones

Gracias por seguir está guía introductoria a Rust. Espero que haya sido educativa.

Si quieres seguir aprendiendo, te recomiendo el libro oficial 100% gratuito The Rust Programming Language.

Gracias!

 

Dejar respuesta

Please enter your comment!
Please enter your name here