Rust

Rust

Tutorial Rust: Operadores

Aprende los operadores aritméticos, lógicos y su precedencia en Rust para escribir código claro y eficiente. Ejemplos y buenas prácticas incluidas.

Aprende Rust y certifícate

Operadores aritméticos y de asignación

Los operadores son símbolos especiales que realizan operaciones sobre uno o más valores. En Rust, como en muchos lenguajes de programación, los operadores aritméticos y de asignación son fundamentales para manipular datos y realizar cálculos.

Operadores aritméticos

Rust proporciona los operadores aritméticos estándar para realizar operaciones matemáticas básicas. Estos operadores trabajan con tipos numéricos como enteros y flotantes.

  • Suma (+): Suma dos valores.
  • Resta (-): Resta el segundo valor del primero.
  • Multiplicación (*): Multiplica dos valores.
  • División (/): Divide el primer valor por el segundo.
  • Resto (%): Devuelve el resto de la división del primer valor por el segundo.

Veamos ejemplos de cada uno:

fn main() {
    // Declaramos algunas variables para nuestros ejemplos
    let a = 10;
    let b = 3;
    
    // Suma
    let suma = a + b;
    println!("Suma: {} + {} = {}", a, b, suma);
    
    // Resta
    let resta = a - b;
    println!("Resta: {} - {} = {}", a, b, resta);
    
    // Multiplicación
    let multiplicacion = a * b;
    println!("Multiplicación: {} * {} = {}", a, b, multiplicacion);
    
    // División
    let division = a / b;
    println!("División: {} / {} = {}", a, b, division);
    
    // Resto (módulo)
    let resto = a % b;
    println!("Resto: {} % {} = {}", a, b, resto);
}

La salida de este programa sería:

Suma: 10 + 3 = 13
Resta: 10 - 3 = 7
Multiplicación: 10 * 3 = 30
División: 10 / 3 = 3
Resto: 10 % 3 = 1

Particularidades de la división en Rust

Es importante destacar que la división en Rust se comporta de manera diferente dependiendo de los tipos de datos:

  • Con enteros: La división realiza un truncamiento hacia cero (división entera), descartando cualquier parte decimal.
  • Con flotantes: La división produce un resultado con decimales.

Veamos la diferencia:

fn main() {
    // División con enteros
    let a = 10;
    let b = 3;
    let division_entera = a / b;
    println!("División entera: {} / {} = {}", a, b, division_entera);
    
    // División con flotantes
    let c = 10.0;
    let d = 3.0;
    let division_flotante = c / d;
    println!("División flotante: {} / {} = {}", c, d, division_flotante);
}

La salida sería:

División entera: 10 / 3 = 3
División flotante: 10 / 3 = 3.3333333333333335

Operador de negación unario

Además de los operadores binarios (que operan sobre dos valores), Rust también proporciona el operador de negación unario (-), que cambia el signo de un valor:

fn main() {
    let positivo = 42;
    let negativo = -positivo;
    println!("Valor original: {}", positivo);
    println!("Valor negado: {}", negativo);
}

Salida:

Valor original: 42
Valor negado: -42

Operadores de asignación

Los operadores de asignación se utilizan para asignar valores a variables. El más básico es el operador de asignación simple (=), pero Rust también ofrece operadores de asignación compuestos que combinan una operación aritmética con la asignación.

  • Asignación simple (=): Asigna el valor de la derecha a la variable de la izquierda.
  • Asignación con suma (+=): Suma el valor de la derecha a la variable de la izquierda.
  • Asignación con resta (-=): Resta el valor de la derecha de la variable de la izquierda.
  • Asignación con multiplicación (*=): Multiplica la variable de la izquierda por el valor de la derecha.
  • Asignación con división (/=): Divide la variable de la izquierda por el valor de la derecha.
  • Asignación con resto (%=): Asigna a la variable de la izquierda el resto de dividirla por el valor de la derecha.

Veamos ejemplos de cada uno:

fn main() {
    // Asignación simple
    let mut x = 5;
    println!("Valor inicial: {}", x);
    
    // Asignación con suma
    x += 3;  // Equivalente a: x = x + 3
    println!("Después de += 3: {}", x);
    
    // Asignación con resta
    x -= 2;  // Equivalente a: x = x - 2
    println!("Después de -= 2: {}", x);
    
    // Asignación con multiplicación
    x *= 4;  // Equivalente a: x = x * 4
    println!("Después de *= 4: {}", x);
    
    // Asignación con división
    x /= 2;  // Equivalente a: x = x / 2
    println!("Después de /= 2: {}", x);
    
    // Asignación con resto
    x %= 3;  // Equivalente a: x = x % 3
    println!("Después de %= 3: {}", x);
}

La salida sería:

Valor inicial: 5
Después de += 3: 8
Después de -= 2: 6
Después de *= 4: 24
Después de /= 2: 12
Después de %= 3: 0

Mutabilidad y asignación

Es importante recordar que en Rust, las variables son inmutables por defecto. Para poder usar operadores de asignación compuestos, necesitamos declarar la variable como mutable usando la palabra clave mut:

fn main() {
    // Variable inmutable (por defecto)
    let a = 5;
    
    // Esto causaría un error de compilación:
    // a += 1;  // error: cannot assign twice to immutable variable
    
    // Variable mutable
    let mut b = 5;
    b += 1;  // Esto es válido
    println!("b después de incrementar: {}", b);
}

Operaciones con diferentes tipos numéricos

Rust es un lenguaje con tipado estático y fuerte, lo que significa que no realiza conversiones implícitas entre tipos numéricos diferentes. Si intentamos operar con tipos diferentes, obtendremos un error de compilación:

fn main() {
    let entero = 5;
    let flotante = 2.5;
    
    // Esto causaría un error:
    // let resultado = entero + flotante;
    
    // Solución: convertir explícitamente
    let resultado = entero as f64 + flotante;
    println!("Resultado: {}", resultado);
}

Para realizar operaciones entre tipos diferentes, debemos hacer conversiones explícitas usando el operador as o métodos de conversión como into() o from().

Desbordamiento en operaciones aritméticas

En Rust, las operaciones aritméticas con tipos enteros tienen comportamientos específicos en caso de desbordamiento (overflow):

  • En modo debug (compilación normal), Rust realiza comprobaciones de desbordamiento y el programa entrará en pánico (panic) si ocurre uno.
  • En modo release (con optimizaciones), Rust realiza "wrapping" (envolvimiento), donde el valor "da la vuelta" al rango del tipo.
fn main() {
    // u8 puede almacenar valores de 0 a 255
    let mut valor: u8 = 250;
    println!("Valor inicial: {}", valor);
    
    // Sumamos 10, lo que causaría un desbordamiento
    // En modo debug, esto causaría un panic
    // En modo release, haría wrapping a 4 (250 + 10 = 260, 260 - 256 = 4)
    valor = valor.wrapping_add(10);
    println!("Después de wrapping_add(10): {}", valor);
}

Rust proporciona métodos específicos para controlar el comportamiento en caso de desbordamiento:

  • wrapping_*: Realiza wrapping (envolvimiento).
  • checked_*: Devuelve None si hay desbordamiento.
  • saturating_*: Se queda en el valor máximo o mínimo del tipo.
  • overflowing_*: Devuelve el resultado con wrapping y un booleano indicando si hubo desbordamiento.
fn main() {
    let a: u8 = 250;
    
    // Diferentes comportamientos ante el desbordamiento
    let wrap = a.wrapping_add(10);
    let check = a.checked_add(10);
    let sat = a.saturating_add(10);
    let over = a.overflowing_add(10);
    
    println!("wrapping_add: {}", wrap);
    println!("checked_add: {:?}", check);
    println!("saturating_add: {}", sat);
    println!("overflowing_add: {:?}", over);
}

Salida:

wrapping_add: 4
checked_add: None
saturating_add: 255
overflowing_add: (4, true)

Estos métodos te dan un control preciso sobre cómo manejar situaciones de desbordamiento, lo que es especialmente útil en aplicaciones donde la seguridad y la precisión son críticas.

Operadores de comparación y lógicos

Los operadores de comparación y lógicos son fundamentales en Rust para evaluar condiciones y tomar decisiones en nuestro código. Estos operadores nos permiten comparar valores y combinar expresiones booleanas para crear lógica más compleja.

Operadores de comparación

Los operadores de comparación evalúan la relación entre dos valores y devuelven un resultado booleano (true o false). Rust proporciona los siguientes operadores de comparación:

  • Igual a (==): Comprueba si dos valores son iguales.
  • Distinto de (!=): Comprueba si dos valores son diferentes.
  • Mayor que (>): Comprueba si el primer valor es mayor que el segundo.
  • Menor que (<): Comprueba si el primer valor es menor que el segundo.
  • Mayor o igual que (>=): Comprueba si el primer valor es mayor o igual que el segundo.
  • Menor o igual que (<=): Comprueba si el primer valor es menor o igual que el segundo.

Veamos ejemplos de cada uno:

fn main() {
    let a = 5;
    let b = 10;
    
    // Igual a
    let igual = a == b;
    println!("{} == {}: {}", a, b, igual);
    
    // Distinto de
    let distinto = a != b;
    println!("{} != {}: {}", a, b, distinto);
    
    // Mayor que
    let mayor = a > b;
    println!("{} > {}: {}", a, b, mayor);
    
    // Menor que
    let menor = a < b;
    println!("{} < {}: {}", a, b, menor);
    
    // Mayor o igual que
    let mayor_igual = a >= b;
    println!("{} >= {}: {}", a, b, mayor_igual);
    
    // Menor o igual que
    let menor_igual = a <= b;
    println!("{} <= {}: {}", a, b, menor_igual);
}

La salida de este programa sería:

5 == 10: false
5 != 10: true
5 > 10: false
5 < 10: true
5 >= 10: false
5 <= 10: true

Comparación de tipos diferentes

En Rust, solo puedes comparar valores del mismo tipo. Si intentas comparar valores de tipos diferentes, el compilador mostrará un error:

fn main() {
    let entero = 5;
    let flotante = 5.0;
    
    // Esto causaría un error de compilación:
    // let comparacion = entero == flotante;
    
    // Solución: convertir explícitamente
    let comparacion = entero as f64 == flotante;
    println!("¿Son iguales? {}", comparacion);
}

Comparación de tipos compuestos

Para tipos primitivos como enteros, flotantes, booleanos y caracteres, las comparaciones funcionan de manera intuitiva. Sin embargo, es importante mencionar que Rust también permite comparar tuplas siempre que sus elementos sean comparables:

fn main() {
    let tupla1 = (1, 2, 3);
    let tupla2 = (1, 2, 3);
    let tupla3 = (1, 2, 4);
    
    println!("tupla1 == tupla2: {}", tupla1 == tupla2);
    println!("tupla1 == tupla3: {}", tupla1 == tupla3);
    println!("tupla1 < tupla3: {}", tupla1 < tupla3);
}

Salida:

tupla1 == tupla2: true
tupla1 == tupla3: false
tupla1 < tupla3: true

Las tuplas se comparan elemento por elemento de izquierda a derecha, hasta encontrar una diferencia.

Operadores lógicos

Los operadores lógicos permiten combinar expresiones booleanas para crear condiciones más complejas. Rust proporciona tres operadores lógicos principales:

  • AND lógico (&&): Devuelve true solo si ambas expresiones son verdaderas.
  • OR lógico (||): Devuelve true si al menos una de las expresiones es verdadera.
  • NOT lógico (!): Invierte el valor de una expresión booleana.

Veamos ejemplos de cada uno:

fn main() {
    let x = 5;
    let y = 10;
    
    // AND lógico
    let ambas_condiciones = x < 10 && y > 5;
    println!("x < 10 && y > 5: {}", ambas_condiciones);
    
    // OR lógico
    let alguna_condicion = x > 10 || y > 5;
    println!("x > 10 || y > 5: {}", alguna_condicion);
    
    // NOT lógico
    let negar = !(x == 5);
    println!("!(x == 5): {}", negar);
}

La salida sería:

x < 10 && y > 5: true
x > 10 || y > 5: true
!(x == 5): false

Evaluación de cortocircuito

Rust implementa la evaluación de cortocircuito para los operadores lógicos && y ||. Esto significa que:

  • Para &&: Si la primera expresión es false, la segunda no se evalúa, ya que el resultado final será false independientemente.
  • Para ||: Si la primera expresión es true, la segunda no se evalúa, ya que el resultado final será true independientemente.

Este comportamiento es útil para evitar operaciones innecesarias o potencialmente problemáticas:

fn main() {
    let a = 5;
    let b = 0;
    
    // Evita la división por cero gracias al cortocircuito
    let resultado = b != 0 && a / b > 2;
    println!("Resultado: {}", resultado);
    
    // La segunda condición solo se evalúa si la primera es falsa
    let otra_condicion = a < 3 || a > 4;
    println!("Otra condición: {}", otra_condicion);
}

En el primer ejemplo, como b != 0 es false, la expresión a / b > 2 nunca se evalúa, evitando así un error en tiempo de ejecución por división por cero.

Combinación de operadores lógicos

Puedes combinar múltiples operadores lógicos para crear expresiones más complejas:

fn main() {
    let edad = 25;
    let tiene_licencia = true;
    let tiene_seguro = false;
    
    // Combinación de operadores lógicos
    let puede_conducir = edad >= 18 && tiene_licencia && tiene_seguro;
    println!("¿Puede conducir? {}", puede_conducir);
    
    // Uso de paréntesis para claridad
    let situacion_especial = (edad < 21 || edad > 65) && tiene_licencia;
    println!("¿Situación especial? {}", situacion_especial);
}

Salida:

¿Puede conducir? false
¿Situación especial? false

Operadores bit a bit

Además de los operadores lógicos estándar, Rust proporciona operadores bit a bit que trabajan a nivel de bits individuales:

  • AND bit a bit (&): Realiza AND en cada par de bits.
  • OR bit a bit (|): Realiza OR en cada par de bits.
  • XOR bit a bit (^): Realiza XOR (OR exclusivo) en cada par de bits.
  • NOT bit a bit (!): Invierte todos los bits.
  • Desplazamiento a la izquierda (<<): Desplaza los bits hacia la izquierda.
  • Desplazamiento a la derecha (>>): Desplaza los bits hacia la derecha.

Estos operadores son útiles para manipulaciones de bajo nivel y operaciones con banderas:

fn main() {
    let a = 0b1010; // 10 en binario
    let b = 0b1100; // 12 en binario
    
    // AND bit a bit
    let and_result = a & b;
    println!("a & b = {:b} ({})", and_result, and_result);
    
    // OR bit a bit
    let or_result = a | b;
    println!("a | b = {:b} ({})", or_result, or_result);
    
    // XOR bit a bit
    let xor_result = a ^ b;
    println!("a ^ b = {:b} ({})", xor_result, xor_result);
    
    // NOT bit a bit (para u8)
    let not_result = !a as u8;
    println!("!a = {:b} ({})", not_result, not_result);
    
    // Desplazamiento a la izquierda
    let shift_left = a << 1;
    println!("a << 1 = {:b} ({})", shift_left, shift_left);
    
    // Desplazamiento a la derecha
    let shift_right = a >> 1;
    println!("a >> 1 = {:b} ({})", shift_right, shift_right);
}

Salida:

a & b = 1000 (8)
a | b = 1110 (14)
a ^ b = 110 (6)
!a = 11110101 (245)
a << 1 = 10100 (20)
a >> 1 = 101 (5)

Operadores de asignación bit a bit

Al igual que con los operadores aritméticos, Rust proporciona operadores de asignación compuestos para operaciones bit a bit:

  • &=: AND bit a bit y asignación
  • |=: OR bit a bit y asignación
  • ^=: XOR bit a bit y asignación
  • <<=: Desplazamiento a la izquierda y asignación
  • >>=: Desplazamiento a la derecha y asignación
fn main() {
    let mut valor = 0b1010; // 10 en binario
    println!("Valor inicial: {:b}", valor);
    
    // AND bit a bit y asignación
    valor &= 0b1100;
    println!("Después de &= 0b1100: {:b}", valor);
    
    // OR bit a bit y asignación
    valor |= 0b0010;
    println!("Después de |= 0b0010: {:b}", valor);
    
    // Desplazamiento a la izquierda y asignación
    valor <<= 1;
    println!("Después de <<= 1: {:b}", valor);
}

Salida:

Valor inicial: 1010
Después de &= 0b1100: 1000
Después de |= 0b0010: 1010
Después de <<= 1: 10100

Casos prácticos

Los operadores de comparación y lógicos son esenciales para crear expresiones condicionales que controlan el flujo de ejecución. Aunque aún no hemos visto estructuras de control, estos operadores serán fundamentales cuando las utilicemos.

Validación de rangos

Un uso común es verificar si un valor está dentro de un rango específico:

fn main() {
    let temperatura = 22;
    
    // Verificar si la temperatura está en un rango confortable
    let es_confortable = temperatura >= 18 && temperatura <= 25;
    println!("¿La temperatura de {}°C es confortable? {}", temperatura, es_confortable);
    
    // Verificar si la temperatura está fuera de un rango seguro
    let es_extrema = temperatura < 0 || temperatura > 40;
    println!("¿La temperatura de {}°C es extrema? {}", temperatura, es_extrema);
}

Uso de banderas con operadores bit a bit

Los operadores bit a bit son útiles para trabajar con banderas (flags), donde cada bit representa una opción o estado:

fn main() {
    // Definimos constantes para nuestras banderas
    const LEER: u8 = 0b0001;    // 1
    const ESCRIBIR: u8 = 0b0010; // 2
    const EJECUTAR: u8 = 0b0100; // 4
    const ADMIN: u8 = 0b1000;    // 8
    
    // Creamos un conjunto de permisos
    let mut permisos = LEER | ESCRIBIR; // 0b0011
    println!("Permisos iniciales: {:04b}", permisos);
    
    // Verificamos si tiene permiso de lectura
    let puede_leer = permisos & LEER != 0;
    println!("¿Puede leer? {}", puede_leer);
    
    // Añadimos permiso de ejecución
    permisos |= EJECUTAR;
    println!("Permisos después de añadir ejecución: {:04b}", permisos);
    
    // Quitamos permiso de escritura
    permisos &= !ESCRIBIR;
    println!("Permisos después de quitar escritura: {:04b}", permisos);
    
    // Alternamos (toggle) permiso de administrador
    permisos ^= ADMIN;
    println!("Permisos después de alternar admin: {:04b}", permisos);
}

Este patrón es muy común en programación de sistemas y APIs de bajo nivel, donde la eficiencia en memoria es crucial.

Precedencia y agrupación

Cuando trabajamos con expresiones que contienen múltiples operadores, es fundamental entender el orden en que Rust evalúa estas operaciones. La precedencia determina qué operadores se ejecutan primero, mientras que la agrupación nos permite modificar este orden predeterminado.

Precedencia de operadores

En Rust, cada operador tiene un nivel de precedencia específico que determina el orden de evaluación cuando aparecen varios operadores en una misma expresión. Los operadores con mayor precedencia se ejecutan antes que los de menor precedencia.

Aquí está la tabla de precedencia de operadores en Rust, ordenada de mayor a menor precedencia:

  • Nivel 1 (mayor precedencia):

  • Acceso a campo (.)

  • Llamada a función (())

  • Indexación ([])

  • Nivel 2:

  • Negación unaria (-), NOT bit a bit (!), desreferencia (*), referencia (&, &mut)

  • Nivel 3:

  • Multiplicación (*), división (/), resto (%)

  • Nivel 4:

  • Suma (+), resta (-)

  • Nivel 5:

  • Desplazamiento bit a bit (<<, >>)

  • Nivel 6:

  • AND bit a bit (&)

  • Nivel 7:

  • XOR bit a bit (^)

  • Nivel 8:

  • OR bit a bit (|)

  • Nivel 9:

  • Comparaciones (==, !=, <, >, <=, >=)

  • Nivel 10:

  • AND lógico (&&)

  • Nivel 11:

  • OR lógico (||)

  • Nivel 12:

  • Asignación (=, +=, -=, *=, etc.)

Veamos un ejemplo que ilustra cómo la precedencia afecta el resultado de una expresión:

fn main() {
    // Sin considerar la precedencia, podríamos pensar que esto se evalúa de izquierda a derecha
    let resultado = 2 + 3 * 4;
    
    // Pero debido a la precedencia, la multiplicación se realiza primero
    // Es equivalente a: 2 + (3 * 4)
    println!("2 + 3 * 4 = {}", resultado);
    
    // Otro ejemplo con múltiples operadores
    let expresion = 10 - 2 * 3 + 4 / 2;
    // Se evalúa como: 10 - (2 * 3) + (4 / 2)
    // = 10 - 6 + 2
    // = 4 + 2
    // = 6
    println!("10 - 2 * 3 + 4 / 2 = {}", expresion);
}

La salida sería:

2 + 3 * 4 = 14
10 - 2 * 3 + 4 / 2 = 6

Agrupación con paréntesis

Cuando queremos modificar el orden de evaluación predeterminado por la precedencia, podemos usar paréntesis para agrupar expresiones. Las operaciones dentro de paréntesis siempre se evalúan primero, independientemente de la precedencia de los operadores involucrados.

fn main() {
    // Sin paréntesis (usando la precedencia predeterminada)
    let sin_parentesis = 2 + 3 * 4;
    println!("2 + 3 * 4 = {}", sin_parentesis);
    
    // Con paréntesis para cambiar el orden de evaluación
    let con_parentesis = (2 + 3) * 4;
    println!("(2 + 3) * 4 = {}", con_parentesis);
    
    // Ejemplo más complejo
    let expresion1 = 10 - 2 * 3 + 4 / 2;
    let expresion2 = (10 - 2) * 3 + 4 / 2;
    let expresion3 = 10 - (2 * 3 + 4) / 2;
    
    println!("10 - 2 * 3 + 4 / 2 = {}", expresion1);
    println!("(10 - 2) * 3 + 4 / 2 = {}", expresion2);
    println!("10 - (2 * 3 + 4) / 2 = {}", expresion3);
}

La salida sería:

2 + 3 * 4 = 14
(2 + 3) * 4 = 20
10 - 2 * 3 + 4 / 2 = 6
(10 - 2) * 3 + 4 / 2 = 26
10 - (2 * 3 + 4) / 2 = 5

Precedencia en operadores lógicos

La precedencia también es importante cuando combinamos operadores lógicos. En Rust, && tiene mayor precedencia que ||, lo que significa que las expresiones con && se evalúan antes que las expresiones con ||.

fn main() {
    let a = true;
    let b = false;
    let c = true;
    
    // Debido a la precedencia, esto se evalúa como: (a && b) || c
    let resultado1 = a && b || c;
    println!("a && b || c = {}", resultado1);
    
    // Usando paréntesis para cambiar el orden
    let resultado2 = a && (b || c);
    println!("a && (b || c) = {}", resultado2);
}

La salida sería:

a && b || c = true
a && (b || c) = true

En este caso particular, ambas expresiones dan el mismo resultado, pero en situaciones más complejas, la diferencia puede ser significativa.

Precedencia con operadores bit a bit

Los operadores bit a bit también siguen reglas de precedencia específicas. El operador & tiene mayor precedencia que ^, que a su vez tiene mayor precedencia que |.

fn main() {
    let a = 0b1010; // 10 en binario
    let b = 0b1100; // 12 en binario
    let c = 0b0101; // 5 en binario
    
    // Debido a la precedencia, esto se evalúa como: (a & b) | c
    let resultado1 = a & b | c;
    println!("a & b | c = {:04b} ({})", resultado1, resultado1);
    
    // Con paréntesis para cambiar el orden
    let resultado2 = a & (b | c);
    println!("a & (b | c) = {:04b} ({})", resultado2, resultado2);
}

La salida sería:

a & b | c = 1101 (13)
a & (b | c) = 1000 (8)

Combinación de diferentes tipos de operadores

Cuando combinamos diferentes tipos de operadores (aritméticos, lógicos, de comparación), la precedencia se vuelve aún más importante:

fn main() {
    let a = 5;
    let b = 10;
    let c = 15;
    
    // Esta expresión combina operadores aritméticos y de comparación
    // Debido a la precedencia, las operaciones aritméticas se realizan primero
    let resultado = a + b * 2 > c + 5;
    
    // Se evalúa como: (a + (b * 2)) > (c + 5)
    // = (5 + 20) > (15 + 5)
    // = 25 > 20
    // = true
    println!("a + b * 2 > c + 5 = {}", resultado);
    
    // Usando paréntesis para cambiar el orden
    let resultado_con_parentesis = (a + b) * 2 > c + 5;
    // = (5 + 10) * 2 > 15 + 5
    // = 30 > 20
    // = true
    println!("(a + b) * 2 > c + 5 = {}", resultado_con_parentesis);
}

Asociatividad de operadores

Además de la precedencia, los operadores en Rust también tienen una propiedad llamada asociatividad, que determina el orden de evaluación cuando hay múltiples operadores con la misma precedencia.

La mayoría de los operadores en Rust son asociativos por la izquierda, lo que significa que se evalúan de izquierda a derecha. Por ejemplo, a - b - c se evalúa como (a - b) - c.

Sin embargo, los operadores de asignación son asociativos por la derecha, lo que significa que se evalúan de derecha a izquierda. Por ejemplo, a = b = c se evalúa como a = (b = c).

fn main() {
    // Asociatividad por la izquierda para la resta
    let resultado1 = 10 - 5 - 2;
    // Se evalúa como: (10 - 5) - 2 = 5 - 2 = 3
    println!("10 - 5 - 2 = {}", resultado1);
    
    // Asociatividad por la derecha para la asignación
    let mut a = 0;
    let mut b = 0;
    let c = 5;
    
    // Esto se evalúa como: a = (b = c)
    a = b = c;
    println!("Después de a = b = c: a = {}, b = {}", a, b);
}

La salida sería:

10 - 5 - 2 = 3
Después de a = b = c: a = 5, b = 5

Recomendaciones para el uso de paréntesis

Aunque la precedencia de operadores en Rust sigue reglas bien definidas, es una buena práctica usar paréntesis para hacer que el código sea más legible y evitar errores sutiles:

  • Claridad: Incluso cuando los paréntesis no son estrictamente necesarios, pueden hacer que el código sea más fácil de entender.
  • Intención: Los paréntesis comunican claramente tu intención a otros programadores (y a ti mismo en el futuro).
  • Prevención de errores: Usar paréntesis puede prevenir errores cuando se modifica el código más adelante.
fn main() {
    let a = 5;
    let b = 3;
    let c = 2;
    
    // Sin paréntesis - correcto pero potencialmente confuso
    let resultado1 = a + b * c;
    
    // Con paréntesis - más claro y explícito
    let resultado2 = a + (b * c);
    
    println!("Ambos resultados son iguales: {} = {}", resultado1, resultado2);
    
    // Expresión compleja sin paréntesis
    let compleja1 = a * b + c * a - b / c;
    
    // La misma expresión con paréntesis para claridad
    let compleja2 = (a * b) + (c * a) - (b / c);
    
    println!("Expresión compleja: {} = {}", compleja1, compleja2);
}

Evaluación de expresiones paso a paso

Para entender mejor cómo funciona la precedencia, es útil descomponer una expresión compleja y evaluarla paso a paso:

fn main() {
    let expresion = 2 * 3 + 4 * 5 % 3 - 1;
    
    // Paso 1: Evaluar operaciones de nivel 3 (*, /, %)
    // 2 * 3 = 6
    // 4 * 5 = 20
    // 20 % 3 = 2
    
    // Paso 2: Evaluar operaciones de nivel 4 (+, -)
    // 6 + 2 = 8
    // 8 - 1 = 7
    
    println!("2 * 3 + 4 * 5 % 3 - 1 = {}", expresion);
    
    // Verificamos con paréntesis explícitos
    let con_parentesis = ((2 * 3) + ((4 * 5) % 3)) - 1;
    println!("Con paréntesis explícitos: {}", con_parentesis);
}

La salida sería:

2 * 3 + 4 * 5 % 3 - 1 = 7
Con paréntesis explícitos: 7

Precedencia en expresiones con tipos mixtos

Cuando trabajamos con expresiones que involucran diferentes tipos de datos, debemos ser especialmente cuidadosos con la precedencia y las conversiones:

fn main() {
    let entero = 10;
    let flotante = 2.5;
    
    // Esto causaría un error de compilación debido a tipos incompatibles
    // let resultado = entero + flotante * 2;
    
    // Solución 1: Convertir explícitamente antes de la operación
    let resultado1 = entero as f64 + flotante * 2.0;
    println!("entero as f64 + flotante * 2.0 = {}", resultado1);
    
    // Solución 2: Usar paréntesis y convertir el resultado de la operación
    let resultado2 = entero + (flotante * 2.0) as i32;
    println!("entero + (flotante * 2.0) as i32 = {}", resultado2);
}

La salida sería:

entero as f64 + flotante * 2.0 = 15
entero + (flotante * 2.0) as i32 = 15

En este ejemplo, la conversión de tipos interactúa con la precedencia de operadores. Es importante entender que las conversiones explícitas (as) tienen una precedencia alta, por lo que a menudo necesitamos paréntesis para aplicar la conversión al resultado de una operación completa.

Aprende Rust online

Otras lecciones de Rust

Accede a todas las lecciones de Rust y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Accede GRATIS a Rust y certifícate

Ejercicios de programación de Rust

Evalúa tus conocimientos de esta lección Operadores con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender y utilizar operadores aritméticos y de asignación en Rust.
  • Conocer los operadores de comparación y lógicos para evaluar condiciones.
  • Aprender el uso de operadores bit a bit y sus aplicaciones prácticas.
  • Entender la precedencia y asociatividad de operadores para controlar el orden de evaluación.
  • Aplicar buenas prácticas con paréntesis para mejorar la legibilidad y evitar errores.