Aprende Rust → Lección 3
Rust comprueba los tipos antes de ejecutar nada, pero casi nunca hay que escribirlos: los deduce del uso. El resultado es un lenguaje que se lee casi como uno dinámico y se equivoca como uno estático, es decir, en el momento de compilar. Esta lección cubre cómo se declaran los valores, qué tipos hay, cuándo la inferencia necesita ayuda y por qué Rust nunca convierte un número por su cuenta.
Las variables no varían, salvo que lo pidas
Un let en Rust no crea una variable en el sentido habitual, sino un nombre para un valor. Y ese nombre, por defecto, no se puede reasignar:
entrada2 o entrada_limpia.Las tres formas en funcionamiento:
fn main() {
let a = 5;
println!("a = {a}");
let mut b = 5;
b = 6;
println!("b = {b}");
let entrada = " 42 ";
let entrada = entrada.trim();
let entrada: i32 = entrada.parse().unwrap();
println!("entrada = {}", entrada + 1);
}
Mira lo que ocurre con entrada. Empieza siendo un texto con espacios, luego un texto limpio y acaba siendo un número, todo con el mismo nombre y sin mut. Eso es sombreado: cada let crea un valor nuevo que tapa al anterior. Con mut no se podría, porque un valor mutable puede cambiar de contenido pero nunca de tipo.
Constantes
Para un valor que se conoce al escribir el programa y no cambia nunca está const. Exige anotar el tipo, se puede declarar fuera de toda función y por convención va en mayúsculas:
const MAX_REINTENTOS: u32 = 3;
fn main() {
for intento in 1..=MAX_REINTENTOS {
println!("intento {intento} de {MAX_REINTENTOS}");
}
}
Esa convención de mayúsculas no es solo estética. Como vimos en la lección 5, dentro de un patrón un nombre en minúsculas crea una variable nueva en vez de comparar; uno en mayúsculas se reconoce como constante y sí compara.
Los tipos que vienen de fábrica
| Tipo | Qué guarda | Notas |
|---|---|---|
i8 … i128 |
Enteros con signo | i32 es el que se usa si no dices nada |
u8 … u128 |
Enteros sin signo | Un u8 va de 0 a 255 |
isize, usize |
Enteros del tamaño de la máquina | usize es el tipo de los índices y las longitudes |
f32, f64 |
Coma flotante | f64 es el predeterminado |
bool |
true o false |
No se convierte a número; un 0 no es falso |
char |
Un carácter Unicode | Ocupa 4 bytes, no 1: cabe una «ñ» o un emoji |
(i32, f64) |
Una tupla | Tamaño fijo, tipos distintos en cada posición |
[i32; 5] |
Un arreglo | Tamaño fijo y conocido al compilar |
&str, String |
Texto prestado y texto propio | La distinción es la de la idea de propiedad |
Dos detalles que ahorran tropiezos. El primero es usize: cuando indexas una colección o pides su longitud, el tipo que aparece es ese, y mezclarlo con un i32 requiere una conversión explícita. El segundo es que bool no es un número; en Rust no existe el if (contador) de otros lenguajes, la condición tiene que ser un booleano de verdad.
fn main() {
let tupla: (i32, f64, char) = (500, 6.4, 'ñ');
let (x, _, c) = tupla;
println!("x = {x}, tercero = {c}, segundo = {}", tupla.1);
let notas = [90, 75, 88, 100, 62];
println!("{} notas, la primera es {}", notas.len(), notas[0]);
let media: f64 = notas.iter().sum::<i32>() as f64 / notas.len() as f64;
println!("media = {media:.2}");
}
En la última línea aparecen dos conversiones con as, y no están ahí por gusto: sum() da un i32 y len() da un usize, así que dividirlos directamente no compila. Rust no asciende tipos automáticamente como hacen C o Java.
La inferencia y dónde se queda corta
Casi nunca hace falta anotar el tipo de una variable local, porque el compilador lo deduce del valor y de lo que se hace con él después. Pero hay tres situaciones en las que la información no le llega, y entonces pide ayuda.
La más frecuente es parse, que puede devolver cualquier tipo numérico y necesita saber cuál quieres. Se puede decir de dos maneras, y las dos son habituales:
fn main() {
let a: u16 = "8080".parse().unwrap();
let b = "8080".parse::<u16>().unwrap();
println!("{a} {b}");
}
La forma con dos puntos dobles y el tipo entre ángulos se conoce como turbofish, y sirve para los casos en los que no hay una variable que anotar, por ejemplo cuando el resultado se usa directamente. Las otras dos situaciones son parecidas: una colección vacía, donde todavía no hay elementos de los que deducir el tipo, y collect, que puede construir muchas colecciones distintas a partir del mismo iterador.
Conversiones: Rust nunca convierte por su cuenta
Esta es probablemente la fricción más común al llegar de otro lenguaje. Sumar un u8 y un u32 no compila, y no porque falte una característica, sino porque cada conversión tiene un coste o un riesgo y Rust quiere que sea visible. Hay cuatro herramientas y conviene no confundirlas.
as es la única que puede estropear un número en silencio.El peligro de as se ve mejor con un número concreto que con una explicación:
use std::convert::TryFrom;
fn main() {
let grande: u32 = 300;
let truncado = grande as u8;
println!("as -> {truncado}");
match u8::try_from(grande) {
Ok(v) => println!("try_from -> {v}"),
Err(e) => println!("try_from -> falla: {e}"),
}
let seguro: u32 = u32::from(7u8);
println!("from -> {seguro}");
}
El 300 no cabe en un u8, que llega hasta 255. Con as el programa no se queja y se queda con 44, que es lo que sobra al descartar los bits que no caben. Con try_from obtienes un Err y decides tú qué hacer, que es justo el Result de la lección 7. Son resultados muy distintos para la misma intención, y el que elijas debería depender de si de verdad sabes que el valor cabe.
cargo run, en modo de depuración, el programa se detiene con attempt to add with overflow; con cargo run --release no hay comprobación y el valor da la vuelta. Si los dos operandos son literales ni siquiera llega a ejecutarse: el compilador lo detecta y rechaza el programa con this arithmetic operation will overflow. Para un comportamiento concreto y garantizado en ambos modos están los métodos explícitos; sobre 250u8 + 10, checked_add devuelve None, saturating_add se queda en 255 y wrapping_add da 4.Funciones: los tipos sí se escriben
La inferencia funciona dentro de una función, no en su frontera. Los parámetros y el valor de retorno llevan tipo obligatoriamente, y eso es deliberado: la firma es un contrato que se puede leer sin mirar el cuerpo, y evita que un cambio interno altere en silencio cómo se usa desde fuera.
fn area(ancho: f64, alto: f64) -> f64 {
ancho * alto
}
fn saludar(nombre: &str) {
println!("Hola, {nombre}");
}
fn main() {
saludar("Ada");
println!("{}", area(3.0, 4.0));
}
Fíjate en que area no lleva return. La última expresión de un bloque, escrita sin punto y coma, es el valor del bloque. Y saludar no declara tipo de retorno porque no devuelve nada; en realidad devuelve (), la tupla vacía, que es la forma que tiene Rust de decir «ningún valor útil».
El punto y coma que rompe el programa
Esa regla tiene una consecuencia que desconcierta a todo el mundo al principio. Estas dos funciones se diferencian en un carácter, y solo una compila:
fn bien(x: i32) -> i32 {
x + 1
}
fn mal(x: i32) -> i32 {
x + 1;
}
Con el punto y coma, x + 1 deja de ser el valor del bloque y pasa a ser una instrucción cuyo resultado se descarta. La función entonces no devuelve nada, es decir devuelve (), y el compilador protesta con mismatched types: expected i32, found (). Cuando veas ese mensaje, lo primero que hay que mirar es el punto y coma de la última línea.
if, a match y a los bloques sueltos, así que en Rust se escribe let estado = if activo { "sí" } else { "no" }; en lugar de declarar la variable vacía y rellenarla en cada rama. Es el motivo de que apenas se vea el operador ternario de otros lenguajes: no hace falta.fn clasificar(nota: u32) -> &'static str {
if nota >= 90 {
"excelente"
} else if nota >= 70 {
"aprobado"
} else {
"insuficiente"
}
}
fn main() {
for n in [95, 80, 50] {
println!("{n}: {}", clasificar(n));
}
}
Las tres ramas devuelven el mismo tipo, que es lo que exige el compilador para poder darle un tipo al if entero. Si una devolviera un número y otra un texto, no habría forma de decir qué tipo tiene la expresión y el programa no compilaría.
En resumen
| Lo que quieres hacer | Qué escribir |
|---|---|
| Dar nombre a un valor que no cambia | let x = …; |
| Poder cambiarle el contenido | let mut x = …; |
| Reutilizar el nombre con otro tipo | otro let, que lo sombrea |
| Un valor fijo conocido al compilar | const NOMBRE: Tipo = …; |
| Indexar o medir una colección | usize |
| Convertir sabiendo que cabe | u32::from(x) o x.into() |
| Convertir sin saber si cabe | u8::try_from(x) |
| Convertir desde texto | texto.parse::<u16>() |
Decirle el tipo a parse o collect |
anotar la variable o usar el turbofish |
| Devolver un valor de una función | la última expresión, sin punto y coma |
La idea que conviene llevarse es que en Rust el tipo no es papeleo, es la información con la que el compilador trabaja por ti. Cuanto más preciso seas al elegirlo —un u8 donde solo caben valores pequeños, un tipo propio en vez de un f64 suelto—, más errores detecta antes de que el programa llegue a ejecutarse.
Para verlo explicado

«3. Curso de programación en RUST: TIPOS de DATOS», del canal Luis Serrano Donaire (13:43). Material de otro canal que recomendamos como complemento, no una producción de decodigo.com. El reproductor solo se carga al pulsar, así que YouTube no recibe nada tuyo hasta entonces. También puedes verlo en YouTube.