Rust
Tutorial Rust: Funciones
Aprende a declarar funciones en Rust, uso de retorno implícito, ámbito de variables y shadowing para escribir código modular y eficiente.
Aprende Rust y certifícateDeclaración y parámetros
Las funciones son bloques de código reutilizables que realizan tareas específicas. En Rust, las funciones son componentes fundamentales que nos permiten organizar nuestro código de manera eficiente y modular.
Declaración básica de funciones
Para declarar una función en Rust, utilizamos la palabra clave fn
seguida del nombre de la función y un par de paréntesis. El cuerpo de la función se encierra entre llaves {}
:
fn saludar() {
println!("¡Hola, mundo!");
}
fn main() {
saludar(); // Llamada a la función
}
En este ejemplo, hemos definido una función llamada saludar
que imprime un mensaje. Luego, desde la función main
(punto de entrada de todo programa Rust), llamamos a nuestra función.
Convenciones de nomenclatura
En Rust, los nombres de las funciones siguen la convención snake_case, es decir, todas las letras en minúsculas con palabras separadas por guiones bajos:
fn calcular_promedio() {
// Código de la función
}
fn validar_usuario() {
// Código de la función
}
Parámetros de función
Las funciones pueden recibir datos de entrada a través de parámetros. En Rust, debemos especificar el tipo de cada parámetro:
fn saludar_persona(nombre: &str) {
println!("¡Hola, {}!", nombre);
}
fn main() {
saludar_persona("Ana");
saludar_persona("Carlos");
}
En este ejemplo, la función saludar_persona
recibe un parámetro nombre
de tipo &str
(una referencia a una cadena de texto).
Múltiples parámetros
Podemos definir funciones con múltiples parámetros separándolos con comas:
fn sumar(a: i32, b: i32) {
let resultado = a + b;
println!("La suma de {} y {} es: {}", a, b, resultado);
}
fn main() {
sumar(5, 3);
sumar(-2, 8);
}
Cada parámetro debe tener su propio tipo especificado. En este caso, ambos parámetros son de tipo i32
(enteros de 32 bits con signo).
Tipo de retorno
Para que una función devuelva un valor, debemos especificar su tipo de retorno después de una flecha ->
:
fn sumar(a: i32, b: i32) -> i32 {
a + b // Retorna la suma de a y b
}
fn main() {
let resultado = sumar(5, 3);
println!("El resultado es: {}", resultado);
}
En este ejemplo, la función sumar
recibe dos enteros y devuelve otro entero. Observa que no utilizamos la palabra clave return
ni un punto y coma al final de la expresión a + b
. Esto es porque en Rust, la última expresión de un bloque se retorna implícitamente (hablaremos más sobre esto en la siguiente sección).
Uso explícito de return
Aunque Rust permite el retorno implícito, también podemos usar la palabra clave return
para devolver un valor explícitamente, especialmente útil para retornos anticipados:
fn valor_absoluto(numero: i32) -> i32 {
if numero >= 0 {
return numero; // Retorno anticipado
}
-numero // Retorno implícito para números negativos
}
fn main() {
println!("Absoluto de 5: {}", valor_absoluto(5));
println!("Absoluto de -3: {}", valor_absoluto(-3));
}
Parámetros con valores por defecto
A diferencia de otros lenguajes, Rust no admite parámetros con valores por defecto directamente. Sin embargo, podemos simular este comportamiento usando patrones como:
fn saludar(nombre: &str, saludo: Option<&str>) {
let saludo_final = saludo.unwrap_or("Hola");
println!("{}, {}!", saludo_final, nombre);
}
fn main() {
saludar("Ana", Some("Bienvenida")); // Con saludo personalizado
saludar("Carlos", None); // Con saludo por defecto
}
En este ejemplo, usamos Option<&str>
para indicar que el parámetro saludo
puede estar presente (Some
) o ausente (None
).
Funciones sin valor de retorno
Si una función no devuelve ningún valor, podemos omitir la especificación del tipo de retorno o usar el tipo especial ()
(unidad):
// Estas dos declaraciones son equivalentes
fn imprimir_mensaje(mensaje: &str) {
println!("{}", mensaje);
}
fn imprimir_mensaje_explicito(mensaje: &str) -> () {
println!("{}", mensaje);
}
El tipo ()
representa la ausencia de un valor y es el tipo de retorno implícito cuando no se especifica ninguno.
Paso de parámetros y propiedad
En Rust, cuando pasamos un valor a una función, puede ocurrir una transferencia de propiedad dependiendo del tipo de dato. Por ejemplo:
fn procesar_texto(texto: String) {
println!("Procesando: {}", texto);
}
fn main() {
let mensaje = String::from("Hola mundo");
procesar_texto(mensaje);
// A partir de aquí, 'mensaje' ya no es utilizable
}
En Rust, pasar un valor a una función puede transferir su propiedad, pero esto lo veremos en módulos posteriores cuando estudiemos el sistema de ownership.
Documentación de funciones
Una buena práctica en Rust es documentar nuestras funciones usando comentarios de documentación (con tres barras ///
):
/// Calcula el área de un rectángulo.
///
/// # Argumentos
///
/// * `ancho` - El ancho del rectángulo
/// * `alto` - El alto del rectángulo
///
/// # Ejemplo
///
/// ```
/// let area = calcular_area(5.0, 3.0);
/// assert_eq!(area, 15.0);
/// ```
fn calcular_area(ancho: f64, alto: f64) -> f64 {
ancho * alto
}
Estos comentarios generan documentación automática cuando compilamos nuestro código con herramientas como cargo doc
.
Funciones anidadas
Rust permite definir funciones dentro de otras funciones, lo que resulta útil cuando una función auxiliar solo se necesita en un contexto específico:
fn operaciones_matematicas(a: i32, b: i32) {
fn sumar(x: i32, y: i32) -> i32 {
x + y
}
fn multiplicar(x: i32, y: i32) -> i32 {
x * y
}
let suma = sumar(a, b);
let producto = multiplicar(a, b);
println!("Suma: {}, Producto: {}", suma, producto);
}
fn main() {
operaciones_matematicas(5, 3);
}
Las funciones anidadas solo son visibles dentro de la función que las contiene.
Retorno implícito
En Rust, una de las características más elegantes del lenguaje es su sistema de retorno implícito, que permite escribir código más conciso y expresivo. A diferencia de otros lenguajes donde siempre necesitas una declaración return
explícita, Rust trata la última expresión de un bloque como su valor de retorno.
Expresiones vs. Sentencias
Para entender el retorno implícito, primero debemos distinguir entre expresiones y sentencias en Rust:
- Una expresión evalúa a un valor
- Una sentencia realiza una acción pero no produce un valor
Esta distinción es fundamental para comprender cómo funcionan los retornos en Rust:
fn ejemplo_sentencia() {
let x = 5; // Esto es una sentencia (no produce un valor)
}
fn ejemplo_expresion() -> i32 {
5 // Esto es una expresión (produce el valor 5)
}
Funcionamiento del retorno implícito
Cuando escribimos una función con un tipo de retorno, la última expresión del bloque (si no termina con punto y coma) se convierte automáticamente en el valor de retorno:
fn calcular_cuadrado(numero: i32) -> i32 {
numero * numero // Sin punto y coma - esta expresión es el valor de retorno
}
fn main() {
let resultado = calcular_cuadrado(4);
println!("El cuadrado es: {}", resultado); // Imprime: El cuadrado es: 16
}
Observa que la expresión numero * numero
no termina con punto y coma. Si añadiéramos un punto y coma, convertiríamos la expresión en una sentencia, lo que causaría un error de compilación:
fn calcular_cuadrado_incorrecto(numero: i32) -> i32 {
numero * numero; // Error: se esperaba un valor de retorno i32, pero se encontró ()
}
Bloques como expresiones
En Rust, los bloques de código también son expresiones, lo que significa que pueden evaluar a un valor. El valor de un bloque es el valor de su última expresión:
fn calcular_valor(x: i32) -> i32 {
let resultado = {
let y = x * 2;
let z = y + 1;
z * 2 // Valor del bloque
};
resultado // Valor de retorno de la función
}
fn main() {
println!("Resultado: {}", calcular_valor(5)); // Imprime: Resultado: 22
}
En este ejemplo, el bloque entre llaves evalúa a z * 2
, que luego se asigna a resultado
.
Combinando retorno implícito con condicionales
Podemos combinar el retorno implícito con expresiones condicionales para crear código conciso:
fn numero_a_texto(n: i32) -> &'static str {
if n == 0 {
"cero"
} else if n == 1 {
"uno"
} else if n == 2 {
"dos"
} else {
"otro número"
}
}
fn main() {
println!("{}", numero_a_texto(1)); // Imprime: uno
println!("{}", numero_a_texto(5)); // Imprime: otro número
}
La expresión if-else
completa es evaluada y su resultado se convierte en el valor de retorno de la función.
Expresión match para retornos
La expresión match
es particularmente útil con el retorno implícito, ya que permite manejar múltiples casos de forma elegante:
fn clasificar_numero(n: i32) -> &'static str {
match n {
0 => "cero",
n if n > 0 => "positivo",
_ => "negativo"
}
}
fn main() {
println!("{}", clasificar_numero(10)); // Imprime: positivo
println!("{}", clasificar_numero(-5)); // Imprime: negativo
println!("{}", clasificar_numero(0)); // Imprime: cero
}
Cada brazo del match
es una expresión cuyo valor puede convertirse en el retorno de la función.
Retorno implícito vs. retorno explícito
Aunque el retorno implícito es elegante, a veces es necesario usar return
explícito, especialmente para retornos anticipados:
fn encontrar_primer_par(numeros: &[i32]) -> Option<i32> {
for &num in numeros {
if num % 2 == 0 {
return Some(num); // Retorno anticipado cuando encontramos un número par
}
}
None // Retorno implícito si no encontramos ningún número par
}
fn main() {
let nums = [1, 3, 5, 6, 7, 8];
match encontrar_primer_par(&nums) {
Some(n) => println!("El primer número par es: {}", n),
None => println!("No hay números pares")
}
}
Retorno implícito en funciones sin valor de retorno
En funciones que no especifican un tipo de retorno (o especifican -> ()
), el valor de retorno implícito es la unidad ()
:
fn saludar(nombre: &str) { // Retorna implícitamente ()
println!("¡Hola, {}!", nombre);
}
fn saludar_explicito(nombre: &str) -> () { // Equivalente a la anterior
println!("¡Hola, {}!", nombre);
}
Consideraciones prácticas
El retorno implícito hace que el código sea más conciso y expresivo, pero hay algunas consideraciones importantes:
- Si una función debe retornar un valor pero la última expresión termina con punto y coma, el compilador mostrará un error
- Es una buena práctica ser consistente: usa retorno implícito para el flujo normal y
return
explícito solo para retornos anticipados - El retorno implícito funciona con cualquier tipo de expresión, incluyendo llamadas a funciones:
fn obtener_valor() -> i32 {
calcular_valor(5) // Llamada a otra función como retorno implícito
}
fn calcular_valor(base: i32) -> i32 {
base * 2
}
Uso con operadores lógicos
Los operadores lógicos también pueden formar parte de expresiones de retorno implícito:
fn es_adulto_y_ciudadano(edad: u8, es_ciudadano: bool) -> bool {
edad >= 18 && es_ciudadano
}
fn main() {
let puede_votar = es_adulto_y_ciudadano(20, true);
println!("¿Puede votar? {}", puede_votar); // Imprime: ¿Puede votar? true
}
El retorno implícito es una característica que hace que el código Rust sea más fluido y natural, permitiéndote expresar la lógica de forma más directa sin la necesidad constante de declaraciones return
explícitas.
Scope y shadowing
El scope (ámbito) en Rust define la región del código donde una variable es válida y accesible. Entender cómo funciona el scope es fundamental para escribir programas correctos y evitar errores comunes relacionados con la visibilidad de las variables.
Ámbito de variables
En Rust, las variables tienen un ámbito léxico, lo que significa que son válidas desde el punto donde se declaran hasta el final del bloque donde fueron creadas:
fn main() {
// La variable 'x' no existe aquí
let x = 10; // 'x' nace aquí
println!("x: {}", x); // Podemos usar 'x'
{ // Inicio de un nuevo bloque
// 'x' sigue siendo accesible aquí
println!("x dentro del bloque interno: {}", x);
let y = 20; // 'y' nace aquí
println!("y: {}", y);
} // 'y' muere aquí al finalizar el bloque
// println!("y: {}", y); // Error: 'y' no existe en este ámbito
println!("x sigue siendo accesible: {}", x);
} // 'x' muere aquí al finalizar el bloque main
Cada par de llaves {}
crea un nuevo ámbito anidado. Las variables declaradas en un ámbito interno no son accesibles desde ámbitos externos.
Ámbito en funciones
Cada función tiene su propio ámbito, y los parámetros de la función son variables que pertenecen a ese ámbito:
fn calcular_area(ancho: f64, alto: f64) -> f64 {
// 'ancho' y 'alto' son válidos dentro de esta función
let area = ancho * alto;
// 'area' también es válida solo dentro de esta función
area // Retornamos el valor de 'area'
}
fn main() {
let resultado = calcular_area(5.0, 3.0);
println!("El área es: {}", resultado);
// println!("ancho: {}", ancho); // Error: 'ancho' no existe en este ámbito
// println!("area: {}", area); // Error: 'area' no existe en este ámbito
}
Shadowing (sombreado) de variables
El shadowing es una característica de Rust que permite declarar una nueva variable con el mismo nombre que una variable existente. La nueva variable "sombrea" a la anterior, ocultándola temporalmente:
fn main() {
let mensaje = "hola"; // Tipo &str
println!("Mensaje original: {}", mensaje);
let mensaje = mensaje.len(); // Tipo usize - sombreamos la variable anterior
println!("Longitud del mensaje: {}", mensaje);
let mensaje = mensaje * 2; // Tipo usize - sombreamos nuevamente
println!("Longitud duplicada: {}", mensaje);
}
Este código imprime:
Mensaje original: hola
Longitud del mensaje: 4
Longitud duplicada: 8
El shadowing es diferente de la mutabilidad. Con shadowing, estamos creando una nueva variable que reutiliza el mismo nombre, pero puede tener un tipo diferente.
Ventajas del shadowing
El shadowing es especialmente útil en situaciones donde:
- Queremos transformar una variable pero mantener el mismo nombre
- Necesitamos cambiar el tipo de una variable sin crear un nuevo nombre
- Queremos mantener una variable inmutable después de su inicialización
fn main() {
// Convertir entrada de usuario a número
let valor = "42"; // Inicialmente es un &str
println!("Valor como texto: {}", valor);
// Transformamos a número usando shadowing
let valor = valor.parse::<i32>().unwrap(); // Ahora es un i32
println!("Valor como número: {}", valor);
// Podemos hacer cálculos con el valor numérico
let valor = valor * 2; // Seguimos usando el mismo nombre
println!("Valor duplicado: {}", valor);
}
Shadowing en bloques anidados
El shadowing también funciona en bloques anidados, donde la variable sombreada solo existe dentro de ese bloque:
fn main() {
let x = 10;
println!("x fuera: {}", x); // Imprime: x fuera: 10
{
let x = x * 2; // Sombreamos 'x' dentro de este bloque
println!("x dentro: {}", x); // Imprime: x dentro: 20
{
let x = "texto"; // Sombreamos nuevamente con un tipo diferente
println!("x más adentro: {}", x); // Imprime: x más adentro: texto
}
println!("x dentro otra vez: {}", x); // Imprime: x dentro otra vez: 20
}
println!("x fuera otra vez: {}", x); // Imprime: x fuera otra vez: 10
}
Cuando salimos de un bloque, las variables sombreadas dentro de ese bloque dejan de existir, y las variables del ámbito exterior vuelven a ser visibles.
Shadowing vs. mutabilidad
Es importante distinguir entre shadowing y mutabilidad:
fn main() {
// Usando shadowing
let valor = 5;
let valor = valor + 1; // Creamos una nueva variable con el mismo nombre
// Usando mutabilidad
let mut contador = 5;
contador = contador + 1; // Modificamos el valor de la misma variable
}
Las diferencias clave son:
- Con shadowing, creamos una nueva variable que puede tener un tipo diferente
- Con mutabilidad (
mut
), modificamos la misma variable, que debe mantener su tipo
Scope en estructuras de control
Las estructuras de control como if
, match
, loop
, while
y for
también crean sus propios ámbitos:
fn main() {
let condicion = true;
if condicion {
let x = 10; // 'x' solo existe dentro de este bloque 'if'
println!("x dentro del if: {}", x);
} else {
let y = 20; // 'y' solo existe dentro de este bloque 'else'
println!("y dentro del else: {}", y);
}
// println!("x: {}", x); // Error: 'x' no existe aquí
// println!("y: {}", y); // Error: 'y' no existe aquí
}
Aplicación práctica: conversión de unidades
Un ejemplo práctico donde el shadowing resulta útil es en la conversión de unidades:
fn convertir_temperatura() {
let temperatura = 22.5; // Celsius
println!("Temperatura en Celsius: {}°C", temperatura);
// Convertimos a Fahrenheit usando el mismo nombre
let temperatura = temperatura * 9.0 / 5.0 + 32.0;
println!("Temperatura en Fahrenheit: {}°F", temperatura);
// Convertimos a Kelvin
let temperatura = (temperatura - 32.0) * 5.0 / 9.0 + 273.15;
println!("Temperatura en Kelvin: {}K", temperatura);
}
fn main() {
convertir_temperatura();
}
Este código mantiene el nombre significativo temperatura
a través de las diferentes representaciones, lo que hace que el código sea más legible y mantenga su intención clara.
Consideraciones sobre el scope y shadowing
- Las variables en Rust tienen un ámbito léxico definido por bloques de código
- El shadowing permite reutilizar nombres de variables para diferentes propósitos
- El shadowing es diferente de la mutabilidad y permite cambiar el tipo de una variable
- Cuando una variable sale de su ámbito, Rust libera automáticamente los recursos asociados
- El shadowing puede mejorar la legibilidad del código al mantener nombres significativos
Entender estos conceptos es esencial para escribir código Rust efectivo y evitar errores comunes relacionados con la visibilidad y el ciclo de vida de las variables.
Otras lecciones de Rust
Accede a todas las lecciones de Rust y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Introducción A Rust
Introducción Y Entorno
Primer Programa
Introducción Y Entorno
Instalación Del Entorno
Introducción Y Entorno
Funciones
Sintaxis
Operadores
Sintaxis
Estructuras De Control Condicional
Sintaxis
Arrays Y Strings
Sintaxis
Manejo De Errores Panic
Sintaxis
Variables Y Tipos Básicos
Sintaxis
Estructuras De Control Iterativo
Sintaxis
Colecciones Estándar
Estructuras De Datos
Option Y Result
Estructuras De Datos
Pattern Matching
Estructuras De Datos
Estructuras (Structs)
Estructuras De Datos
Enumeraciones Enums
Estructuras De Datos
El Concepto De Ownership
Ownership
Lifetimes Básicos
Ownership
Slices Y Referencias Parciales
Ownership
References Y Borrowing
Ownership
Funciones Anónimas Closures
Abstracción
Traits De La Biblioteca Estándar
Abstracción
Traits
Abstracción
Generics
Abstracción
Channels Y Paso De Mensajes
Concurrencia
Memoria Compartida Segura
Concurrencia
Threads Y Sincronización Básica
Concurrencia
Introducción A Tokio
Asincronía
Fundamentos Asíncronos Y Futures
Asincronía
Async/await
Asincronía
Ejercicios de programación de Rust
Evalúa tus conocimientos de esta lección Funciones con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
En esta lección
Objetivos de aprendizaje de esta lección
- Comprender cómo declarar funciones y utilizar parámetros en Rust.
- Aprender el concepto de retorno implícito y explícito en funciones.
- Entender el ámbito (scope) de variables y cómo afecta su visibilidad.
- Conocer el shadowing y su diferencia con la mutabilidad.
- Aplicar buenas prácticas en la documentación y organización de funciones.