Manipulación de bits y desplazamientos en Dart
Los operadores de bits realizan cálculos directamente sobre la representación binaria de los enteros (int). En lugar de operar con el valor decimal que vemos, la CPU manipula cada bit de forma independiente. Esto es fundamental porque, a nivel de hardware, estas operaciones son extremadamente rápidas y permiten empaquetar múltiples estados booleanos (flags) en un solo entero, optimizando drásticamente el uso de memoria y el ancho de banda en la serialización de datos.
Existen seis operadores principales: & (AND), | (OR), ^ (XOR), ~ (NOT/complemento), << (left shift) y >> (right shift). A partir de Dart 2.14 [disponible desde Dart 2.14], se introdujo >>> (unsigned right shift), que permite tratar al entero como un conjunto de bits sin considerar su signo.
Cuando trabajas con protocolos de red, procesamiento de archivos binarios o sistemas embebidos, usarás estas herramientas para aplicar “máscaras” que aíslen bits específicos. Sin embargo, debes tener cuidado con el signo: en Dart, los enteros usan complemento a dos para representar números negativos, lo que significa que el operador de negación bit a bit ~ no solo invierte los bits, sino que transforma el valor de forma que ~x equivale a -(x + 1). Si intentas manipular bits asumiendo que el signo no afecta el desplazamiento, podrías terminar con valores inesperados debido a la extensión de signo (donde el bit de signo se duplica al desplazar a la derecha).
void main() {
// Definición de flags de estado usando literales binarios (0b)
const statusReady = 0b0001; // 1
const statusError = 0b0010; // 2
const statusBusy = 0b0100; // 4
const statusWarning = 0b1000; // 8
int deviceStatus = 0; // Estado inicial: todo apagado (0000)
// 1. Set (Activar bits): Usamos OR (|) para encender flags
deviceStatus |= statusReady;
deviceStatus |= statusBusy;
print('Estado (Ready | Busy): 0b${deviceStatus.toRadixString(2)}'); // 0b101
// 2. Check (Verificar bits): Usamos AND (&) para aislar un bit
final isError = (deviceStatus & statusError) != 0;
final isReady = (deviceStatus & statusReady) != 0;
print('¿Hay error?: $isError, ¿Está listo?: $isReady');
// 3. Toggle (Alternar bits): Usamos XOR (^) para invertir un estado
deviceStatus ^= statusReady; // Si estaba activo, se apaga; si estaba apagado, se enciende
print('Estado tras toggle Ready: 0b${deviceStatus.toRadixString(2)}');
// 4. Desplazamientos (Shifts)
// Left shift (<<): Desplaza bits a la izquierda (multiplica por 2 por cada posición)
int shiftedLeft = statusReady << 2; // 0b0001 -> 0b0100 (4)
print('StatusReady << 2: $shiftedLeft');
// Diferencia crítica en números negativos
int negativeNum = -2; // Representación (2's complement): ...11111110
// Right shift (>>): Desplazamiento aritmético (preserva el bit de signo)
print('Negativo (-2) >> 1: ${negativeNum >> 1}');
// Resultado: -1 (...11111111)
// Unsigned right shift (>>>): Desplazamiento lógico (rellena con ceros)
print('Negativo (-2) >>> 1: ${negativeNum >>> 1}');
// Resultado: Un número positivo muy grande (0111...1111)
}
En el ejemplo anterior, fíjate en cómo deviceStatus |= statusReady utiliza el operador | para realizar una operación de unión de bits. Al aplicar deviceStatus & statusError, estamos creando una máscara que descarta todos los bits excepto el que representa el error; si el resultado es distinto de cero, sabemos que ese bit estaba activo.
Cuando realizamos el toggle con deviceStatus ^= statusReady, el operador XOR compara los bits: si son iguales, el resultado es 0, si son diferentes, es 1. Esto es ideal para alternar estados de hardware sin conocer el estado previo.
La parte más delicada es la diferencia entre >> y >>>. En negativeNum >> 1, la VM realiza una extensión de signo: como el bit más significativo de -2 es 1 (indicando un negativo), al desplazar a la derecha, el espacio vacío se rellena con 1s, manteniendo el número negativo (-1). En cambio, con negativeNum >>> 1, el operador trata al entero como una secuencia de bits pura y rellena con 0s, transformando un número que era negativo en uno extremadamente grande y positivo.
El error frecuente
Un error común al trabajar con máscaras de bits es usar el operador de complemento ~ esperando que simplemente “limpie” un bit, cuando en realidad afecta a toda la representación del entero.
int status = 0b0001; // Status: Ready int mask = 0b0001; // Máscara para Ready // Intento erróneo de "limpiar" el bit Ready int badClean = status & ~mask; // Esto es correcto (aplica la máscara invertida) // Error conceptual: Confundir el resultado de ~ con un valor de bits limpio int unexpected = ~mask; print(unexpected); // No es 0, es -2.
Si intentas usar ~mask como un valor absoluto para comparar, fallarás. ~0b0001 no es 0b0000, es una cadena de bits donde todos los bits (incluidos los de signo y los de mayor orden) se han invertido, resultando en -2 debido al complemento a dos. Para limpiar un bit específico, la técnica correcta siempre es valor & ~mascara.
N° 17