Funciones
Las Funciones son muy comunes en el código 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. También has visto la palabra clave fn
, que te
permite declarar nuevas funciones.
El código en Rust usa snake case como 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 un ejemplo de definición de una función:
Nombre de 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 conjunto de paréntesis. Las llaves indican al compilador donde comienza y
termina el cuerpo de la función.
Podemos llamar a cualquier función que hayamos definido escribiendo su nombre
seguido de un conjunto de paréntesis. Como another_function
está definida en
el programa, se puede llamar desde dentro de la función main
. Ten en cuenta
que definimos another_function
después de la función main
en el código
fuente; también podríamos haberla definido antes. A Rust no le importa dónde
definas tus funciones, sólo que estén definidas en algún lugar en un ámbito que
pueda ser visto por el invocador.
Empecemos un nuevo proyecto binario llamado functions para explorar las
funciones más a fondo. 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` profile [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 para que tengan 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 coloquialmente, la gente tiende a usar las palabras parámetro y argumento indistintamente para las variables en la definición de una función o los valores concretos que se pasan cuando llamas a una función.
En esta versión de another_function
agregamos un parámetro:
Nombre de 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` profile [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!
pone 5
donde estaba el par de llaves que contenía x
en la
cadena de formato.
En las firmas de las funciones, debes declarar el tipo de cada parámetro. Esta es una decisión deliberada en el diseño de Rust: requerir anotaciones de tipo en las definiciones de las funciones significa que el compilador casi nunca necesita que las uses en otro lugar del código para averiguar a qué tipo te refieres. El compilador también puede dar mensajes de error más útiles si sabe qué tipos espera la función.
Al definir múltiples parámetros, separa las declaraciones de parámetros con comas, como esto:
Nombre de 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 parámetro se
llama unit_label
y es de tipo char
. Luego, la función imprime texto que
contiene tanto el value
como el unit_label
.
Intentemos ejecutar este código. Reemplaza el programa actual en tu
proyecto functions en el archivo src/main.rs con el ejemplo anterior y
ejecútalo usando cargo run
:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/functions`
The measurement is: 5h
Como hemos 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.
Sentencias y Expresiones
Los cuerpos de las funciones están compuestos por una serie de sentencias opcionalmente terminadas en una expresión. Hasta ahora, las funciones que hemos visto no incluyen una expresión final, pero has visto una expresión como parte de una sentencia. 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 sentencias y las expresiones y cómo sus diferencias afectan a los cuerpos de las funciones.
- Sentencias son instrucciones que realizan alguna acción y no devuelven un valor.
- Expresiones evalúan a un valor resultante. Veamos algunos ejemplos.
Hemos usado realmente sentencias y expresiones. Crear una variable y asignarle
un valor con la palabra clave let
es una sentencia. En el Listado 3-1,
let y = 6;
es una sentencia.
Nombre de archivo: src/main.rs
fn main() { let y = 6; }
Listado 3-1: Una declaración de la función main
que contiene una sentencia
Las definiciones de las funciones también son sentencias; todo el ejemplo anterior es una sentencia en sí misma.
Las sentencias no devuelven valores. Por lo tanto, no puedes asignar una
sentencia let
a otra variable, como intenta hacer el siguiente código;
obtendrás un error:
Nombre de archivo: src/main.rs
fn main() {
let x = (let y = 6);
}
Cuando ejecutes este programa, el error que obtendrás se verá así:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
error: expected expression, found `let` statement
--> src/main.rs:2:14
|
2 | let x = (let y = 6);
| ^^^
|
= note: only supported directly in conditions of `if` and `while` expressions
warning: unnecessary parentheses around assigned value
--> src/main.rs:2:13
|
2 | let x = (let y = 6);
| ^ ^
|
= note: `#[warn(unused_parens)]` on by default
help: remove these parentheses
|
2 - let x = (let y = 6);
2 + let x = let y = 6;
|
warning: `functions` (bin "functions") generated 1 warning
error: could not compile `functions` (bin "functions") due to 1 previous error; 1 warning emitted
La sentencia let y = 6
no devuelve un valor, por lo que no hay nada a lo
que x
se pueda vincular. Esto es diferente a lo que ocurre 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 hacer que tanto x
como y
tengan el valor 6
;
eso no es el caso en Rust.
Las expresiones evalúan a un valor y componen 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 evalúa al valor 11
. Las expresiones pueden ser parte
de las sentencias: en el Listado 3-1, el 6
en la sentencia let y = 6;
es
una expresión que 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 de archivo: src/main.rs
fn main() { let y = { let x = 3; x + 1 }; println!("The value of y is: {y}"); }
Esta expresión:
{
let x = 3;
x + 1
}
es un bloque que, en este caso, evalúa a 4
. Ese valor se enlaza a y
como
parte de la sentencia let
. Ten en cuenta que la línea x + 1
no tiene un
punto y coma al final, lo que es diferente a 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
sentencia, y entonces no devolverá un valor. Ten esto en cuenta a medida que
exploras los valores de retorno de las funciones y las 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 la función es sinónimo del valor de la última
expresión en el bloque del cuerpo de una función. Puedes devolver un valor antes de que la función finalice utilizando la palabra clavereturn
y especificando un valor, pero la
mayoría de las funciones devuelven la última expresión implícitamente. Aquí
hay un ejemplo de una función que devuelve un valor:
Nombre de 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 sentencias let
en la función
five
- solo el número 5
por sí solo. Esa es una función perfectamente
válida en Rust. Ten en cuenta 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` profile [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
. Veamos esto con más detalle. Hay dos partes 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 la misma que la siguiente:
#![allow(unused)] fn main() { 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 solitario 5
sin punto y coma
porque es una expresión cuyo valor queremos devolver.
Veamos otro ejemplo:
Nombre de 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 }
La ejecución de este código imprimirá The value of x is: 6
. Pero si colocamos
un punto y coma al final de la línea que contiene x + 1
, cambiándolo de una
expresión a una sentencia, obtendremos un error:
Nombre de 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;
}
La compilación de 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 to return this value
For more information about this error, try `rustc --explain E0308`.
error: could not compile `functions` (bin "functions") due to 1 previous error
El mensaje de error principal, mismatched types
, revela el problema principal
con este código. La definición de la función plus_one
dice que devolverá un
i32
, pero las sentencias no evalúan 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 como resultado un error. En esta salida, Rust
proporciona un mensaje para posiblemente ayudar a corregir este problema:
sugiere eliminar el punto y coma, lo que arreglaría el error.