Definiendo funciones de Rust en LabEx

Beginner

This tutorial is from open-source community. Access the source code

Introducción

Bienvenido a Funciones. Esta práctica es parte del Rust Book. Puedes practicar tus habilidades de Rust en LabEx.

En esta práctica, aprenderás a definir y llamar funciones en Rust utilizando la palabra clave fn y la convención de nomenclatura en minúsculas con guiones bajos.

Funciones

Las funciones son muy comunes en el código de Rust. Ya has visto una de las funciones más importantes del lenguaje: la función main, que es el punto de entrada de muchos programas. Has visto también la palabra clave fn, que te permite declarar nuevas funciones.

Crea un nuevo proyecto llamado functions:

cargo new functions
cd functions

El código de Rust utiliza el snake case como el estilo convencional para los nombres de funciones y variables, en el que todas las letras son minúsculas y los guiones bajos separan las palabras. Aquí hay un programa que contiene una definición de función de ejemplo:

Nombre del archivo: src/main.rs

fn main() {
    println!("Hello, world!");

    another_function();
}

fn another_function() {
    println!("Another function.");
}

Definimos una función en Rust escribiendo fn seguido del nombre de la función y un par de paréntesis. Las llaves indican al compilador dónde comienza y termina el cuerpo de la función.

Podemos llamar a cualquier función que hayamos definido escribiendo su nombre seguido de un par de paréntesis. Dado que another_function está definida en el programa, se puede llamar desde dentro de la función main. Observe que definimos another_function después de la función main en el código fuente; también podríamos haberla definido antes. Rust no importa dónde defines tus funciones, solo que estén definidas en algún lugar de un ámbito que sea visible para el llamador.

Vamos a comenzar un nuevo proyecto binario llamado functions para explorar las funciones más detenidamente. Coloca el ejemplo de another_function en src/main.rs y ejecútalo. Deberías ver la siguiente salida:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 0.28s
     Running `target/debug/functions`
Hello, world!
Another function.

Las líneas se ejecutan en el orden en que aparecen en la función main. Primero se imprime el mensaje "Hello, world!", y luego se llama a another_function y se imprime su mensaje.

Parámetros

Podemos definir funciones con parámetros, que son variables especiales que forman parte de la firma de una función. Cuando una función tiene parámetros, puedes proporcionarle valores concretos para esos parámetros. Técnicamente, los valores concretos se llaman argumentos, pero en la conversación cotidiana, la gente tiende a usar las palabras parámetro y argumento de manera intercambiable tanto para las variables en la definición de una función como para los valores concretos que se pasan cuando se llama a una función.

En esta versión de another_function agregamos un parámetro:

Nombre del archivo: src/main.rs

fn main() {
    another_function(5);
}

fn another_function(x: i32) {
    println!("The value of x is: {x}");
}

Intenta ejecutar este programa; deberías obtener la siguiente salida:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 1.21s
     Running `target/debug/functions`
The value of x is: 5

La declaración de another_function tiene un parámetro llamado x. El tipo de x se especifica como i32. Cuando pasamos 5 a another_function, la macro println! coloca 5 donde estaba el par de llaves que contenía x en la cadena de formato.

En las firmas de funciones, debes declarar el tipo de cada parámetro. Esta es una decisión deliberada en el diseño de Rust: exigir anotaciones de tipo en las definiciones de funciones significa que el compilador casi nunca necesita que las uses en otros lugares del código para entender qué tipo quieres decir. El compilador también puede dar mensajes de error más útiles si sabe qué tipos espera la función.

Cuando se definen múltiples parámetros, separa las declaraciones de parámetros con comas, como esto:

Nombre del archivo: src/main.rs

fn main() {
    print_labeled_measurement(5, 'h');
}

fn print_labeled_measurement(value: i32, unit_label: char) {
    println!("The measurement is: {value}{unit_label}");
}

Este ejemplo crea una función llamada print_labeled_measurement con dos parámetros. El primer parámetro se llama value y es un i32. El segundo se llama unit_label y es de tipo char. Luego, la función imprime texto que contiene tanto el value como el unit_label.

Vamos a intentar ejecutar este código. Reemplaza el programa que actualmente está en el archivo src/main.rs de tu proyecto functions con el ejemplo anterior y ejecútalo usando cargo run:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/functions`
The measurement is: 5h

Debido a que llamamos a la función con 5 como valor para value y 'h' como valor para unit_label, la salida del programa contiene esos valores.

Declaraciones y expresiones

Los cuerpos de las funciones están compuestos por una serie de declaraciones que, opcionalmente, terminan en una expresión. Hasta ahora, las funciones que hemos visto no han incluido una expresión final, pero has visto una expresión como parte de una declaración. Debido a que Rust es un lenguaje basado en expresiones, esta es una distinción importante de entender. Otros lenguajes no tienen las mismas distinciones, así que veamos qué son las declaraciones y las expresiones y cómo sus diferencias afectan los cuerpos de las funciones.

  • Declaraciones: son instrucciones que realizan alguna acción y no devuelven un valor.
  • Expresiones: se evalúan a un valor resultante. Echemos un vistazo a algunos ejemplos.

En realidad, ya hemos utilizado declaraciones y expresiones. Crear una variable y asignarle un valor con la palabra clave let es una declaración. En la Lista 3-1, let y = 6; es una declaración.

Nombre del archivo: src/main.rs

fn main() {
    let y = 6;
}

Lista 3-1: Una declaración de función main que contiene una declaración

Las definiciones de funciones también son declaraciones; el ejemplo completo anterior es una declaración en sí misma.

Las declaraciones no devuelven valores. Por lo tanto, no puedes asignar una declaración let a otra variable, como intenta hacer el siguiente código; obtendrás un error:

Nombre del archivo: src/main.rs

fn main() {
    let x = (let y = 6);
}

Cuando ejecutas este programa, el error que obtendrás se parece a esto:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
error: expected expression, found statement (`let`)
 --> src/main.rs:2:14
  |
2 |     let x = (let y = 6);
  |              ^^^^^^^^^
  |
  = note: variable declaration using `let` is a statement

error[E0658]: `let` expressions in this position are unstable
 --> src/main.rs:2:14
  |
2 |     let x = (let y = 6);
  |              ^^^^^^^^^
  |
  = note: see issue #53667 <https://github.com/rust-lang/rust/issues/53667> for
more information

La declaración let y = 6 no devuelve un valor, por lo que no hay nada a lo que x pueda enlazarse. Esto es diferente de lo que sucede en otros lenguajes, como C y Ruby, donde la asignación devuelve el valor de la asignación. En esos lenguajes, puedes escribir x = y = 6 y que tanto x como y tengan el valor 6; eso no es así en Rust.

Las expresiones se evalúan a un valor y forman la mayor parte del resto del código que escribirás en Rust. Considera una operación matemática, como 5 + 6, que es una expresión que se evalúa al valor 11. Las expresiones pueden ser parte de declaraciones: en la Lista 3-1, el 6 en la declaración let y = 6; es una expresión que se evalúa al valor 6. Llamar a una función es una expresión. Llamar a una macro es una expresión. Un nuevo bloque de ámbito creado con llaves es una expresión, por ejemplo:

Nombre del archivo: src/main.rs

fn main() {
  1 let y = {2
        let x = 3;
      3 x + 1
    };

    println!("The value of y is: {y}");
}

La expresión [2] es un bloque que, en este caso, se evalúa a 4. Ese valor se enlaza a y como parte de la declaración let [1]. Observe la línea sin punto y coma al final [3], que es diferente de la mayoría de las líneas que has visto hasta ahora. Las expresiones no incluyen punto y coma al final. Si agregas un punto y coma al final de una expresión, la conviertes en una declaración y entonces no devolverá un valor. Tien esto en cuenta cuando explores los valores de retorno de funciones y expresiones a continuación.

Funciones con valores de retorno

Las funciones pueden devolver valores al código que las llama. No nombramos los valores de retorno, pero debemos declarar su tipo después de una flecha (->). En Rust, el valor de retorno de una función es sinónimo del valor de la última expresión en el bloque del cuerpo de una función. Puedes retornar tempranamente desde una función usando la palabra clave return y especificando un valor, pero la mayoría de las funciones retornan la última expresión implícitamente. Aquí hay un ejemplo de una función que devuelve un valor:

Nombre del archivo: src/main.rs

fn five() -> i32 {
    5
}

fn main() {
    let x = five();

    println!("The value of x is: {x}");
}

No hay llamadas a funciones, macros ni siquiera declaraciones let en la función five; solo el número 5 por sí mismo. Esa es una función perfectamente válida en Rust. Observe que también se especifica el tipo de retorno de la función, como -> i32. Intenta ejecutar este código; la salida debería verse así:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/functions`
The value of x is: 5

El 5 en five es el valor de retorno de la función, por eso el tipo de retorno es i32. Vamos a examinar esto con más detalle. Hay dos aspectos importantes: primero, la línea let x = five(); muestra que estamos usando el valor de retorno de una función para inicializar una variable. Debido a que la función five devuelve un 5, esa línea es equivalente a la siguiente:

let x = 5;

Segundo, la función five no tiene parámetros y define el tipo del valor de retorno, pero el cuerpo de la función es un solo 5 sin punto y coma porque es una expresión cuyo valor queremos retornar.

Veamos otro ejemplo:

Nombre del archivo: src/main.rs

fn main() {
    let x = plus_one(5);

    println!("The value of x is: {x}");
}

fn plus_one(x: i32) -> i32 {
    x + 1
}

Ejecutar este código imprimirá The value of x is: 6. Pero si ponemos un punto y coma al final de la línea que contiene x + 1, cambiándola de una expresión a una declaración, obtendremos un error:

Nombre del archivo: src/main.rs

fn main() {
    let x = plus_one(5);

    println!("The value of x is: {x}");
}

fn plus_one(x: i32) -> i32 {
    x + 1;
}

Compilar este código produce un error, como sigue:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
error[E0308]: mismatched types
 --> src/main.rs:7:24
  |
7 | fn plus_one(x: i32) -> i32 {
  |    --------            ^^^ expected `i32`, found `()`
  |    |
  |    implicitly returns `()` as its body has no tail or `return` expression
8 |     x + 1;
  |          - help: remove this semicolon

El mensaje de error principal, mismatched types, revela el problema central de este código. La definición de la función plus_one dice que devolverá un i32, pero las declaraciones no se evalúan a un valor, lo que se expresa por (), el tipo unitario. Por lo tanto, no se devuelve nada, lo que contradice la definición de la función y da lugar a un error. En esta salida, Rust proporciona un mensaje que puede ayudar a corregir este problema: sugiere quitar el punto y coma, lo que solucionaría el error.

Resumen

¡Felicitaciones! Has completado el laboratorio de Funciones. Puedes practicar más laboratorios en LabEx para mejorar tus habilidades.