Variables, Mutabilidad y Tipos Escalares/Compuestos
En Rust, cada variable está bloqueada por defecto — tienes que pedir permiso para cambiarla, y esa sola regla evita toda una categoría de errores.
En la mayoría de los lenguajes, una variable que declaras es mutable a menos que te tomes el trabajo de marcarla de otra forma — final en Java, const en JavaScript cuando te acuerdas de escribirlo. Rust invierte ese valor por defecto: escribir let x = 5; crea una variable que no puedes reasignar, y el compilador te va a detener con un error claro — cannot assign twice to immutable variable — en el momento en que lo intentes. No es una restricción arbitraria; es una decisión de diseño deliberada, porque la mayoría de las variables en programas reales nunca se reasignan, y hacer de la inmutabilidad el valor por defecto significa que cada mut que ves en el código de alguien es una señal: "este valor cambia, presta atención aquí".
Cuando un valor realmente necesita cambiar, lo habilitas con let mut x = 5;, y después de eso x = 6; es perfectamente válido. Sin embargo, el tipo de una variable mut nunca puede cambiar entre reasignaciones — estás actualizando el mismo binding en el lugar, no creando uno nuevo. Esta distinción importa más de lo que parece, porque Rust te da una segunda herramienta, completamente distinta, para una situación que se ve parecida: el shadowing.
Shadowing significa declarar una nueva variable con let usando un nombre que ya existe en ese scope. Se ve como una reasignación, pero no lo es — cada let crea un binding completamente nuevo, por eso puedes hacer shadowing de un &str con un usize, o de un valor inmutable con otro valor inmutable que resulta ser una versión transformada del primero. Este es el patrón idiomático en Rust para algo muy común: parsear o transformar un valor en varios pasos sin necesitar una variable mut ni una pila de variables con nombres distintos como raw_input, trimmed_input, parsed_input.
Debajo de todo esto están los tipos escalares de Rust — los que representan un único valor. Los enteros vienen en versiones con signo (i8 hasta i128, más isize) y sin signo (u8 hasta u128, más usize), donde el número indica el ancho en bits; si no anotas ninguno, Rust usa i32 por defecto, que es la opción más rápida en la mayoría de las máquinas. usize e isize son especiales: su tamaño coincide con el ancho de puntero de tu plataforma, y usize es el tipo que se usa para indexar arrays y colecciones. Los números de punto flotante vienen como f32 o f64, con f64 por defecto por su precisión extra a un costo prácticamente nulo en CPUs modernas. Completan el conjunto bool (true/false) y char, que — a diferencia del char de un solo byte en C — siempre es un valor escalar Unicode completo de 4 bytes, así que puede contener un emoji o una letra acentuada con la misma facilidad que 'a'.
Los tipos compuestos agrupan varios valores en uno solo. Una tupla, escrita (3.0, 4.0, 0.0), puede mezclar tipos distintos en posiciones fijas, y puedes extraer los valores ya sea desestructurando (let (x, y, z) = point;) o por índice (point.0). Un array, escrito [i32; 3], tiene longitud fija y contiene un único tipo, ubicado de forma contigua en el stack — precisamente por eso no puede crecer; cuando necesites una lista redimensionable vas a recurrir a Vec<T> en una lección posterior, que es el primo del array con memoria en el heap y tamaño variable.
const se parece a let, pero es un mecanismo completamente distinto: una constante siempre debe llevar una anotación de tipo explícita, su valor debe poder calcularse enteramente en tiempo de compilación, nunca puede sombrearse ni volverse mutable, y por convención su nombre va en SCREAMING_SNAKE_CASE. Como no tiene una dirección de memoria fija como sí la tiene un static, el compilador es libre de insertar su valor directamente en cada lugar donde se usa — se comporta menos como una variable y más como un literal con nombre y tipo verificado que simplemente vive en un solo lugar de tu código fuente.
Por último, fíjate en lo poco que has tenido que anotar tipos hasta ahora. El compilador de Rust infiere el tipo de la mayoría de los bindings let a partir de cómo se usa el valor más adelante en la función, por eso let x = 5; y let point: (f64, f64, f64) = (3.0, 4.0, 0.0) pueden coexistir — la tupla necesitó una anotación solo porque los literales de punto flotante por defecto ya son f64 de todas formas, así que anotar ahí documenta la intención más que resolver una ambigüedad. La inferencia se detiene en los límites de las funciones, eso sí: todo parámetro y tipo de retorno debe escribirse explícitamente, porque Rust deliberadamente no infiere a través del límite de una API pública — alguien que llama a la función y solo lee su firma nunca debería tener que mirar dentro del cuerpo para saber qué tipos espera.
fn main() {let x = 5;println!("The value of x is: {}", x);let mut y = 5;y = 6;println!("The value of y is: {}", y);}
let x is immutable and cannot be reassigned, but let mut y opts into reassignment — removing mut from y's declaration would turn the y = 6 line into a compile error.
fn main() {let spaces = " ";let spaces = spaces.len();println!("Number of spaces: {}", spaces);let x = 5;let x = x + 1;let x = x * 2;println!("x is now: {}", x);}
Shadowing lets the name spaces go from a &str to a usize, and lets x be transformed twice in a row, without ever needing a mut binding.
const MAX_PLAYERS: u32 = 4;fn main() {let point: (f64, f64, f64) = (3.0, 4.0, 0.0);let (x, y, z) = point;println!("Coordinates: ({}, {}, {})", x, y, z);let scores: [i32; 3] = [90, 85, 100];println!("First score: {}", scores[0]);println!("Max players allowed: {}", MAX_PLAYERS);}
A tuple destructured by pattern, a fixed-size array indexed by position, and a top-level const — notice const requires its type annotation while the let bindings do not.
🧠 Comprueba tu comprensión
0/1 · 0/1 answered1. What's the real difference between shadowing a variable with a new let x = ... and reassigning it with x = ... after declaring let mut x?