En esta página
Variables, mutabilidad y tipos primitivos
Variables en Rust: inmutables por defecto
Una de las primeras sorpresas de Rust para desarrolladores que vienen de otros lenguajes es que las variables son inmutables por defecto. Cuando declaras let x = 5, el valor de x no puede cambiar.
Esto no es una limitación — es una característica de diseño deliberada. La inmutabilidad por defecto ayuda al compilador a hacer optimizaciones, facilita el razonamiento sobre el código y previene una categoría completa de bugs donde un valor cambia inesperadamente.
fn main() {
let x = 5;
x = 6; // Error: cannot assign twice to immutable variable `x`
}El compilador de Rust produce un mensaje de error claro:
error[E0384]: cannot assign twice to immutable variable `x`
--> src/main.rs:3:5
|
2 | let x = 5;
| - first assignment to `x`
3 | x = 6;
| ^^^^^ cannot assign twice to immutable variable
|
help: consider making this binding mutable
|
2 | let mut x = 5;
| +++Nota cómo el compilador incluso te sugiere la solución.
Variables mutables con `mut`
Cuando necesitas que una variable cambie, añades la palabra clave mut:
fn main() {
let mut temperatura = 20.0_f64;
println!("Temperatura inicial: {temperatura}°C");
temperatura += 5.0;
println!("Temperatura aumentada: {temperatura}°C");
temperatura *= 1.8;
temperatura += 32.0;
println!("En Fahrenheit: {temperatura}°F");
}La mutabilidad es explícita e intencionada. Cuando lees código Rust y ves let mut, sabes inmediatamente que esa variable va a cambiar. Cuando ves solo let, sabes que no va a cambiar — lo cual es información valiosa para entender el flujo del programa.
Constantes con `const`
Las constantes se declaran con const y siempre requieren una anotación de tipo explícita. Además, solo pueden contener valores que se calculan en tiempo de compilación:
// Convención: SCREAMING_SNAKE_CASE para constantes
const VELOCIDAD_DE_LUZ: u64 = 299_792_458; // metros por segundo
const PI: f64 = 3.14159265358979323846;
const NOMBRE_APP: &str = "MiAplicación";
fn main() {
let radio = 5.0_f64;
let area = PI * radio * radio;
println!("Área del círculo: {area:.2}");
}Las constantes son diferentes a las variables inmutables:
- Existen para toda la duración del programa (no tienen un scope como
let) - Pueden declararse en el scope global (fuera de funciones)
- Su valor debe ser calculable en tiempo de compilación
Shadowing: redeclarar variables
Rust permite redeclarar una variable con let en el mismo scope. La nueva declaración "sombrea" (shadows) la anterior:
fn main() {
let x = 5;
// El segundo `let x` sombrea al primero
let x = x + 1;
{
// Este shadowing solo existe en este bloque
let x = x * 2;
println!("x en el bloque interno: {x}"); // 12
}
println!("x en el scope externo: {x}"); // 6
}La diferencia crucial entre shadowing y mut es que el shadowing puede cambiar el tipo:
fn main() {
// Primero es una cadena de texto
let valor = "42";
println!("Como texto: {valor}");
// Ahora es un número — mismo nombre, distinto tipo
let valor: i32 = valor.parse().expect("No es un número");
println!("Como número: {valor}");
// Ahora es un booleano
let valor = valor > 0;
println!("Es positivo: {valor}");
}Con mut esto sería imposible — no puedes cambiar el tipo de una variable mutable.
Tipos escalares en Rust
Enteros
Rust tiene enteros con signo (i) y sin signo (u) de varios tamaños:
| Tipo | Rango |
|---|---|
i8 |
-128 a 127 |
i16 |
-32,768 a 32,767 |
i32 |
-2,147,483,648 a 2,147,483,647 (por defecto) |
i64 |
-9.2 × 10¹⁸ a 9.2 × 10¹⁸ |
i128 |
Enorme |
isize |
Dependiente de la arquitectura (32 o 64 bits) |
u8 |
0 a 255 |
u32 |
0 a 4,294,967,295 |
u64 |
0 a 18.4 × 10¹⁸ |
usize |
Para índices y tamaños de colecciones |
fn main() {
let entero_con_signo: i32 = -42;
let entero_sin_signo: u32 = 42;
let indice: usize = 0; // Para indexar arrays/vectores
// Literales en distintas bases
let decimal = 1_000_000;
let hexadecimal = 0xFF; // 255
let octal = 0o77; // 63
let binario = 0b1111_0000; // 240
let byte: u8 = b'A'; // 65 (código ASCII)
println!("{decimal} {hexadecimal} {octal} {binario} {byte}");
}Flotantes
fn main() {
let f32_val: f32 = 3.14; // 32 bits, menor precisión
let f64_val: f64 = 3.14159265358979; // 64 bits, por defecto
// Notación científica
let avogadro = 6.022e23_f64;
let electron = 1.6e-19_f64;
println!("{f32_val:.2}");
println!("{f64_val:.10}");
}Booleanos
fn main() {
let es_rust_genial: bool = true;
let tiene_gc: bool = false;
println!("¿Es Rust genial? {es_rust_genial}");
println!("¿Tiene GC? {tiene_gc}");
// Los booleanos ocupan 1 byte en memoria
println!("Tamaño de bool: {} byte(s)", std::mem::size_of::<bool>());
}Caracteres
El tipo char en Rust representa un punto de código Unicode completo (4 bytes), no solo ASCII:
fn main() {
let letra: char = 'A';
let emoji: char = '🦀'; // El cangrejo de Rust
let chino: char = '中';
let acento: char = 'ñ';
println!("{letra} {emoji} {chino} {acento}");
println!("Tamaño de char: {} bytes", std::mem::size_of::<char>());
}&str vs String
Esta distinción es fundamental en Rust y merece atención especial:
&str (string slice): Es una referencia a datos de texto que alguien más posee. El texto suele estar en el binario del programa o en algún String en el heap. Es inmutable y de tamaño conocido en tiempo de compilación (o en tiempo de ejecución).
String: Es un tipo de texto de tamaño dinámico que posee sus datos en el heap. Es mutable y puede crecer o reducirse.
fn main() {
// &str — string literal, en el segmento de datos del binario
let saludo: &str = "Hola, Rust";
// String — en el heap, poseído y mutable
let mut nombre = String::from("Rustáceo");
nombre.push_str(" el Cangrejo");
nombre.push('!');
println!("{saludo}, {nombre}");
// Conversiones
let s: String = saludo.to_string();
let r: &str = &nombre; // String -> &str con &
// len() en &str es bytes, no caracteres Unicode
let texto = "ñoño";
println!("Bytes: {}", texto.len()); // 6 (ñ ocupa 2 bytes en UTF-8)
println!("Chars: {}", texto.chars().count()); // 4
}Inferencia de tipos
Rust tiene inferencia de tipos sofisticada. En la mayoría de casos no necesitas anotar el tipo explícitamente:
fn main() {
let x = 42; // i32 por defecto
let y = 3.14; // f64 por defecto
let z = true; // bool
let s = "hola"; // &str
// Cuando el contexto lo requiere, anotas el tipo
let numeros: Vec<i32> = Vec::new();
let parsed: u64 = "100".parse().unwrap();
// O con turbofish cuando llamas funciones genéricas
let parsed2 = "100".parse::<u64>().unwrap();
println!("{x} {y} {z} {s} {parsed} {parsed2}");
}Con un dominio sólido de variables y tipos primitivos, estás listo para explorar funciones y tipos compuestos, que te permitirán estructurar programas más complejos.
fn main() {
// Por defecto: inmutable
let x = 5;
println!("x = {x}");
// x = 6; // Error de compilación
// Con mut: mutable
let mut contador = 0;
contador += 1;
contador += 1;
println!("contador = {contador}"); // 2
// Shadowing: redeclarar con let
let y = 10;
let y = y * 2; // sombrea la anterior
let y = y + 3; // sombrea de nuevo
println!("y = {y}"); // 23
// Shadowing permite cambiar el tipo
let espacios = " "; // &str
let espacios = espacios.len(); // usize
println!("espacios = {espacios}");
// Constantes: siempre inmutables, necesitan tipo
const MAX_PUNTOS: u32 = 100_000;
println!("máximo = {MAX_PUNTOS}");
}
Inicia sesión para guardar tu progreso