wandres.dev
CONTROL DE FLUJO · if, loop, while, match

if como expresión

En Rust, if no es una sentencia sino una expresión que devuelve un valor: let x = if cond { a } else { b }. La razón profunda de por qué Rust es un lenguaje orientado a expresiones.

⏱ 12 min

En C, if es una sentencia: bifurca la ejecución pero no produce nada, y para elegir entre dos valores necesitas el operador ternario ?:. En Rust, if es una expresión: evalúa a un valor que puedes asignar, retornar o pasar como argumento. El cambio no es cosmético; delata que Rust pertenece a la estirpe de los lenguajes orientados a expresiones, y esa propiedad reorganiza toda la sintaxis que verás después.

🎯 Al terminar esta lección sabrás
  • Distinguir una expresión, que produce un valor, de una sentencia, que no.
  • Usar if/else como expresión: let x = if cond { a } else { b }.
  • Entender por qué las ramas deben unificar en un único tipo.
  • Ver el papel del ; como el operador que descarta un valor.

Expresión contra sentencia

La distinción que gobierna toda la sintaxis de Rust es esta: una expresión se evalúa y produce un valor; una sentencia ejecuta una acción y no produce ninguno. 5 + 3 es una expresión (vale 8); let y = 6; es una sentencia (liga un nombre, pero no es un valor). En Rust casi todo es expresión —literales, llamadas, bloques, if, match, loop— y solo hay dos sentencias genuinas: las declaraciones let y las definiciones de ítems (fn, struct).

Esto tiene una consecuencia inmediata que sorprende a quien viene de C:

let x = (let y = 6); // ERROR: `let` es una sentencia, no una expresión

Como let y = 6 no produce valor, no puede aparecer donde se espera uno. En C, x = y = 6 funciona porque la asignación es una expresión que devuelve el valor asignado; Rust corta esa fuente de bugs de raíz: la asignación evalúa a (), no al valor.

if devuelve un valor

Puesto que if es una expresión, su resultado se puede asignar directamente. Esta es la forma idiomática de lo que en otros lenguajes sería un ternario:

let condicion = true;
let numero = if condicion { 5 } else { 6 };
//                       ^ sin punto y coma: el bloque "vale" 5
println!("{numero}"); // 5

Rust no tiene operador ternario ?:, y no lo necesita: if/else ocupa ese lugar sin introducir una segunda sintaxis. Fíjate en que dentro de cada rama, la última expresión sin ; es el valor de ese bloque. Si le pusieras punto y coma, el bloque valdría () y todo se rompería (lo veremos abajo).

Como es una expresión, un if encadenado funciona igual que la cascada else if de siempre, pero devolviendo un valor:

let nota = 87;
let letra = if nota >= 90 {
    'A'
} else if nota >= 80 {
    'B'
} else if nota >= 70 {
    'C'
} else {
    'F'
};

Y como cualquier expresión, un if puede vivir dentro de otra sin pasar por una variable intermedia: como argumento de una llamada, o como un sumando dentro de un cálculo mayor.

println!("{}", if aprobado { "apto" } else { "no apto" });
let total = base + if premium { 100 } else { 0 };

Por qué las ramas deben ser del mismo tipo

Aquí aparece el rigor de Rust. La expresión if completa tiene un tipo estático, decidido en compilación, así que todas las ramas deben producir el mismo. Si difieren, el compilador se niega:

let x = if condicion { 5 } else { "seis" };
// ERROR E0308: `if` and `else` have incompatible types
//   expected integer, found `&str`

No hay conversión implícita ni “tipo común” al estilo de C: i32 y &str no unifican, y punto. Esta exigencia es la que hace seguro asignar el resultado: el tipo de x queda determinado sin ambigüedad, sin importar qué rama se ejecute en tiempo real.

De aquí sale también la regla del else obligatorio. Un if sin else solo puede usarse por su efecto, no por su valor, porque cuando la condición es falsa no hay nada que devolver. Rust rellena esa rama ausente con ():

let x = if condicion { 5 }; // ERROR: `if` may be missing an `else` clause
//   expected `()` because of implicit return, found integer

El mensaje lo dice todo: la rama implícita vale () y la explícita vale un entero; no unifican. Para usar if como expresión, el else no es opcional.

Hay una excepción luminosa a que las ramas deban coincidir: una rama puede divergir —entrar en pánico, retornar del método— y el if compila igual, porque una expresión que nunca produce valor encaja con cualquier tipo. Lo estudiarás a fondo con el tipo never en la última lección de este nivel.

let dato: i32 = if hay_dato { leer() } else { panic!("falta el dato") };
//                            i32              ! encaja como i32
ℹ️
El punto y coma es un operador, no puntuación

En Rust, ; no es decorado sintáctico: transforma una expresión en sentencia y descarta su valor, dejando () en su lugar. Por eso { 5 } vale 5 pero { 5; } vale (). Cuando un if usado como expresión falla con “expected (), found integer”, casi siempre es un ; de más al final de una rama. Aprende a leer el punto y coma como lo que es: el gesto de decir “ejecuta esto y tira el resultado”.

Todo confluye en un valor

El patrón se generaliza. match, loop y los bloques { } son también expresiones, de modo que se componen unos con otros con naturalidad: puedes devolver un if desde una función sin return, meterlo dentro de un match, o pasarlo como argumento.

fn signo(x: i32) -> i32 {
    if x > 0 { 1 } else if x < 0 { -1 } else { 0 }
    // sin `return`, sin `;`: esta expresión ES el valor de la función
}

El mismo signo escrito con match deja ver que ambas construcciones son expresiones intercambiables: las dos producen el valor devuelto sin un return a la vista.

fn signo(x: i32) -> i32 {
    match x {
        0 => 0,
        n if n > 0 => 1,
        _ => -1,
    }
}
flowchart LR
E[Expresion] -->|produce| V[Un valor con tipo]
S[Sentencia let o item] -->|produce| U[unit]
V -->|un punto y coma| U
style E fill:#a6e3a1,color:#11111b
style V fill:#89b4fa,color:#11111b
style S fill:#f9e2af,color:#11111b
style U fill:#f38ba8,color:#11111b
💡
Expresión no significa 'todo en una línea'

Que if sea una expresión no te obliga a exprimir cada rama a una sola línea ni a encadenar diez else if ilegibles. Si una rama necesita varias sentencias, ponlas: el bloque las ejecuta y su última expresión sin ; es el valor. Y si un condicional se usa solo por su efecto —imprimir, mutar— escríbelo como un if normal sin asignarlo; forzar un valor donde no lo hay es tan artificioso como esconderlo donde sí. La regla es de intención: usa if como expresión cuando de verdad estás eligiendo un valor.

Orientado a expresiones: una gramática, no una lista de casos especiales

Que Rust sea un lenguaje orientado a expresiones —herencia directa de la familia ML y Haskell— no es un detalle de estilo: es una decisión de diseño que colapsa una docena de construcciones separadas en una sola idea. En C conviven el if sentencia y el ?: expresión, el switch sentencia y ninguna forma de que un bloque “valga” algo; cada una con su gramática y sus límites. Rust borra esa dualidad: si casi todo es una expresión que produce un valor con un tipo, entonces todo compone con todo bajo las mismas reglas. Un if puede vivir dentro de un match, dentro de un let, dentro de un argumento, y el verificador de tipos razona sobre el conjunto con una única ley: cada expresión tiene un tipo, y los contextos exigen tipos. La ausencia del return en la última línea de una función no es azúcar: es la consecuencia natural de que el cuerpo de la función es un bloque, el bloque es una expresión, y su valor es el valor devuelto. Interioriza esto y dejarás de ver “reglas” sueltas para ver una sola gramática recursiva. Ese es el andamiaje mental sobre el que se sostienen match, if let y el tipo never de las próximas lecciones.

⚔️ Piensa en expresiones
  1. Reescribe una cascada if/else if/else que asigne una categoría (&str) a un número, usando if como expresión y sin ningún return.
  2. Provoca a propósito el error de tipos incompatibles poniendo un entero en una rama y un carácter en la otra; lee el mensaje E0308 completo.
  3. Añade un ; al final de una rama de un if que asignas a una variable y explica, con la regla del punto y coma, por qué ahora falla.
  4. Convierte la función signo para que use match en lugar de if encadenado y comprueba que el tipo devuelto es idéntico.