Ir al contenido

Ámbito en Rust

El ámbito (scope) es la zona del código en la que una variable es válida. Es una idea sencilla, pero entenderla hace que muchas otras reglas de Rust (sobre todo el ownership, la propiedad) encajen solas.

Una variable existe desde el punto en que se declara hasta la } que cierra el bloque en el que está:

fn main() {
let x = 5; // x entra en el ámbito aquí
{
let y = 10; // y entra en el ámbito aquí
println!("x = {x}, y = {y}"); // las dos son válidas
} // y sale del ámbito aquí
println!("x = {x}");
// println!("{y}"); // ❌ error: y ya no está en el ámbito
}

Salida:

x = 5, y = 10
x = 5
▶ Pruébalo tú mismo

y solo existe dentro del bloque interior { }. En cuanto ese bloque termina, y desaparece: intentar usarla después es un error de compilación, no una sorpresa en tiempo de ejecución.

El ámbito solo funciona en una dirección: un bloque interior puede ver las variables de los bloques que lo rodean (por eso el bloque interior puede imprimir x), pero un bloque exterior no puede ver lo que hay dentro de uno interior.

El cuerpo de una función también es un ámbito

Sección titulada «El cuerpo de una función también es un ámbito»

Los parámetros y cualquier variable que crees dentro de una función solo existen mientras dura esa llamada a la función:

fn calculate() -> i32 {
let result = 5 * 2; // result solo existe dentro de calculate
result
}
fn main() {
let value = calculate();
println!("{value}");
// println!("{result}"); // ❌ error: result no existe aquí fuera
}

Salida:

10
▶ Pruébalo tú mismo

Lo mismo pasa en sentido contrario: una función no puede ver las variables locales de main. Si necesita un valor, se lo pasas como parámetro, como viste en la página de Funciones.

Un bloque { } no solo limita dónde viven las variables. También puede producir un valor: la última expresión de su interior, escrita sin punto y coma. Es la misma regla que hace que una función devuelva su última línea.

Esto es muy útil cuando necesitas algunas variables auxiliares para calcular algo, pero no quieres que se queden por ahí después:

fn main() {
let price = 40;
let total = {
let tax = price / 10; // tax solo vive dentro de este bloque
let shipping = 5;
price + tax + shipping // sin punto y coma: este es el valor del bloque
};
println!("total = {total}");
// println!("{tax}"); // ❌ error: tax está fuera de su ámbito
}

Salida:

total = 49
▶ Pruébalo tú mismo

Dos detalles a tener en cuenta:

  • La última línea, price + tax + shipping, no lleva punto y coma. Si se lo pones, el bloque devuelve () (nada) en lugar de un número, y println! no puede imprimirlo.
  • Todo el conjunto es una instrucción let, así que termina con }; después de la llave de cierre.

Cuando el bloque termina, tax y shipping desaparecen. Solo queda el resultado, total. Guardar las variables auxiliares en un bloque pequeño como este deja claro que solo hacen falta para un cálculo.

Por qué el ámbito importa para el ownership

Sección titulada «Por qué el ámbito importa para el ownership»

Esto conecta directamente con el ownership (propiedad), que conocerás un poco más adelante: un valor se limpia automáticamente en el momento en que termina el ámbito de su dueño.

fn main() {
{
let text = String::from("hola");
println!("{text}");
} // aquí termina el ámbito de text: Rust lo libera (drop) automáticamente
}

Salida:

hola
▶ Pruébalo tú mismo

Ese es todo el mecanismo que permite a Rust funcionar sin recolector de basura (garbage collector): ya sabe, al compilar, exactamente dónde termina cada ámbito.

¿Recuerdas el shadowing (sombreado) de la página de Variables? Cada let con el mismo nombre dentro de un ámbito crea una variable distinta: no borra la de fuera, solo ocupa su lugar durante el resto de ese ámbito:

fn main() {
let x = 5;
{
let x = x * 2; // una x nueva, solo dentro de este bloque
println!("x interior = {x}"); // 10
}
println!("x exterior = {x}"); // 5: no ha cambiado
}

Salida:

x interior = 10
x exterior = 5
▶ Pruébalo tú mismo

Este programa no compila, porque area se usa después de que su bloque haya terminado:

fn main() {
let width = 3;
{
let height = 4;
let area = width * height;
}
println!("área = {area}");
}

Cámbialo para que imprima área = 12, sin que height deje de vivir solo dentro del bloque. Pista: haz que el bloque produzca el área y guarda el resultado en una variable declarada fuera de él.

Ver solución
fn main() {
let width = 3;
let area = {
let height = 4;
width * height
};
println!("área = {area}");
}

Salida:

área = 12
▶ Pruébalo tú mismo

Ahora area vive en el ámbito de main, así que println! puede verla. height sigue haciendo falta solo dentro del bloque, y se libera cuando el bloque termina.

Crea una variable level con el valor 1. Abre un bloque que la sombree con 2 y, dentro de él, otro bloque que la sombree con 3. Imprime level en cada bloque, y otra vez justo después de que se cierre cada bloque. Pista: deberías ver los números 1, 2, 3, y luego bajar de nuevo a 2 y 1.

Ver solución
fn main() {
let level = 1;
println!("nivel {level}");
{
let level = 2;
println!("nivel {level}");
{
let level = 3;
println!("nivel {level}");
}
println!("de vuelta al nivel {level}");
}
println!("de vuelta al nivel {level}");
}

Salida:

nivel 1
nivel 2
nivel 3
de vuelta al nivel 2
de vuelta al nivel 1
▶ Pruébalo tú mismo

Cada level interior oculta al exterior solo hasta que termina su bloque. Las variables exteriores nunca cambiaron, así que siguen ahí, esperando.

  • El ámbito es la zona (marcada por { }) en la que una variable es válida.
  • Una variable deja de existir en el instante en que se llega a la } que cierra su ámbito.
  • El cuerpo de una función también es un ámbito: sus variables locales desaparecen cuando la función termina.
  • Un bloque es una expresión: su última línea (sin punto y coma) es su valor, y sus variables auxiliares desaparecen cuando termina.
  • El ámbito es lo que le dice a Rust exactamente cuándo liberar un valor, y por eso no necesita recolector de basura.

Comprueba lo aprendido

1. Según esta página, ¿por qué Rust no necesita un recolector de basura?

2. ¿Qué pasa cuando haces shadowing de una variable con let x = x * 2; dentro de un bloque interior?

3. ¿Cuál es el valor de total después de let total = { let a = 2; a * 10 };?

Puntuación: 0 / 3