Primeros pasos en Rust - Operadores

Apuntes de Rust - Parte 9

Lo sé, debí introducir los operadores con anterioridad. Sin embargo, como mencione en el primer post de Rust, estos son apuntes para personas con algo de experiencia en algún lenguaje de programación. Por lo que doy por hecho que entendemos el concepto de operador y como funcionan estos, ya que es algo que funciona de la misma forma en cualquier lenguaje de programación de propósito general.

Un operador es un símbolo que ejecuta operaciones con los valores o las variables de un programa. Por ejemplo, el signo menos (-) es un operador que hace una resta entre dos valores.

En programación Rust, hay varios operadores que se pueden agrupar en las siguientes categorías principales:

  • Operadores aritméticos

  • Operadores de asignación

  • Operadores de comparación

  • Operadores lógicos

Operadores Aritméticos

La siguiente tabla presenta los operadores aritméticos en Rust y un ejemplo de uso para cada uno:

OperadorOperaciónEjemplo de UsoResultado
+Sumalet suma = 5 + 3;8
-Restalet resta = 10 - 2;8
*Multiplicaciónlet multiplicacion = 4 * 6;24
/Divisiónlet division = 15 / 3;5
%Módulolet modulo = 7 % 3;1

Ejemplo:

fn main() {
    let a = 10;
    let b = 3;

    let suma = a + b;
    let resta = a - b;
    let multiplicacion = a * b;
    let division = a / b;
    let modulo = a % b;

    println!("Suma: {}", suma);
    println!("Resta: {}", resta);
    println!("Multiplicación: {}", multiplicacion);
    println!("División: {}", division);
    println!("Módulo: {}", modulo);
}

Operadores de Asignación

La siguiente tabla presenta los operadores de asignación en Rust y un ejemplo de uso para cada uno:

OperadorOperaciónEjemplo de UsoEquivalente Algebraico
\=Asignaciónlet x = 5;x = 5
+=Suma y asignaciónx += 3;x = x + 3
-=Resta y asignaciónx -= 2;x = x - 2
*=Multiplicación y asignaciónx *= 4;x = x * 4
/=División y asignaciónx /= 2;x = x / 2
%=Módulo y asignaciónx %= 7;x = x % 7
<<=Desplazamiento a la izquierda y asignaciónx <<= 1;x = x << 1
>>=Desplazamiento a la derecha y asignaciónx >>= 2;x = x >> 2
&=AND bit a bit y asignaciónx &= 0b1010;x = x & 0b1010
^=XOR bit a bit y asignaciónx ^= 0b1100;x = x ^ 0b1100
\=OR bit a bit y asignaciónx

Ejemplo:

fn main() {
    let mut x = 5;

    x += 3;   // Equivalente a: x = x + 3;
    x -= 2;   // Equivalente a: x = x - 2;
    x *= 4;   // Equivalente a: x = x * 4;
    x /= 2;   // Equivalente a: x = x / 2;
    x %= 7;   // Equivalente a: x = x % 7;

    println!("Valor final de x: {}", x);
}

Operadores de Comparación

La siguiente tabla presenta los operadores de comparación en Rust y un ejemplo de uso para cada uno:

OperadorOperaciónEjemplo de UsoDescripción
\==Igualdadif x == y { /* código */ }Comprueba si dos valores son iguales.
!=Desigualdadif x != y { /* código */ }Comprueba si dos valores son diferentes.
<Menor queif x < y { /* código */ }Comprueba si el valor de la izquierda es menor que el de la derecha.
>Mayor queif x > y { /* código */ }Comprueba si el valor de la izquierda es mayor que el de la derecha.
<=Menor o igual queif x <= y { /* código */ }Comprueba si el valor de la izquierda es menor o igual que el de la derecha.
>=Mayor o igual queif x >= y { /* código */ }Comprueba si el valor de la izquierda es mayor o igual que el de la derecha.

Ejemplo:

fn main() {
    let x = 5;
    let y = 10;

    if x == y {
        println!("x es igual a y");
    } else {
        println!("x no es igual a y");
    }

    if x != y {
        println!("x no es igual a y");
    } else {
        println!("x es igual a y");
    }

    if x < y {
        println!("x es menor que y");
    } else {
        println!("x no es menor que y");
    }

    if x > y {
        println!("x es mayor que y");
    } else {
        println!("x no es mayor que y");
    }

    if x <= y {
        println!("x es menor o igual que y");
    } else {
        println!("x no es menor o igual que y");
    }

    if x >= y {
        println!("x es mayor o igual que y");
    } else {
        println!("x no es mayor o igual que y");
    }
}

Operadores Lógicos

La siguiente tabla presenta los operadores lógicos en Rust y un ejemplo de uso para cada uno:

OperadorOperaciónEjemplo de UsoDescripción
&&AND lógicoif x > 0 && y < 10 { /* código */ }Verifica si ambas condiciones son verdaderas.
OR lógico
!NOT lógicoif !(x > 0) { /* código */ }Niega la condición, es decir, verifica si la condición es falsa.

Ejemplo:

fn main() {
    let a = true;
    let b = false;

    if a && b {
        println!("Ambas condiciones son verdaderas");
    } else {
        println!("Al menos una de las condiciones es falsa");
    }

    if a || b {
        println!("Al menos una de las condiciones es verdadera");
    } else {
        println!("Ambas condiciones son falsas");
    }

    if !a {
        println!("La condición 'a' es falsa");
    } else {
        println!("La condición 'a' es verdadera");
    }
}

Operadores Binarios

La siguiente tabla presenta los operadores binarios en Rust y un ejemplo de uso para cada uno:

OperadorOperaciónEjemplo de UsoResultado
&AND bit a bitlet resultado = 0b1101 & 0b1010;0b1000
OR bit a bitlet resultado = 0b1101
^XOR bit a bitlet resultado = 0b1101 ^ 0b1010;0b0111
<<Desplazamiento a la izquierdalet resultado = 0b1101 << 2;0b110100
>>Desplazamiento a la derechalet resultado = 0b1101 >> 1;0b0110

Que los operadores sean binarios, no implica que nuestros valores deban estar en binario, este sería el equivalente de la tabla anterior utilizando valores enteros en decimal.

OperadorOperaciónEjemplo de UsoResultado
&AND bit a bitlet resultado = 13 & 10;8
OR bit a bitlet resultado = 13
^XOR bit a bitlet resultado = 13 ^ 10;7
<<Desplazamiento a la izquierdalet resultado = 13 << 2;52
>>Desplazamiento a la derechalet resultado = 13 >> 1;6

Ejemplo:

fn main() {
    let a: u8 = 0b1010;
    let b: u8 = 0b1100;

    let result_and = a & b;  // AND binario
    let result_or = a | b;   // OR binario
    let result_xor = a ^ b;  // XOR binario
    let result_shift_left = a << 2;   // Desplazamiento a la izquierda
    let result_shift_right = a >> 3;  // Desplazamiento a la derecha
    let result_not = !a;     // NOT binario

    println!("AND: {:b}", result_and);
    println!("OR: {:b}", result_or);
    println!("XOR: {:b}", result_xor);
    println!("Desplazamiento a la izquierda: {:b}", result_shift_left);
    println!("Desplazamiento a la derecha: {:b}", result_shift_right);
    println!("NOT: {:b}", result_not);
}

En este punto es importe recordar que la notación 0b1010 representa un número binario. Se utiliza el prefijo 0b para indicar que el número a continuación está en notación binaria, y por ende solo puede estar representado por 0s y 1s. Para resaltar u8, indica que es un numero entero sin signo de 8 bits.

En Rust, el {:b} es un especificador de formato utilizado dentro de la macro println! para imprimir un valor en su representación binaria.

Jugando con Operadores Binarios

En esta sección vamos a jugar un poco más con los operadores binarios de Rust. Vamos a crear una variable que almacene una letra (un carácter) y a partir de este vamos a imprimir su valor en diferentes bases numéricas y vamos a empezar a alternar su valor utilizando operadores binarios. Para la primera parte del código tenemos:

fn main() {
    let letra = 'A';
    let valor_numerico = letra as u8;

    println!("Carácter original: {}", letra);
    println!("Valor decimal: {}", valor_numerico);
    println!("Valor hexadecimal: {:X}", valor_numerico);
    println!("Valor octal: {:o}", valor_numerico);
    println!("Valor binario: {:b}", valor_numerico);
}

Realicemos un ejercicio sencillo, súmenos 32 en decimal a la letra e imprimamos nuevamente los valores de la misma.

fn main() {
    let letra = 'A';
    let mut valor_numerico = letra as u8;

    println!("Carácter original: {}", letra);
    println!("Valor decimal: {}", valor_numerico);
    println!("Valor binario: {:b}", valor_numerico);

    valor_numerico += 32;
    let nueva_letra = valor_numerico as char;

    println!("\nCarácter modificado: {}", nueva_letra);
    println!("Valor decimal modificado: {}", valor_numerico);
    println!("Valor binario modificado: {:b}", valor_numerico);
}

En esta ocasión nos concentramos en la representación de la letra, tanto como carácter, como número decimal y como número binario. Al valor numérico de la letra 'A' le sumamos el número decimal 32.

Tabla ASCCII - Indices en hexa

Nota: La tabla ASCII presentada se lee de la siguiente manera. Columna, Fila. Que quiere decir esto, que la letra 'A' corresponde al valor numérico 41, pero en hexadecimal. Convirtiendo este valor a decimal correspondería al número 65.

Si revisamos la tabla ASCII o la tabla utf-8 vemos que el valor numérico de la letra 'A' corresponde al número 65 y el valor numérico de la letra 'a' corresponde al número 97, los separa una distancia de 32 (o 20 en hexadecimal).

El alfabeto en mayúscula está separado a una distancia de 32 al alfabeto en minúscula. Ejemplo, si al número 98 le restamos 32 y lo imprimimos como carácter, obtenemos la letra 'B'.

fn main() {
    let letra = 'z';
    let mut valor_numerico = letra as u8;

    println!("Carácter original: {}", letra);
    println!("Valor decimal: {}", valor_numerico);
    println!("Valor binario: {:b}", valor_numerico);

    valor_numerico -= 32;
    let nueva_letra = valor_numerico as char;

    println!("\nCarácter modificado: {}", nueva_letra);
    println!("Valor decimal modificado: {}", valor_numerico);
    println!("Valor binario modificado: {:b}", valor_numerico);
}

Este ejemplo, solo cambio el valor de letra por la 'z' minúscula y decidimos restar 32. Lo que nos dio como resultado 'Z' mayúscula.

Podríamos agregar operaciones de corrimiento de bits al ejemplo, pero nos daríamos cuenta de que en muchos casos obtendremos valor de la tabla ASCII que no se pueden imprimir en pantalla, aunque sí podemos validar su valor numérico.

Ahora analicemos el siguiente bloque de código:

fn main() {
    let numero: u8 = 204;
    let mut resultado:u8;

    println!("decimal: {}", numero);
    println!("binario: {:b}", numero);

    resultado = numero << 2;
    println!("\n204 << 2 => {}",resultado);
    println!("204 << 2 => {:b}",resultado);

    resultado = numero >> 2;
    println!("\n204 >> 2 => {}",resultado);
    println!("204 << 2 => {:b}",resultado);
}

Tenemos el número 204 en decimal, y declaramos una variable resultado de mutable que luego almacenara el resultado de unas operaciones. Tanto el número, como el resultado de las operaciones, se imprimen en decimal y en binario.

Primero revisemos la siguiente operación. Esta corresponde a un corrimiento de dos bits a la derecha, eso qué significa?

resultado = numero << 2;

Revisemos el siguiente gráfico.

En la representación binaria del número, imaginemos los recuadros azules como la posición del bit que conforma el número 204. Al desplazar el número 2 bits a la izquierda es como si virtualmente moviéramos cada bit unas casillas en el gráfico, el bit en la posición 0 ahora está en la posición 2, el bit en la posición 1 ahora está en la posición 3 y así sucesivamente.

Al llegar a los bits en la posición 6 y 7, estos se desplazan a las posiciones 8 y 9. Pero como nuestra variable fue declarada específicamente como un número sin signo de ocho bits (u8), estas posiciones no existen, por ende estos 2 bits simplemente se pierden.

Ahora, en las posiciones 0 y 1, ya no tenemos bits porque los desplazamos a la izquierda. Por ende, la máquina rellena con 0s.

Un caso similar ocurre con el corrimiento a la derecha. Como lo ilustra el gráfico.

resultado = numero >> 2;

Para este escenario, los bits 0 y 1 (los menos significativos) se pierden. Y los bits 6 y 7 la máquina los rellena con 0s.

Pregunta: Si el tipo de dato fuera u16 en lugar de u8, ¿cuál sería el resultado para este mismo corrimiento de bits a la derecha y a la izquierda? ¿Cambiaria la respuesta?

Referencias

  • The Rust Programming Language. 2nd Edition by Steve Klabnik and Carol Nichols, with contributions from the Rust Community. 2023

Did you find this article valuable?

Support Jesse Padilla by becoming a sponsor. Any amount is appreciated!