Rust: tutorial del popular lenguaje de programación

Rust es un lenguaje de programación de Mozilla con el que se pueden escribir herramientas de línea de comandos, aplicaciones web y programas de red. Además, este lenguaje también se puede usar para la programación de bajo nivel. Entre los programadores de Rust, el lenguaje goza de gran popularidad.

En este tutorial de Rust, te mostramos las principales características del lenguaje. Al hacerlo, podrás observar las similitudes y diferencias con otros lenguajes. Te guiaremos asimismo en la instalación de Rust y aprenderás a escribir y compilar el código de Rust en tu propio sistema.

Breve introducción a Rust: aspectos clave

Rust es un lenguaje compilado, lo que garantiza un alto rendimiento. Al mismo tiempo, el lenguaje proporciona sofisticadas abstracciones que facilitan en gran medida el trabajo del programador. La seguridad también se presenta como una de las principales ventajas de Rust frente a lenguajes anteriores como C y C++.

Usar Rust en un sistema propio

Dado que Rust es un software libre y de código abierto (FOSS), cualquiera puede descargarse la cadena de herramientas del lenguaje de programación y utilizarla en su propio sistema. A diferencia de Python o JavaScript, Rust no es un lenguaje interpretado. En lugar de un intérprete, utiliza un compilador, como en C, C++ y Java. En la práctica, esto se traduce en una ejecución del código en dos pasos:

  1. Compilar el código fuente. Esto genera un archivo ejecutable binario.
  2. Ejecutar el archivo binario resultante.

En el caso más simple, ambos pasos se controlan desde la interfaz de línea de comandos.

Consejo

En este otro artículo de la Digital Guide, se profundiza en la diferencia entre compilador e intérprete.

Además de los archivos binarios ejecutables, Rust permite también crear bibliotecas. Si el código compilado es un programa que se puede ejecutar directamente, se debe definir una función main() en el código fuente. Al igual que en C/C++, esto sirve como punto de partida para ejecutar el código.

Instalar Rust en el sistema local

Para usar Rust, es necesario que primero lo instales de forma local. En macOS se puede usar el sistema de gestión de paquetes Homebrew, también compatible con Linux. Abre la interfaz de línea de comandos (Terminal.App en Mac), copia la siguiente línea de código en la terminal y ejecútala.

brew install rust
Nota

Para instalar Rust en Windows o en otro sistema operativo donde no se pueda usar Homebrew, utiliza la herramienta oficial Rustup.

Para comprobar que Rust se ha instalado con éxito, abre una nueva ventana en la consola y ejecuta el código que aparece a continuación:

rustc --version

Si Rust se ha instalado correctamente, aparecerá la versión del compilador de Rust. En cambio, si se muestra un mensaje de error, vuelve a iniciar la instalación.

Compilación de código Rust

Para compilar el código Rust, necesitas un archivo de código fuente Rust. Abre la terminal de comandos y ejecuta los siguientes fragmentos de código. Con ellos, crearemos una carpeta en el escritorio para el tutorial de Rust y la abriremos:

cd "$HOME/Escritorio/"
mkdir rust-tutorial && cd rust-tutorial

A continuación, se creará el archivo de código fuente de Rust con el sencillo ejemplo “Hello, World”:

cat << EOF > ./rust-tutorial.rs
fn main() {
    println!("Hello, World!");
}
EOF
Nota

Los archivos de código fuente Rust terminan en .rs.

Finalmente, se compilará el código fuente Rust y se ejecutará el archivo binario resultante:

# Rust-compilar código fuente
rustc rust-tutorial.rs
# Ejecutar el archivo binario resultante
./rust-tutorial
Consejo

Utiliza el comando rustc rust-tutorial.rs && ./rust-tutorial para combinar los dos pasos. De esta manera podrás volver a compilar y ejecutar tu programa en la interfaz de línea de comandos pulsando la tecla de la flecha hacia arriba seguida de “Intro”.

Gestionar los paquetes de Rust con Cargo

Además del lenguaje Rust, hay una serie de paquetes externos. Los llamados Crates se pueden obtener en el Rust Package Registry. Para ello, se usa la herramienta Cargo, instalada junto con Rust. El comando cargo permite tanto la instalación de paquetes como la creación de otros nuevos. Comprueba que Cargo se ha instalado correctamente:

cargo --version

Conoce los fundamentos de Rust

Para aprender a usar Rust, te recomendamos que vayas probando los ejemplos de código. Para ello, puedes usar el archivo rust-tutorial.rs que hemos creado. Copia un ejemplo de código en el archivo, compílalo y ejecuta el archivo binario resultante. Para que funcione, debes insertar el código de ejemplo dentro de la función main().

Aunque, para probar el código Rust, también puedes usar Rust Playground directamente en tu navegador.

Declaraciones y bloques

En Rust, las declaraciones son componentes de código básico. Una declaración termina con un punto y coma (;) y, a diferencia de una expresión, no devuelve un valor. En un mismo bloque, se pueden agrupar varias declaraciones. Como en C/C++ y Java, los bloques están delimitados por llaves ({}).

Comentarios

Los comentarios son una función importante en cualquier lenguaje de programación. Se utilizan tanto para la documentación del código, como en la planificación, antes de que se escriba el código real. Para los comentarios, Rust utiliza la misma sintaxis que C, C++, Java y JavaScript: todo texto incluido después de la doble barra se interpreta como un comentario y el compilador lo ignora.

// Esto es un comentario
// Esto es 
// un comentario
// formado
// por varias líneas.

Variables y constantes

En Rust se usa la palabra clave let para declarar una variable y puede declararse de nuevo una variable que ya existe, sobrescribiéndose. A diferencia de muchos otros lenguajes, no es fácil cambiar el valor de una variable:

// Declarar la variable ‘edad’ y fijar el valor en ‘42’
let edad = 42;
// El valor de la variable ‘edad’ no se puede cambiar
edad = 49; // Error de compilación
// con una nueva declaración ‘let’, la variable ‘edad’ puede sobrescribirse
let edad = 49;

Para indicar que el valor de una variable se pueda cambiar con posterioridad, Rust propone la palabra clave mut. El valor de una variable declarada con mut sí puede cambiarse:

let mut peso = 78;
peso = 75;

Con la palabra clave const se define una constante. El valor de una constante en Rust debe ser conocido en el momento de la compilación. Asimismo, el tipo debe especificarse de forma explícita:

const VERSION: &str = "1.46.0";

El valor de una constante no se puede cambiar. Una constante tampoco puede declararse como mut, ni puede sobrescribirse:

// Definir constantes
const MAX_NUM: u8 = 255;
MAX_NUM = 0; // Error al compilar, pues el valor de la constante no se puede cambiar 
const MAX_NUM = 0; // Error al compilar, pues el valor de la constante no se puede volver a declarar

Modelo de propiedad

Una de las características más importantes de Rust es el modelo de propiedad (en inglés, ownership). La propiedad está estrechamente relacionada con el valor de las variables, su vida útil y la gestión del almacenamiento de los objetos en la memoria heap. Cuando una variable sale del rango válido (Scope), su valor se destruye y la memoria se libera. Por lo tanto, Rust puede prescindir del recolector de basura, lo que favorece un alto rendimiento.

Cada valor de Rust pertenece a una variable, el propietario. Solo puede haber un propietario para cada valor. Si el propietario cede el valor a otra variable, entonces deja de ser propietario:

let name = String::from("Ana Ejemplo");
let _name = name;
println!("{}, world!", name); // Error de compilación, dado que el valor ‘name’ dirige a ‘name’

Hay que tener especial cuidado con la definición de las funciones. Si se pasa una variable a una función, el propietario del valor cambia. La variable no puede volver a utilizarse después de la llamada a la función. Aquí, Rust recurre a un truco. En lugar de trasladar el valor a la función, se declara una referencia con el signo et (&). Esto permite “prestar” el valor de una variable, como se aprecia en el ejemplo:

let name = String::from("Ana Ejemplo");
// si el tipo del parámetro ‘name’ se define como ‘String’ en lugar de ‘&String’
// la variable ‘name’ ya no puede utilizarse después de la llamada de la función
fn hello(name: &String) {
    println!("Hello, {}", name);
}
// el argumento de la función también debe contener ‘&’
// como referencia
hello(&name);
// esta línea, al no usar referencia, conduce a un error de compilación
println!("hello, {}", name);

Estructuras de control

Una propiedad básica de la programación es hacer que la ejecución del programa no sea lineal. Un programa puede ramificarse y sus componentes pueden ejecutarse más de una vez. Es esta versatilidad lo que hace que un programa sea realmente útil.
Rust cuenta con las estructuras de control disponibles en la mayoría de los lenguajes de programación. Estos incluyen las construcciones en bucle for y while, así como la ramificación a través de if y else. Sin embargo, Rust cuenta también con algunas características especiales. La palabra clave match permite la asignación de patrones, mientras que la declaración loop crea un bucle infinito. Para que esto último sea práctico, se recurre a la declaración break.

Bucles

La ejecución repetida de un bloque de código mediante bucles se conoce también como iteración. A menudo, la iteración se produce en los elementos de un contenedor. Al igual que Python, Rust utiliza también el concepto de iterador. Un iterador abstrae el acceso sucesivo a los elementos de un contenedor. Por ejemplo:

// Lista con nombres
let nombres = ["Jim", "Jack", "John"];
// bucle ‘for’ con iterador en la lista
for name in namen.iter() {
    println!("Hello, {}", name);
}

¿Y si quieres escribir un bucle for como en C/C++ o Java? Si quieres especificar un número de inicio y un número final y recorrer todos los valores intermedios, Rust cuenta, igual que Python, con el objeto rango. Esto a su vez crea un iterador en el que opera la palabra clave for:

// Introducir las cifras del ‘1’ al ‘10’
// bucle ‘for’ con iterador ‘range’
// Importante: el rango no contiene la cifra final
for cifra in 1..11 {
    println!("Cifra: {}", cifra);
}
// alternativa que incluye la notación del rango
for cifra in 1..=10 {
    println!("Cifra: {}", cifra);
}

El bucle while funciona en Rust como en la mayoría de los lenguajes de programación. Se establece una condición y se ejecuta el cuerpo del bucle siempre y cuando la condición sea cierta:

// emitir las cifras del ‘1’ al ‘10’ a través del bucle ‘while’
let mut cifra = 1;
while (cifra <= 10) {
    println!(Cifra: {}, cifra);
    cifra += 1;
}

En todos los lenguajes de programación, es posible establecer un bucle infinito con while. Aunque normalmente esto se considera un error, existen casos en los que puede ser necesario. En esos casos, Rust aconseja usar loop:

// Bucle infinito con ‘while’
while true {
    // …
}
// Bucle infinito con ‘loop’
loop {
    // …
}

En ambos casos, se puede recurrir a la palabra clave break para romper el bucle.

En ambos casos, se puede recurrir a la palabra clave break para romper el bucle.

Ramificaciones

Las ramificaciones con if y else funcionan en Rust igual que en otros lenguajes de programación similares:

const limit: u8 = 42;
let cifra = 43;
if cifra < límite {
    println!("Por debajo del límite.");
}
else if cifra == limit {
    println!("En el límite…");
}
else {
    println!("Sobre el límite");
}

Más interesante es la palabra clave match, con una función parecida a la declaración switch en otros lenguajes de programación. Como ejemplo, echa un vistazo a la función símbolode_carta() en la sección “Tipos de datos compuestos”.

Funciones, procedimientos y métodos

En la mayoría de los lenguajes de programación, las funciones son el elemento básico de la programación modular. En Rust, las funciones se definen con la palabra clave fn. No se hace una distinción estricta entre los conceptos función y procedimiento, que se definen de forma casi idéntica.

En sentido estricto, una función devuelve un valor. Aunque, como muchos otros lenguajes de programación, Rust también cuenta con procedimientos, esto es, funciones que no devuelven ningún valor. La única restricción fija es que el tipo de retorno de una función debe especificarse de forma explícita. Si no se especifica ningún tipo de retorno, la función no puede devolver un valor. En este caso, se define como procedimiento.

fn procedimiento() {
    println!("Este procedimiento no devuelve ningún valor.");
}
// negar una cifra
// Tipo de retorno tras el operador ‘->’
fn negado(cifra completa: i8) -> i8 {
    return cifra completa * -1;
}

Además de funciones y procedimientos, Rust también trabaja con métodos conocidos de la programación orientada a objetos. Un método es una función que está ligada a una estructura de datos. Como en Python, los métodos en Rust se definen con el primer parámetro self. La llamada a un método se realiza según el esquema habitual object.method(). A continuación, se muestra un ejemplo del method surface(), ligado a una estructura de datos struct:

// definición ‘struct’ 
struct rectángulo {
    ancho: u32,
    alto: u32,
}
// Implementación ‘struct’
impl rectángulo {
    fn superficie(&self) -> u32 {
        return self.ancho * self.alto;
    }
}
let rectángulo = Rectángulo {
    ancho: 30,
    alto: 50,
};
println!("El área del rectángulo es {}.", superficie.rectángulo());

Tipos de datos y estructuras de datos

Rust es un lenguaje que usa un tipado estático. A diferencia de los lenguajes dinámicos como Python, Ruby, PHP o JavaScript, en Rust tiene conocerse el tipo de cada variable cuando tiene lugar la compilación.

Tipos de datos primitivos

Como la mayoría de los lenguajes de programación superiores, Rust posee algunos tipos de datos elementales (o “primitivos”). Las instancias de tipos de datos elementales se asignan a la pila, que cuenta con un alto rendimiento. Además, los valores de los tipos de datos elementales pueden definirse utilizando una sintaxis “literal”, es decir, basta con escribir los valores.

Tipo de dato Explicación Anotación de tipo
Integer Entero i8, u8, etc.
Floating point Valores de puntos flotantes f64, f32
Boolean Valor verdadero bool
Character Carácter char
String Cadena de caracteres unicode str

Aunque Rust es un lenguaje estáticamente tipado, el tipo de un valor no siempre tiene que declararse de forma explícita. En muchos casos, el compilador puede deducir el tipo a partir del contexto (inferencia de tipos). Si no es el caso, el tipo se especifica explícitamente mediante una anotación de tipo, que en algunos casos es incluso obligatoria:

  • El tipo de retorno de una función siempre debe especificarse explícitamente.
  • El tipo de una constante también debe especificarse siempre explícitamente.
  • Los strings literales deben tratarse de forma especial, para que se conozca su tamaño en la compilación.

Aquí se incluyen algunos ejemplos en los que se crean instancias de tipos de datos elementales con sintaxis literal.

// el compilador reconoce aquí de forma automática el tipo de variable
let cents = 42;
// anotación de tipo: cifra positiva (‚u8‘ = "unsigned, 8 bits")
let edad: u8 = -15; // error de compilación, ya que se ha proporcionado una cifra negativa
// Valores de puntos flotantes
let ángulo = 38,5;
// equivalente a
let ángulo: f64 = 38,5;
// Valor verdadero
let usuario_registrado = true;
// equivalente a
let usuario_registrado: bool = true;
// el carácter requiere comillas simples
let carácter = 'a';
// cadena de caracteres estática requiere comillas 
let nombre = "Ana";
// con tipo explícito
let nombre: &'static str = "Ana";
// alternativa como string dinámico con string::from()
let nombre: string = string::from("Ana");

Tipos de datos compuestos

Los tipos de datos elementales reproducen valores individuales, mientras que los tipos de datos compuestos agrupan varios valores. Rust proporciona a los programadores una gran variedad de tipos de datos compuestos.

Tanto las instancias de los tipos de datos compuestos, como las instancias de los tipos de datos elementales, se asignan a la pila. Para ello, las instancias deben tener un tamaño determinado. Esto también significa que después de la instanciación no se pueden cambiar de forma arbitraria. Entre los principales datos compuestos de Rust se encuentran:

Tipo de datos Explicación Tipo del elemento Sintaxis literal
Array Lista de varios valores El mismo tipo [a1, a2, a3]
Tuple Disposición de varios valores Cualquier tipo (t1, t2)
Struct Agrupación de varios valores nombrados Cualquier tipo
Enum Enumeración Cualquier tipo

Veamos primero una estructura de datos struct. Definimos una persona con tres campos de nombre:

struct Persona = {
    nombre: String,
    apellido: String,
    edad: u8,
}

Para representar a una persona concreta, se instancia struct:

let jugador = Persona {
    nombre: String::from("Ana"),
    apellido: String::from("Ejemplo"),
    edad: 42,
};
// acceder al campo de una instancia struct
println!("Edad del jugador es: {}", jugador.edad);

enum (enumeración) representa las posibles variantes de una propiedad. Aquí se muestra un ejemplo con los cuatro palos presentes en una baraja de cartas:

enum palosdelabaraja {
    Trébol,
    Pica,
    Corazón,
    Diamante,
}
// los palos de una baraja de cartas
let palo = palodebaraja::trébol;

Rust utiliza la palabra clave match para buscar patrones, lo que se conoce como pattern matching. Su funcionalidad puede compararse con la declaración switch de otros lenguajes. Aquí hay un ejemplo:

// determinar el símbolo que pertenece a cada palo
fn símbolode_carta(palo: PaloDeLaBaraja) -> &'static str {
    match palo {
        Palo::Trébol => "♣︎",
        Palo::Pica => "♠︎",
        Palo::Corazón => "♥︎",
        Palo::Diamante => "♦︎",
    }
}
println!("Symbol: {}", símbolo_carta(Palo::Trébol)); // mostrar el símbolo ♣︎

Una tupla es la disposición de varios valores, que pueden ser de diferentes tipos. Los valores individuales de la tupla pueden asignarse a varias variables mediante desestructuración. Si uno de los valores no es necesario, se utiliza el guion bajo (_) como marcador de posición, como es habitual en Haskell ,Python y JavaScript. He aquí un ejemplo

// Definir la carta como tupla
let carta: (PaloDeLaCarta, u8) = (PaloDeLaCarta::Corazón, 7);
// Asignar los valores de una tupla a varias variables
let (palo, valor) = carta;
// en caso de necesitar el valor
let (_, valor) = carta;

Dado que los valores de la tupla están ordenados, también es posible acceder a ellos mediante un índice numérico. Para la indexación no se utilizan corchetes, sino que se recurre a la sintaxis de puntos. En la mayoría de los casos, la desestructuración debería resultar en un código más legible:

let Nombre completo = ("Ana", "Ejemplo");
let nombre = name.0;
let apellido = name.1;

Aprender construcciones de programación superiores en Rust

Estructuras de datos dinámicos

Las instancias de los tipos de datos compuestos presentados se asignan a la pila. Sin embargo, la biblioteca estándar de Rust también contiene una serie de estructuras de datos dinámicos de uso común. Las instancias de estas estructuras de datos se asignan a la memoria heap, lo que implica que el tamaño de las instancias puede cambiarse a posteriori. A continuación, presentamos brevemente las estructuras de datos dinámicos que se usan con mayor frecuencia:

Tipo de datos Explicación
Vector Lista dinámica de varios valores del mismo tipo
String Secuencia dinámica de caracteres Unicode
HashMap Asignación dinámica de los pares clave-valor

A continuación, te mostramos un ejemplo de vector de crecimiento dinámico:

// Declarar el vector con mut como modificable 
let mut nombre = Vec::new();
// Añadir valores al vector
nombres.push("Jim");
nombres.push("Jack");
nombres.push("John");

Programación orientada a objetos (OOP) en Rust

A diferencia de lenguajes como C++ y Java, Rust no conoce el concepto de clases. Sin embargo, es posible programar según la metodología OOP, utilizando como base los tipos de datos ya presentados. El tipo struct puede usarse para definir la estructura de los objetos.

Además, Rust también cuenta con los traits. Un trait engloba un conjunto de métodos que cualquier tipo puede implementar. Comprende, además, declaraciones de método, aunque también puede contener implementaciones. En lo que al concepto se refiere, un trait se encuentra entre una interfaz de Java y una clase base abstracta.

Un trait que ya existe puede ser implementado por diferentes tipos. A su vez, un tipo puede implementar varios traits. Rust permite así la composición de la funcionalidad para diferentes tipos sin necesidad de un antecedente común.

Programación meta

Como muchos otros lenguajes de programación, Rust permite escribir código para la metaprogramación. Se trata de un código que genera más código. En Rust se encuentran, por un lado, las macros, que terminan con un signo de exclamación (!) y que puedes conocer de C/C++. La macro println!, para mostrar texto en la línea de comandos, ya se ha mencionado varias veces en este artículo.

Por otro lado, Rust también recurre a los llamados genéricos, que permiten escribir código que se puede abstraer en varios tipos. Los genéricos son comparables con las plantillas en C++ o a los también llamados genéricos en Java. Un genérico que se usa a menudo en Rust es Option<T>, que abstrae la dualidad Ninguno/Algunos(T) para cualquier tipo de T.

En resumen

Rust tiene el potencial de reemplazar a los populares C y C++ como lenguaje de programación de sistemas.

¿Le ha resultado útil este artículo?
Page top