Este es un curso de Rust pensado especialmente para personas con conocimientos en JavaScript, de forma que encontrarás muchas analogías útiles entre ambos lenguajes de programación, su sintaxis e incluso su ecosistema.
Introducción a Rust para desarrolladores JavaScript
¿Qué es Rust?
Rust es un lenguaje de programación de propósito general desarrollado originalmente por Mozilla en 2010 y ahora mantenido por la Fundación Rust. Al igual que JavaScript es multiparadigma, por lo que puedes usar programación funcional, imperativa o incluso orientada a objetos, entre otras.
A diferencia de JavaScript, para poder ejecutar un programa en Rust, primero debes compilarlo. Esto es debido a que Rust es un lenguaje de programación compilado, mientras que JavaScript es un lenguaje de programación interpretado (aunque en realidad existe un paso de compilación interno a nivel de motor de JavaScript).
En cuanto al tipado de Rust es estático y fuerte, todo lo contrario a JavaScript que es dinámico y débil. Esto significa que las variables se declaradan con un tipo de dato (o detecta automáticamente el tipo con inferencia) y que no se pueden cambiar de tipo de dato durante la ejecución del programa (esto es que su tipado es fuerte).
Rust ha sido diseñado para ser seguro y rápido, por lo que es un lenguaje de programación muy adecuado para el desarrollo de aplicaciones de alto rendimiento, como por ejemplo, servidores web, aplicaciones de escritorio, sistemas embebidos, etc. JavaScript, aunque también se puede usar para muchas de ellas, en realidad fue diseñado para añadir interactividad a las webs.
¿Por qué aprender Rust?
Rust ha ganado popularidad en los últimos años y se está convirtiendo en un lenguaje que, cada vez, se usa en más empresas y están surgiendo más oportunidades laborales. En el mundo de JavaScript, muchas herramientas están migrando de JavaScript a Rust (por paradógico que parezca) gracias a su velocidad.
Algunos ejemplos son SWC (alternative a Babel), Rome (alternative a Eslint y Prettier) o Turbopack (alternativa a Webpack).
Además, si ya sabes programar en JavaScript, seguramente estés buscando un lenguaje de programación para seguir mejorando tus habilidades. Rust es un lenguaje más avanzado que JavaScript en muchos aspectos y, seguramente, será un reto dominarlo… pero al hacerlo vas a mejorar tus conocimientos de programación en general y tu perfil será mucho más atractivo de cara a encontrar trabajo o mejorar tu salario.
Cómo instalar Rust
Si te encuentras en Linux o macOS, puedes instalar Rust con el siguiente comando:
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Al ejecutarlo, se descargará un script que instalará rustup. Para que te hagas una idea, rustup sería el equivalente a nvm para poder instalar y administrar diferentes versiones de Node en tu sistema.
Si lo prefieres, puedes probar Rust sin instalarlo con este playground.
Ahora, vamos a revisar que la instalación ha sido correcta y que ya podemos ejecutar el compilador de Rust:
$ rustc --version
rustc 1.66.0 (69f9c33d7 2022-12-12)
Si te aparece que el comando no ha sido encontrado, seguramente la instalación no ha dejado correctamente el binario en el PATH. Prueba a cerrar la terminal y abrirla de nuevo, a veces eso lo soluciona. Si no, puedes añadir la ruta a mano con
export PATH="$PATH:$HOME/.cargo/bin"
.
Como ves, en Rust tenemos un compilador oficial mientras que en JavaScript no tenemos (ya que son los entornos de ejecución de los navegadores, Node, Deno y similares que compilan y evalúan al vuelo nuestro código).
Hola mundo en Rust
Para trabajar con Rust en el editor Visual Studio Code, te recomiendo que instales la extensión Rust Analyzer. Esta extensión nos proporciona autocompletado, linting, formateo y muchas cosas más que te facilitarán la vida trabajando con el lenguaje de programación.
Ahora que ya la tienes, vamos a escribir nuestro primer programa en Rust. Para ello, crea un fichero llamado hello-world.rs
y escribe el siguiente código:
fn main() {
println!("Hello World");
}
Ahora, para compilarlo, ejecuta el siguiente comando:
$ rustc hello-world.rs
Si todo ha ido bien, se habrá generado un fichero ejecutable llamado hello-world
en la misma carpeta, que puedes ejecutar con el siguiente comando:
$ ./hello-world
Hello World
Este es tu primer programa con Rust. Ahora, vamos a diseccionar el código para entenderlo mejor.
La función main
Nuestro código comienza con un fn
. Esta es la forma en la que se declaran las funciones en Rust. Después indicamos el nombre de la función como main
. Este nombre no es una casualidad y es que cualquier aplicación o programa que escribamos en Rust debe tener siempre una función main
.
Es el punto de entrada de nuestra aplicación y será la primera función que se llamará al ejecutar el binario que hemos compilado. Esta es una gran diferencia respecto a JavaScript ya que no tiene este concepto.
En cualquier caso, si intentas compilar el código anterior sin la función main
o con otro nombre, obtendrás el siguiente error:
rustc hello-world.rs
error[E0601]: `main` function not found in crate `hello_world`
--> hello-world.rs:3:2
|
3 | }
| ^ consider adding a `main` function to `hello-world.rs`
error: aborting due to previous error
Así que es imposible que se te pueda olvidar esta regla.
En Rust también existe algo similar a las Arrow Function, que se llama Closure. Lo veremos más adelante en el curso.
Mostrando mensajes en la consola con println!
Mientras que en JavaScript usamos el mítico método console.log
, en Rust usamos println!
. El objetivo es similar pero la particularidad es que println!
no es una función. Es un macro.
Aunque más adelante exploraremos este concepto, un macro no es nada más que una forma de generar código en tiempo de compilación. Como en JavaScript no tenemos compilación, tampoco existe este concepto, pero lo más parecido, si sabes algo de React, sería pensar en JSX. Tu escribes código JSX y React lo transforma en código JavaScript en un paso de compilación (con Babel, SWC o TypeScript):
const App = () => <h1>Hello World</h1>;
// compila en:
var _jsxRuntime = require("react/jsx-runtime");
const App = () =>
/*#__PURE__*/ (0, _jsxRuntime.jsx)("h1", {
children: "Hello World",
});
En Rust, el compilador transforma el código que escribimos en código que puede ejecutar el sistema operativo. Por eso, cuando escribimos println!
, el compilador genera el código necesario para mostrar el mensaje en la consola.
Para diferenciar un macro de una función, sólo tienes que fijarte en la exclamación antes de los paréntesis.
Cadenas de texto
En Rust, las cadenas de texto se declaran entre comillas dobles "
. Las comillas simples '
se usan sólo para declarar caracteres por lo que no pueden usarlas para declarar cadenas de texto (algo que en JavaScript sí está permitido).
En Rust puedes escapar las comillas dobles con \
:
fn main() {
println!("Hello \"World\"");
}
Algo muy interesante de las cadenas de texto en Rust es que, igual que los template string de JavaScript, respeta los saltos de línea dentro de la cadena de texto.
Prueba a compilar y ejecutar este código y fíjate en la salida:
fn main() {
println!("Hello
World");
}
Más adelante hablaremos más de las cadenas de texto en Rust porque vienen con un montón de diferencias con JavaScript que no se aprecian en la superficie…
Puntos y coma
En Rust, los puntos y coma no son opcionales (a diferencia de JavaScript). Todas las sentencias de Rust deben terminar con un punto y coma… con una excepción que veremos más adelante en el curso.
Comentarios
Como es tu primera vez escribiendo código en Rust, vamos a utilizar a lo largo del curso anotaciones para que puedas entender qué hace cada línea. Y, para ello, vamos a usar comentarios en el código.
Los comentarios son muy similares a JavaScript, son anotaciones que no interfieren en la ejecución del programa y podemos describir lo que hace nuestro código o dar un mejor contexto del mismo.
En Rust podemos usar la misma sintaxis que JavaScript para crear comentarios de una línea con //
y multilínea con /* */
:
fn main() {
// Esto es un comentario de una línea
// Y podemos describir que mostramos
// Hello World en la consola
println!("Hello World");
/*
Esto es un comentario
de varias líneas
*/
}
Más adelante veremos otro tipo de comentarios específicos de Rust que nos ayudará a documentar nuestros programas pero, por ahora, usaremos sólo estos tipos.
Variables
Como cualquier lenguaje de programación, en Rust también podemos crear variables y, aunque la sintaxis nos puede parecer muy similar a JavaScript, debemos de tener cuidado porque el comportamiento puede ser muy distinto.
let
Para crear una variable en Rust podemos usar la palabra clave let
, igual que en JavaScript.
fn main() {
let name = "midu";
}
Como ves, no hemos tenido que indicar el tipo de la variable. Rust es un lenguaje de tipado estático y el compilador puede inferir el tipo de la variable en tiempo de compilación. Lo mismo que hace TypeScript.
Sin embargo, let
no funciona igual en Rust que JavaScript y debes tener cuidado. La diferencia es que en Rust las variables que creamos son inmutables por defecto. Esto quiere decir que no puedes reasignar el valor de una variable después de asignarle uno.
Por ejemplo, el siguiente código no compilará y te dará un error:
// filename: let.rs
// este código no compila y sirve sólo de ejemplo
fn main() {
let name = "midu";
name = "midudev";
}
$ rustc let.rs
error[E0384]: cannot assign twice to immutable variable `name`
--> let.rs:3:3
|
2 | let name = "midu";
| ----
| |
| first assignment to `name`
| help: consider making this binding mutable: `mut name`
3 | name = "midudev";
| ^^^^^^^^^^^^^^^^ cannot assign twice to immutable variable
Además del error indicando que la variable es inmutable, también verás dos advertencias indicando que estás creando una variable pero no la estás usando. Para que veas lo potente que es el compilador de Rust y cómo nos ayuda a hacer nuestro código más óptimo.
Como ves, por defecto, no puedes reasignar el valor de una variable let
. Pero, ¿qué pasa si quieres crear una variable que sí que pueda ser reasignada? En el mensaje de error ya nos indica que podemos añadir la palabra clave mut
para que la variable sea mutable:
fn main() {
let mut name = "midu";
name = "midudev";
}
Este código es válido, ya que le estamos indicando que la variable es mutable y luego le estamos reasignando otra cadena de texto.
Asignar más tarde
Al crear nuestras variables, no es obligatorio que hagamos la asignación desde el principio. Si no usamos mut
, sólo podremos hacer la asignación una vez pero no significa que tengamos que hacer la asignación justamente cuando declaramos la variable. Por ejemplo, este código es válido:
fn main() {
// declaramos la variable
let name;
// asignamos el valor después
name = "midudev";
}
Tipado estático y fuerte
Tienes que tener en cuenta que, aunque uses mut
, no puedes cambiar el tipo de dato de una variable al reasignarla. Eso siempre será un error. Si al principio le asignaste un número y después intentas asignarle una cadena de texto, el compilador de dará problemas:
fn main() {
// creamos una variable mutable
// y le asignamos un número
let mut name = 1;
// después una cadena de texto
name = "midudev";
}
Al compilar nos indicará que el tipo de dato de la variable no es el mismo que el que le estamos asignando:
$ rustc let.rs
error[E0308]: mismatched types
--> let.rs:3:10
|
2 | let mut name = 1;
| - expected due to this value
3 | name = "midudev";
| ^^^^^^^^^ expected integer, found `&str`
error: aborting due to previous error
const
En Rust también existen las constantes pero, de nuevo, su comportamiento es muy diferente al de JavaScript.
Cuando creas una constante con const
en Rust estás creando un valor constante en tiempo de compilación. Esto significa que no hay forma de cambiar o mutar su valor en tiempo de ejecución (algo que sí es posible en JavaScript al, por ejemplo, añadir una propiedad a un objeto que creaste con const
).
Al ser valores que quedan fijados en tiempo de compilación, Rust tampoco infiere el tipo y es obligatorio que indiquemos de qué tipo de dato se trata.
// filename: constant.rs
const LUCKY_NUMBER = 7;
fn main() {
println!("El número es {LUCKY_NUMBER}");
}
Si intentamos compilar este código, el compilador nos indicará que no hemos indicado el tipo de dato de la constante y nos dará una pista de cómo arreglarlo:
$ rustc constant.rs
error: missing type for `const` item
--> constant.rs:1:19
|
1 | const LUCKY_NUMBER = 7;
| ^ help: provide a type for the constant: `: i32`
error: aborting due to previous error
Vamos a arreglar el problema indicando que es un tipo i32
. Significa que se trata de un entero de 32 bits que puede tener valores positivos y negativos. Más adelante veremos que existen otro tipo de datos mejores para este caso pero por ahora usaremos este:
// filename: constant.rs
const LUCKY_NUMBER: i32 = 7;
fn main() {
println!("El número es {LUCKY_NUMBER}");
}
Si compilamos y ejecutamos el código, veremos que ahora la consola nos indica:
El número es 7
Interpolación de variables
En el código anterior has visto que hemos usado la interpolación de variables para mostrar el valor de la constante en la consola. En Rust la interpolación de variables se hace con llaves ({}
) y dentro de ellas el nombre de la variable:
fn main() {
let name = "midu";
println!("Hola, {name}");
}
Si ejecutamos el código, veremos que la consola nos indica:
Hola, midu
También podemos hacer la interpolación dejando placeholders usando las llaves vacías ({}
) y luego indicando el valor de la variable en el método println!
como segundo argumento:
fn main() {
let name = "midu";
println!("Hola, {}", name);
}
Puedes usarlo tantas veces como quieras:
const LUCKY_NUMBER: i32 = 7;
fn main() {
let name = "midu";
println!("El número de la suerte de {} es {}", name, LUCKY_NUMBER);
}
Al ejecutarlo, veremos que la consola nos indica:
El número de la suerte de midu es 7
Tipos de datos
Rust es un lenguaje con tipado estático por lo que debe, en tiempo de compilación, el tipo de cualquier valor que uses en el código.
Como hemos visto en ejemplos anteriores, a veces no es necesario indicar el tipo ya que Rust lo puede inferir gracias al valor que le estamos asignando. Pero en otros casos sí es necesario.
Tipos de datos primitivos
Igual que en JavaScript, en Rust también tenemos datos primitivos. Se le llaman así porque estos datos son la construcción básica de los lenguajes de programación. Son los tipos de datos más simples que existen y que no se pueden descomponer en otros más pequeños.
Sin embargo, en Rust estos primitivos se dividen en dos grupos: los escalares y los compuestos.
Tipos escalares
En este grupo tienes todos los tipos que representan un único valor y Rust tiene cuatro:
- bool: Para guardar un valor cierto o falso con true o false respectivamente.
- char: Un carácter Unicode de 4 bytes envuelto en comillas simples.
- integers: Un número entero (que puede ser de diferentes tamaños).
- float: Un número decimal (que puede ser de diferentes tamaños).
Antes de entrar a detallar cada uno, ya podemos ver algunas diferencias con JavaScript.
Primero, en Rust la cadena de texto no es un tipo escalar (aunque sí es un primitivo). Esto es porque, como ves, existe el escalar a nivel de caracter (char
) por lo que una cadena de texto es una construcción a base de caracteres. Por eso está en el grupo de los compuestos.
También, en Rust no tenemos un sólo tipo de número. En JavaScript tenemos un único tipo de número que es el number
y que puede ser entero o decimal. En Rust tenemos dos tipos de números: integers
y floats
y cada uno tiene diferentes tamaños en bytes como veremos a continuación.
Tampoco hay rastro de los tipos null
y undefined
que existen en JavaScript. En Rust no hay valores nulos. Si quieres representar un valor nulo, puedes usar el tipo Option
que veremos más adelante.
Booleanos
Los booleanos son los valores true
y false
que representan la verdad y la falsedad. Ocupan un solo byte y en Rust se representan con el tipo bool
:
fn main() {
// indicar el tipo es opcional
// ya que Rust lo puede inferir
let is_active = true;
// pero también podemos indicarlo
// si lo preferimos
let is_greater: bool = 10 > 5;
}
Este tipo escalar funciona de forma idéntica a JavaScript. Por ejemplo, podemos usarlo en una condición:
fn main() {
let is_active = true;
if is_active {
println!("Está activo");
}
}
Caracteres
Los caracteres son los valores que representan un carácter Unicode y ocupan 4 bytes. Se debe envolver con comillas simples. En Rust se representan con el tipo char
:
fn main() {
// indicar el tipo es opcional
// ya que Rust lo puede inferir
let letter = 'a';
// pero también podemos indicarlo
// si lo preferimos
let letter: char = 'a';
}
Este es un tipo de dato que no existe en JavaScript, donde solo tenemos cadenas de texto. Así que en Rust ya ves que las comillas simples se usan para caracteres y las dobles para cadenas de texto.
Números enteros
Representa un número entero (un número que no tiene decimales). Como Rust es un lenguaje pensado para tener el mejor rendimiento, los números enteros pueden ser de diferentes tamaños en bytes.
Así que a la hora de tipar un número deberemos tomar dos decisiones:
- El número de bytes que queremos que ocupe el número entero.
- Si queremos que el número sea positivo o negativo.
Los tipos disponibles para representar números enteros en Rust se representan con los tipos i8
, i16
, i32
, i64
, i128
y isize
(para números enteros positivos y negativos) o u8
, u16
, u32
, u64
, u128
y usize
(para números enteros positivos).
Por ejemplo, con i8
podremos representar números que van del -128 al 127. Con u8
podremos representar números que van del 0 al 255. Con i32
podremos representar números que van del -2.147.483.648 al 2.147.483.647. Con u32
podremos representar números que van del 0 al 4.294.967.295.
isize
y usize
son tipos que dependen de la arquitectura de la máquina donde se ejecute el programa. En una máquina de 64 bits, isize
y usize
serán de 64 bits.
fn main() {
// si dejamos que Rust infiera el tipo
// lo hará como i32
let number = 10;
// pero podemos indicar el tipo nosotros
// si sabemos qué números representaremos
let number: u8 = 10;
}
Como ves, en Rust no existe un único tipo de número entero. En JavaScript tenemos number
que puede ser entero o decimal. En Rust tenemos integers
y floats
y cada uno tiene diferentes tamaños en bytes.
Punto flotante
Los números con punto flotante nos permite representar números con mayor precisión y, por lo tanto, pueden contener fracciones (decimales).
En este caso sólo podemos tener de 32 y 64 bits con los tipos f32
y f64
.
fn main() {
// si dejamos que Rust infiera el tipo
// lo hará como f64 por defecto
let number = 10.5;
// pero podemos indicar el tipo nosotros
// si sabemos qué números representaremos
let number: f32 = 10.5;
}
Con f32
puedes represntar números que van del -3.4028235e+38 al 3.4028235e+38. Con f64
puedes representar números que van del -1.7976931348623157e+308 al 1.7976931348623157e+308.
Tipos compuestos
Dentro de los tipos primitivos, también tenemos los tipos compuestos. Estos tipos nos permiten agrupar valores de otros tipos y tenemos dos tipos de datos compuestos: los arrays y las tuplas.
Arrays
Los Arrays en JavaScript y Rust son similares. En ambos casos, son estructuras de datos que nos permiten crear una lista de elementos.
El siguiente código es válido en ambos lenguajes de programación:
let avengers = ["Iron Man", "Thor", "Hulk", "Captain America"];
Sin embargo hay dos diferencias clave entre los arrays de JavaScript y los arrays de Rust:
- En JavaScript, los Arrays son dinámicos, es decir, podemos cambiar su longitud en cualquier momento. En Rust los Arrays tienen un tamaño fijo y una vez creados no se pueden modificar.
- En JavaScript los Arrays pueden contener elementos de diferentes tipos. En Rust los Arrays solo pueden contener elementos de un mismo tipo.
// ❌ este código es incorrecto
// ya que un Array en Rust necesita
// que todos sus elementos sean del mismo tipo
fn main() {
let avengers = ["Iron Man", 2, "Black Widow"];
}
Si intentas compilar el código anterior, verás que Rust te mostrará un error de compilación error[E0308]: mismatched types
.
Mutabilidad de Array
Como hemos visto antes, las variables que creamos con let
en Rust son inmutables por defecto. Esto también se aplica a los Arrays. Si intentamos modificar un elemento de un Array, Rust nos mostrará un error de compilación.
// ❌ Este código es incorrecto porque
// intentamos modificar un elemento de un Array
// que es inmutable
fn main() {
let midu = ["Miguel", "Duran"];
midu[0] = "Miguel Ángel";
}
Pero podemos crear Arrays mutables usando la palabra clave mut
:
// ✅ Este código es correcto porque
// creamos un Array mutable
fn main() {
let mut midu = ["Miguel", "Duran"];
midu[0] = "Miguel Ángel";
}
Como ves, para acceder a una posición de un Array usamos la misma notación que en JavaScript: array[index]
.
Tamaño de Array
En JavaScript, la longitud de un Array se puede obtener usando la propiedad length
. En Rust, para obtener el tamaño de un Array usamos la función len()
:
fn main() {
let fav_movies = ["Avengers", "Iron Man", "Thor"];
println!("Tengo {} películas favoritas", fav_movies.len());
}
Tuplas
Las tuplas son un tipo de dato que nos permite agrupar valores de distintos tipos. Al igual que los Array, las tuplas tienen un tamaño fijo y una vez creadas no se pueden modificar.
Es un tipo de dato que no existe en JavaScript (lo más parecido sería un Array de tamaño fijo y de distintos tipos de elementos).
fn main() {
// indicar el tipo es opcional
// ya que Rust lo puede inferir
let videoconsole = ("PS5", 550, true);
// pero también podemos indicarlo
// si lo preferimos:
let videoconsole: (&str, i32, bool) = ("PS5", 550, true);
}
Para acceder a los valores de una tupla podemos usar la notación de punto y el índice del valor que queremos obtener:
fn main() {
let videoconsole = ("PS5", 550, true);
let name = videoconsole.0;
let price = videoconsole.1;
let is_latest = videoconsole.2;
}
Igual que en los Arrays, si creamos la variable con mut
podremos modificar los valores de la tupla. Pero, recuerda, no podemos modificar su tamaño ni guardar un elemento de un tipo distinto al que ya tenía esa posición.
fn main() {
let mut videoconsole = ("PS5", 550, true);
videoconsole.0 = "Xbox Series X";
videoconsole.1 = 499;
videoconsole.2 = true;
// ❌ la siguiente línea sería incorrecta
// ya que estamos intentando guardar un valor
// de tipo &str en una posición que ya tiene
// un valor de tipo i32
videoconsole.1 = "Mucho dinero";
}
Desestructuración de tuplas y Arrays
Al igual que en JavaScript, podemos desestructurar tuplas y Arrays para obtener sus valores de forma más sencilla.
fn main() {
// desestructuración de una tupla
let game = ("God of War", 70);
let (game_name, game_price) = game;
println!("El juego {game_name} cuesta {game_price}€");
// desestructuración de un Array
let languagues = ["JavaScript", "Rust", "Python"];
let [js, rust, python] = languagues;
println!("Los lenguajes son {js}, {rust} y {python}");
}
A veces, no nos interesa obtener todos los valores de una tupla o un Array, sino solo algunos. En estos casos podemos usar la desestructuración y usar _
para indicar que no nos interesa ese valor:
fn main() {
// desestructuración de una tupla
let game = ("God of War", 70);
let (_, game_price) = game;
println!("El juego cuesta {game_price}");
// desestructuración de un Array
let languagues = ["JavaScript", "Rust", "Python"];
let [js, _, python] = languagues;
println!("Aprende {js} y {python}");
}
Cuando usamos
_
en una desestructuración, Rust no crea una variable para ese valor. Es como si no estuviera ahí.
¿Qué pasa si sólo quieres obtener el primer valor de una tupla o un Array? Aquí hay una diferencia importante con JavaScript: en Rust siempre debemos indicar qué hacemos con el resto de valores. Si no lo hacemos, Rust nos mostrará un error de compilación.
fn main() {
// ❌ Esto es incorrecto porque no indicamos
// qué hacemos con el resto de valores
let game = ("God of War", 70);
let (game_name) = game;
// ✅ Esto es correcto porque indicamos
// qué hacemos con el resto de valores
let game = ("God of War", 70);
let (game_name, _) = game;
}
Seguramente te estarás preguntando… ¿Qué pasa si la Tupla o Array tiene más valores? ¿Tengo que usar _
para todos ellos? ¿Y qué pasa si no conozco exactamente el tamaño de la Tupla o Array?
En estos casos, podemos usar ..
para indicar que no nos interesa el resto de valores
fn main() {
// desestructuración de una tupla
let game = ("God of War", 70, "PS4");
let (game_name, ..) = game;
println!("El juego es {game_name}");
// desestructuración de un Array
let languagues = ["JavaScript", "Rust", "Python", "Java"];
let [js, ..] = languagues;
println!("Aprende {js}");
}
Con ..
estamos indicando que el resto de valores no nos interesan. Pero, ¿qué pasa si queremos obtener el último valor de una tupla o un Array? En este caso, podemos usar ..
al final de la desestructuración:
fn main() {
// desestructuración de una tupla
let game = ("God of War", 70, "PS4");
let (.., game_platform) = game;
println!("El juego es para {game_platform}");
// desestructuración de un Array
let languagues = ["JavaScript", "Rust", "Python", "PHP"];
let [.., php] = languagues;
println!("Aprende {php}");
}
Funciones
En Rust, las funciones se definen con la palabra reservada fn
y ya hemos visto algunos ejemplos:
fn main() {
// aqui va el código de la función
}
Mientras que en JavaScript usamos la palabra function
, aquí usamos fn
. Por lo demás, es muy similar.
Vamos a crear nuestra primera función fuera de main
que al recibir un número, nos devuelva el doble de ese número:
fn multiply_by2(number: i32) -> i32 {
return number * 2
}
Ya empezamos a ver más diferencias importantes respecto a JavaScript.
En Rust, como en JavaScript, las funciones pueden recibir parámetros. Pero aquí necesitamos indicar el tipo de dato que reciben. En este caso, la función multiply_by2
recibe un número entero (i32
).
También es obligatorio que indiquemos el tipo de dato que devuelve la función. En este caso, la función multiply_by2
devuelve un número entero (i32
).
Así que sí, para que nuestras funciones de Rust funcionen necesitamos indicar los tipos de los parámetros y del resultado.
En Rust, las funciones siempre deuvuelven algo. Por defecto, si no indicamos nada, devuelven
()
, una tupla vacía que se le conoce comounit
.
Vamos a usar nuestra función multiply_by2
en main
:
fn multiply_by2(number: i32) -> i32 {
return number * 2
}
fn main() {
let result = multiply_by2(5);
println!("El resultado es {result}");
}
¿Por qué no hemos usado camelCase para el nombre de la función? En Rust, el estilo de nombrado de funciones recomendado es con snake_case.
Múltiples parámetros
En Rust, las funciones pueden recibir múltiples parámetros. En este caso, la función multiply
recibe dos números enteros (i32
) y devuelve un número entero (i32
):
fn multiply(number1: i32, number2: i32) -> i32 {
return number1 * number2
}
fn main() {
let result = multiply(5, 2);
println!("El resultado es {result}");
}
La separación de parámetros se hace con ,
, igual que en JavaScript. Pero en Rust debemos indicar el tipo de dato de cada parámetro.
Retorno implícito
En Rust, las funciones pueden devolver el resultado sin usar la palabra reservada return
. En este caso, el resultado de la función es el último valor que se evalúa en la función:
fn multiply(number1: i32, number2: i32) -> i32 {
number1 * number2
}
Fíjate que en este caso, no hemos usado return
ni tampoco hemos terminado la función con ;
. No debes usar ;
al final de la sentencia, de lo contrario Rust no sabrá que el resultado de la función es el último valor que se evalúa.
fn multiply(number1: i32, number2: i32) -> i32 {
number1 * number2; // ❌ Esto es incorrecto
}
¿Existen las funciones anónimas y arrow function en Rust? Existe algo parecido llamado closures, pero no es exactamente lo mismo. Lo veremos más adelante cuando hablemos de la programación funcional en Rust.
Flujo de ejecución
En ocasiones necesitamos controlar el flujo de ejecución de nuestro código.
A veces, queremos que se ejecute una parte de código si se cumple una condición y otra parte de código si no se cumple.
Otras veces, queremos que se ejecute una parte de código tantas veces como se cumpla una condición.
Esto lo logarmos con condicionales y bucles.
Condicionales con if
Igual que en JavaScript, en Rust podemos controlar el flujo de ejecución con if
:
fn main() {
let number = 5;
if number > 5 {
println!("El número es mayor que 5");
} else if number < 5 {
println!("El número es menor que 5");
} else {
println!("El número es 5");
}
}
Una de las diferencias más importantes es que en Rust no necesitamos usar paréntesis para indicar la condición del if
. En este caso, la condición es number > 5
.
Puedes usar paréntesis si lo prefieres, pero no es obligatorio y no es recomendable (el compilador te dará una advertencia ya que por defecto está configurado para que no los uses).
warning: unnecessary parentheses around `if` condition
--> src/main.rs:4:6
|
4 | if (number > 5) {
| ^ ^
|
= note: `#[warn(unused_parens)]` on by default
La otra gran diferencia respecto a JavaScript es que if
en Rust es una expresión (en JavaScript es una declaración que no devuelve un resultado).
Esto significa que podemos usar if
para, por ejemplo, asignar un valor a una variable:
fn check_is_odd_or_even(number: i32) {
let result = if number % 2 == 0 { "par" } else { "impar" };
println!("El número es {result}");
}
fn main() {
check_is_odd_or_even(10)
}
Como ves, hemos asignado a la variable result
el resultado de la expresión if
. En este caso, el resultado es un string
.
En JavaScript, para lograr algo similar, necesitamos usar una ternaria:
// JavaScript
const checkIsOddOrEven = (number) => {
const result = number % 2 === 0 ? "par" : "impar";
console.log(`El número es ${result}`);
};
En Rust no existe switch
, existe match
En Rust existe el operador match
que viene a sustituir al switch
de JavaScript. En este caso, vamos a comprobar si un número es par o impar:
fn check_is_odd_or_even(number: i32) {
match number % 2 {
0 => println!("El número es par"),
1 => println!("El número es impar"),
_ => println!("El número no es ni par ni impar"),
}
}
fn main() {
check_is_odd_or_even(10)
}
La sintaxis de match
es similar a la de if
y mucho más agradable que la de switch
de JavaScript. Además, es mucho más declarativa.
El match
es una expresión que evalúa el valor que le pasamos como parámetro y compara con los valores que le pasamos en cada uno de los casos.
En este caso, el valor que le pasamos es number % 2
y comparamos con los valores 0
y 1
.
Si el valor que le pasamos es 0
, se ejecuta el código que está dentro del primer caso (0 => println!("El número es par")
).
Si el valor que le pasamos es 1
, se ejecuta el código que está dentro del segundo caso (1 => println!("El número es impar")
).
Si el valor que le pasamos no es ni 0
ni 1
, se ejecuta el código que está dentro del tercer caso (_ => println!("El número no es ni par ni impar")
).
¿Qué es el
_
? Es un placeholder que se usa para indicar que el valor puede ser cualquier cosa. En este caso, si el valor que le pasamos no es ni0
ni1
, se ejecuta el código que está dentro del tercer caso.
También match
devuelve el valor que le indiquemos y es muy útil para asignarlo a una variable:
fn check_is_odd_or_even(number: i32) -> String {
match number % 2 {
0 => "par".to_string(),
1 => "impar".to_string(),
_ => "no es ni par ni impar".to_string(),
}
}
Bucles en Rust
En Rust tenemos tres formas de crear bucles: loop
, while
y for
.
loop
El bucle loop
es similar a while (true)
en JavaScript. Se ejecuta hasta que se rompe con break
. En el siguiente ejemplo, vamos a imprimir por consola los números del 1 al 5 y, al llegar a 5, paramos el bucle:
fn main() {
let mut number = 0;
loop {
println!("El número es {number}");
number += 1;
if number == 5 {
break;
}
}
}
Igual que con el if
, podemos devolver un valor en el loop
y asignarlo a una variable:
fn main() {
let mut number = 1;
let result = loop {
number += 1;
if number == 5 {
break "El número es 5";
}
};
println!("{result}");
}
En este ejemplo hemos usado break
con un valor. Esto hace que el bucle se rompa y se devuelva el valor que hemos indicado. Este valor, que es una cadena de texto, lo hemos asignado a la variable result
.
Así que al usar println!
con la variable result
, se imprimirá por consola El número es 5
.
while
El bucle while
es similar a while
en JavaScript. Se ejecuta mientras la condición sea verdadera. En el siguiente ejemplo, vamos a imprimir por consola los números del 1 al 5:
fn main() {
let mut number = 0;
while number < 5 {
println!("El número es {number}");
number += 1;
}
}
La diferencia con JavaScript, al igual que el if
, es que no necesita paréntesis para indicar la condición.
for in
A la hora de iterar sobre una colección de elementos, podemos usar un índice con while
:
fn main() {
let numbers = [1, 2, 3, 4, 5];
let mut index = 0;
while index < numbers.len() {
println!("El número es {numbers[index]}");
index += 1;
}
}
Esto es similar a lo que hacemos en JavaScript con while
de esta manera:
// JavaScript
const numbers = [1, 2, 3, 4, 5];
let index = 0;
while (index < numbers.length) {
console.log(`El número es ${numbers[index]}`);
index += 1;
}
Sin embargo, el código queda demasiado verboso y, además, nos obliga a crear una variable index
que no vamos a usar más que para iterar.
En Rust, podemos usar for
para iterar sobre una colección de elementos. Este for
es similar al for...of
de JavaScript:
fn main() {
let numbers = [1, 2, 3, 4, 5];
for number in numbers {
println!("El número es {number}");
}
}
Esto genera una variable number
que toma el valor de cada elemento de la colección numbers
en cada iteración. En este caso, la variable number
toma los valores 1
, 2
, 3
, 4
y 5
en cada una de las itearciones. En JavaScript, esto sería similar a:
// JavaScript
const numbers = [1, 2, 3, 4, 5];
for (const number of numbers) {
console.log(`El número es ${number}`);
}
Otra forma de usar for
es indicando un rango de números. Lo podemos indicar con ..
. Por ejemplo si queremos los números del 1 al 6, podemos usar 1..6
. Los números del 1 al 6 son: 1, 2, 3, 4, 5.
Sé que puede parecer un poco raro que le digamos hasta el 6 y no lo incluya (así también funciona el método .slice
de JavaScript) así que si lo prefieres, puedes usar ..=
para indicar que el último elemento también debe incluirlo.
Así que si queremos los números 1
, 2
, 3
, 4
y 5
, podemos usar 1..=5
:
fn main() {
// for number in 1..6 {
for number in 1..=5 {
println!("El número es {number}");
}
}
Así que 1..6
y 1..=5
son equivalentes.
Con esto ya puedes hacer una cuenta atrás gracias al método rev()
de las tuplas (parecido al .reverse
de los Array en JavaScript):
fn main() {
for second in (1..=10).rev() {
println!("Despegue en {second}...");
}
println!("Despegue!!!");
}
Enums
Los Enums nos permite definir constantes nombradas y son útiles cuando queremos definir un conjunto de valores que son posibles para una variable. Por ejemplo, para el día de la semana:
enum Day {
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday,
}
Tiene sentido que Day
sea un enum ya que sus valores son finitos, constantes y pueden ser nombrados. Si queremos usarlo, podemos crear una variable de tipo Day
y asignarle un valor:
fn main() {
let day = Day::Monday;
}
Si compilas el código verás que te muestra advertencias. Esto es porque no estamos usando todas las variantes del enum. Para evitar esto, podríamos usar el atributo
#[allow(dead_code)]
antes de la definición del enum.
Para acceder a los valores de un enum, usamos el operador ::
y el nombre del valor. En este caso, Day::Monday
.
En JavaScript no existen los enums pero podemos simularlos con objetos:
// JavaScript
const Day = {
Monday: "Monday",
Tuesday: "Tuesday",
Wednesday: "Wednesday",
Thursday: "Thursday",
Friday: "Friday",
Saturday: "Saturday",
Sunday: "Sunday",
};
Donde sí existen los enums son en TypeScript que funcionan de forma muy similar a los de Rust e incluso su sintaxis es idéntica.
Variantes
Cada valor de un enum se llama variante. Lo interesante de las variantes es que también pueden ser dinámicas. Con los días ya sabemos de antemano sus nombres y sabemos que son finitos. Pero también podríamos tener variantes dinámicas.
Imagina que queremos representar los colores. En un sistema RGB
tenemos los colores Red
, Green
y Blue
. Pero también podríamos tener un color Rgb
que es un color compuesto por los valores Red
, Green
y Blue
en una escala de 0 a 255.
enum Color {
Red,
Green,
Blue,
Rgb(u8, u8, u8)
}
fn main() {
let red = Color::Red;
let dark_blue = Color::Rgb(0, 0, 100);
}
En este caso, Color::Red
es una variante estática y Color::Rgb(0, 0, 100)
es una variante dinámica que hemos creado con los valores 0
, 0
y 100
.
El tipo de dato es u8
ya que los valores RGB van del 0 al 255 y u8
es un tipo de dato que representa un número entero sin signo de 8 bits (0 a 255).
Sígueme en Twitch para saber cuanto llega la próxima entrega.
De vuelta con los Strings
Ya hemos trabajado anteriormente con los Strings en Rust y hemos visto que, visualmente, son muy similares a los Strings de JavaScript. Sin embargo, existen diferencias más profundas que debes conocer…
Para empezar, cuando hablamos de Strings en Rust hay que diferenciar entre dos tipos de Strings: &str
y String
.
&str
En este caso hablamos de una cadena de texto inmutable, con la longitud fija y que no puede ser modificada. Y este es el tipo de dato que hemos visto hasta ahora.
fn main() {
// Aquí inferimos el tipo de la cadena de texto
let hello = "Hola";
// Y lo que estamos infiriendo es un &str
let world : &str = "Mundo";
let hello_world = hello + " " + world;
println!("{}", hello_world);
}
A este tipo de dato se le conoce como String Slice, en español sería como Trozo de String.
Como hemos visto, podemos concatenar dos String Slices con el operador +
y el resultado es un nuevo String Slice, pero en ningún momento hemos modificado los String Slices originales.
Su comportamiento es muy parecido a los String de JavaScript de tipo primitivo, ya que tampoco podemos mutarlo una vez creado y siempre tenemos que crear un nuevo String.
const hello = "Hola";
const world = "Mundo";
const helloWorld = hello + " " + world;
console.log(helloWorld);
Por ahora no te preocupes por el símbolo
&
que aparece delante del tipo de dato. Lo veremos más adelante.
String
El segundo tipo es String
y es un tipo de dato que representa una cadena de texto mutable, con la longitud variable y que puede ser modificada.
fn main() {
// Creamos un String a partir del String Slice
let mut hello = String::from("Hola");
// Vamos a modificar la longitud de la cadena de texto
hello.push_str(", Mundo!");
// Mostramos en pantalla el String
println!("{hello}");
}
Vamos a ver qué está pasando en este código paso a paso:
let mut hello = String::from("Hola");
Creamos una variable let
llamada hello
y le asignamos un valor de tipo String
. Para crear un String
usamos el método from
del tipo String
y le pasamos un &str
como parámetro.
¿Qué es el ::
? Es un operador que nos permite acceder a un namespace
y acceder a sus métodos. En este caso, estamos accediendo al namespace
String
y al método from
.
Además, fíjate que estamos creando la variable como mutable, es decir, que podemos modificarla. Si no la creamos como mutable, obtendremos un error, ya que más adelante estamos intentando modificar el valor de la variable con el push_str
.
hello.push_str(", Mundo!");
El método push_str
nos permite añadir un &str
al final del String
. En este caso, estamos añadiendo la cadena de texto ", Mundo!"
. Gracias a que la variable es mut
, no tenemos ningún problema en modificar el valor de la variable.
println!("{hello}")
Y finalmente, mostramos en pantalla el valor de la variable hello
.
Para que veas la diferencia entre un &str
y un String
, vamos a ver un ejemplo:
// ❌ Código incorrecto
fn main() {
let mut hello: &str = "Hola";
hello.push_str(", Mundo!");
println!("{hello}");
}
Este código no compilará, ya que no podemos modificar un &str
y no tiene el método push_str
. Si queremos modificar un &str
, tenemos que convertirlo a String
:
// ✅ Código correcto
fn main() {
let str: &str = "Hola";
let mut hello = String::from(str);
hello.push_str(", Mundo!");
println!("{hello}");
}
Resumen: cuando hablamos de cadenas de texto en Rust debemos diferenciar entre dos tipos. Tenemos el tipo &str
que se llama Slice String, que es un tipo de dato inmutable y que no podemos modificar, similar al String de JavaScript. Por otro lado, tenemos el tipo String
en Rust, que es un tipo de dato mutable y que podemos modificar para cambiar su contenido y su longitud.
No confundas que la variable sea mutable con que el tipo de dato es mutable. Si usas
let mut hello = "Hello"
estás creando una variable mutable y, por lo tanto, podrás reasignar la variable. Pero el tipo de dato es&str
y no podrás modificar el valor. Reasignar la variable no es lo mismo que modificar el valor.
Structs
En JavaScript cuando queremos agrupar un conjunto de datos que definen un objeto podemos usar class
:
// JavaScript
class Animal {
constructor(name, specie, age) {
this.name = name;
this.specie = specie;
this.age = age;
}
}
Ahora podemos crear un objeto de tipo Animal
pasando los parámetros necesarios:
// JavaScript
const dog = new Animal("Zeus", "Perro", 3);
En Rust no existen las clases pero sí los Structs. Los Structs son una forma de agrupar datos complejos que definen algo. Por ejemplo, podemos crear un Struct para definir un Animal
, tal y como hemos hecho antes:
// Rust
struct Animal {
name: String,
specie: String,
age: u8,
}
Al crear un struct
es necesario que definamos los tipos de datos de cada uno de los campos. En este caso, hemos definido que el campo name
es de tipo String
, el campo specie
es de tipo String
y el campo age
es de tipo u8
.
Ahora podemos crear un Animal
:
// Rust
let dog = Animal {
name: String::from("Zeus"),
specie: String::from("Perro"),
age: 3,
};
También puedes usar la notación corta para crear un struct
, similar a la que usamos en JavaScript, si ya tienes una variable con el mismo nombre que el campo:
// Rust
let name = String::from("Zeus");
let specie = String::from("Perro");
let age = 3;
let dog = Animal { name, specie, age };
Para acceder a los campos de un struct
podemos usar la notación de punto:
// Rust
println!("El nombre del perro es {}", dog.name);
Mostrar Structs en consola
Ahora, ¿Cómo podemos mostrar el valor de un struct
en la consola? En JavaScript podemos usar console.log
:
// JavaScript
console.log(dog);
En Rust ya hemos visto que hay que usar println!
para mostrar el valor de una variable en la consola. Si intentamos usar println!
con un struct
nos dará un error:
println!("El perro es {dog}");
// ❌: `Animal` cannot be formatted with the default formatter
Si no queremos ir propiedad por propiedad y queremos mostrar todo el objeto, vamos a necesitar hacer dos cosas.
- Indicarle al compilador que queremos usar la macro
println!
con el formatoDebug
. - Usar un placeholder para indicarle al compilador que queremos mostrar el valor de un
struct
.
Para lograr la primera, hay que añadir la anotación #[derive(Debug)]
antes de la definición del struct
:
// Rust
#[derive(Debug)]
struct Animal {
name: String,
specie: String,
age: u8,
}
Para lograr la segunda, tenemos que usar el placeholder :?
:
// Rust
println!("El perro es {:?}", dog);
Si ejecutamos el programa, veremos el siguiente resultado:
El perro es Animal { name: "Zeus", specie: "Perro", age: 3 }
Crea un nuevo Struct a partir de otro
En JavaScript podemos usar el operador spread ...
para crear un nuevo objeto a partir de otro objeto:
// JavaScript
const dog = { name: "Zeus", specie: "Perro", age: 3 };
const dog2 = { ...dog, age: 4 };
En Rust el operador ..
es parecido aunque tiene alguna diferencia que es importante apuntar. Lo usaremos así:
// Rust
let dog = Animal {
name: String::from("Zeus"),
specie: String::from("Perro"),
age: 3,
};
let dog2 = Animal { age: 4, ..dog };
También fíjate que hemos usado el ..dog
al final, después de los campos que queremos sobreescribir.
El operador ..
se usa para indicar el struct base que queremos usar y siempre se tiene que usar al final. Si intentamos usar el ..
en otra posición nos dará un error de sintaxis. Esto es muy diferente a JavaScript, ya que la posición del operador spread puede ser cualquiera y su posición cambiará el resultado. En Rust, siempre va al final.
Tuple Structs
Los tuple structs son una variante de los structs
que no tienen nombres de campo. En lugar de tener nombres de campo, los structs
tienen tipos de datos. Los tuple structs son útiles cuando queremos dar un nombre a una tupla.
struct ColorRGB(i8, i8, i8);
struct LatLng(f64, f64);
Para crear una instancia de un tuple struct tenemos que usar la misma sintaxis que para crear una tupla:
let red = ColorRGB(255, 0, 0);
let barcelona_marker = LatLng(41.3851, 2.1734);
Lo bueno de los tuple structs es aunque no tienen nombres de campo, hace que nuestro código sea mucho más legible.
Para acceder a los valores de un tuple struct tenemos que usar la misma sintaxis que para acceder a los valores de una tupla:
let red = ColorRGB(255, 0, 0);
let red_r = red.0;
let red_g = red.1;
let red_b = red.2;
Pero también podemos usar destructuring para acceder a los valores de un tuple struct, tal y como haríamos en JavaScript pero usando la sintaxis de Rust con paréntesis:
let red = ColorRGB(255, 0, 0);
let (red_r, red_g, red_b) = red;
Métodos en Structs
Una vez que tenemos definido un Struct en Rust, podemos añadir métodos a ese Struct. Los métodos son funciones que están definidas dentro de un Struct y que tienen acceso a los datos del Struct.
Ahora, la sintaxis para definir métodos en Rust es un poco diferente a la que estamos acostumbrados en JavaScript. En Rust los métodos se definen dentro de un bloque impl
.
Vamos a crear un método al struct Animal
que nos permita crear una nueva instancia de Animal
. Para ello, vamos a crear un método llamado new
que reciba los datos necesarios para crear una nueva instancia de Animal
y que devuelva una nueva instancia de Animal
.
impl Animal {
fn new(name: String, specie: String, age: u8) -> Animal {
Animal {
name,
specie,
age,
}
}
}
No es el método más útil del mundo porque ya hemos visto lo fácil que es crear una nueva instancia de Animal
usando el struct literal pero es un ejemplo para que veáis cómo se definen los métodos en Rust en los structs (además que podrías hacer aquí cosas más interesantes, como comprobar que el nombre no está vacío, por ejemplo o transformar los datos).
Ahora, para usar este método, tenemos que llamarlo usando la sintaxis de Rust para llamar a métodos:
let animal = Animal::new("Paco".to_string(), "Perro".to_string(), 5);
Fíjate que para llamar al método new
tenemos que usar la sintaxis ::
para indicar que el método new
está definido en el struct Animal
.
Además, como los campos name
y specie
son String, usamos la cadena de texto con comillas dobles (que es un &str como hemos visto antes) y usamos el método to_string()
para convertirlo a String.
Vamos a ver otro ejemplo de método en un struct, esta vez para poder recuperar el nombre del animal:
impl Animal {
fn get_name(&self) -> &String {
&self.name
}
}
¡Ah! Esto tiene una pinta muy rara, ¿verdad? ¿Qué es ese &
que hemos puesto delante de self
? ¿Y ese &
que hemos puesto delante de String
? ¿Y ese &
que hemos puesto delante de self.name
? ¡Muchos &
que seguro no esperabas!
Por ahora, no te preocupes por los &
y simplemente piensa que es la forma de indicar una referencia. Así que no devolvemos un String, si no una referencia a uno.
Lo más interesante es el self, que se refiere al propio struct
de Animal
y que nos permite acceder a sus datos. Es un parámetro que se pasa implícitamente a todos los métodos de un struct, no tienes que pasárselo explícitamente.
Ahora vamos a ver cómo usar este método:
let animal = Animal::new("Paco".to_string(), "Perro".to_string(), 5);
let name = animal.get_name();
println!("{name}") // -> Paco
También podemos tener un método para modificar los datos de un struct. Por ejemplo, vamos a crear un método para modificar la edad del animal:
impl Animal {
fn set_age(&mut self, age: u8) {
self.age = age;
}
}
Te dejo aquí todo el código del ejemplo para que puedas entenderlo con todo el contexto y puedas ejecutarlo y jugar con él:
#[derive(Debug)]
struct Animal {
name: String,
specie: String,
age: u8,
}
impl Animal {
fn new(name: String, specie: String, age: u8) -> Animal {
Animal {
name,
specie,
age,
}
}
fn get_name(&self) -> &String {
&self.name
}
}
fn main() {
let animal = Animal::new("Paco".to_string(), "Perro".to_string(), 5);
let name = animal.get_name();
println!("{name} tiene {} años", animal.age);
}
Ownership
Una de las características más especiales de Rust es el concepto de Ownership. Este concepto es muy importante… y no existe en JavaScript por lo que no se puede crear una analogía directa.
¿Qué es el Ownership?
El Ownership son una serie de reglas que nos permite controlar el acceso a los datos de una forma muy estricta y, además, liberar la memoria de forma automática cuando ya no la necesitamos. Las 3 reglas del Ownership son:
- Cada valor en Rust tiene un dueño (owner).
- Sólo puede haber un dueño a la vez.
- Cuando el dueño sale del ámbito, el valor se descarta.
Inicio de Ownership y move
En Rust, como en muchos otros lenguajes como JavaScript, usamos el símbolo de igual =
para asignar un valor a una variable. Sin embargo, en Rust están pasando más cosas de las que parece.
let num = 5;
Al hacer esto, estamos iniciando el Ownership de la variable num
. Esto significa que la variable num
es el dueño del valor 5
.
¿Esto significa que no se puede usar más el valor 5
? No. Los tipos primitivos como i32
o bool
se copian cuando se asignan a una variable. Esto significa que podemos seguir usando el valor 5
sin problemas:
let num = 5;
let num2 = num; // Se copia el valor de num a num2
println!("{num} {num2}"); // -> 5 5
Pero esto no es así con otros tipos compuestos como String o Vec. En estos casos, cuando reasignamos un valor a una variable, lo que realmente estamos haciendo es cambiar el ownership sobre ese valor.
// Creamos un String a partir del String Slice
let name = String::from("Miguel");
// Reasignamos el valor de name a name2
let name2 = name;
// Intentamos mostrar el valor de name
println!("{name}"); // -> Error: value borrowed here after move
¿Qué ha pasado aquí? Pues que cuando hacemos una reasignación no estamos haciendo una copia del valor, si no que estamos cambiando el dueño del valor. Esto significa que el valor ya no pertenece a la variable name
y, por lo tanto, no podemos usarla desde ahí. Ahora el dueño es la variable name2
.
A este proceso se le llama move y es una de las características más importantes de Rust.
Como has visto, Rust es muy agresivo a la hora de liberar los recursos. En JavaScript contamos con un Garbage Collector pero Rust incorpora este sistema de Ownership que libera el recurso automáticamente cuando el dueño sale del ámbito. Esto no evita que puedan existir memory leaks pero sí que se minimizan.
Ownership y funciones
Hay veces que puede ser un reto detectar cuando se produce un move en Rust ya que también ocurre cuando usamos la variable como argumento de una función. Por ejemplo, vamos a crear una función que recibe un String y lo pasa a todo minúsculas:
fn to_lowercase(name: String) -> String {
name.to_lowercase()
}
fn main() {
let name = String::from("Miguel");
let lower_case_name = to_lowercase(name);
// ¡name ya no existe!
println!("{name}"); // -> Error: value borrowed here after move
}
Vamos a ver qué ha pasado aquí paso a paso.
- Creamos una variable
name
y le asignamos un valor de tipo String. - Llamamos a la función
to_lowercase
pasándole la variablename
como argumento. - En este punto, estamos haciendo un move de la variable
name
a la funciónto_lowercase
. Esto significa que la variablename
ya no existe en el ámbito de la funciónmain
y que el dueño del valor es la funciónto_lowercase
. - La función
to_lowercase
devuelve un valor de tipo String que se asigna a la variablelower_case_name
. - Intentamos mostrar el valor de la variable
name
pero no podemos porque ya no existe. El dueño del valor era la funciónto_lowercase
.
Este suele ser un problema muy común en Rust ya que, por defecto, las funciones toman la propiedad de los valores que recibe como argumentos. Esto es muy útil para evitar que se modifiquen los valores que se pasan a la función. Sin embargo, hay veces que no queremos que esto ocurra y, en esos casos, tenemos que indicar explícitamente que no queremos que se produzca el move.
Ten en cuenta que este problema no existe con los tipos primitivos ya que estos se copian cuando se pasan como argumento a una función. Por ejemplo, vamos a crear una función que recibe un i32
y lo duplica:
fn double(num: i32) -> i32 {
num * 2
}
fn main() {
let num = 5;
let double_num = double(num);
// num sigue existiendo
println!("{num}"); // -> 5
}
¿Por qué los tipos primitivos sí los copia? De los tipos como
i32
, Rust conoce su tamaño en tiempo de compilación y, por lo tanto, puede copiarlos sin problemas. Sin embargo, los tipos compuestos como String o Vec no pueden ser copiados de esta forma ya que no sabemos su tamaño en tiempo de compilación.
Copia de valores
Así que ya sabemos que tipos como i32
o bool
se copian automáticamente con reasignaciones o al pasarlos como argumento a una función. Pero, ¿qué pasa con los tipos compuestos? ¿Cómo podemos copiarlos? Existen diferentes estrategias para poder lograrlo. A la hora de trabajar con String, por ejemplo, podemos usar el método clone
:
let name = String::from("Miguel");
let name2 = name.clone();
// name sigue existiendo
println!("{name} {name2}"); // -> Miguel Miguel
…