Máscaras Binárias no PHP

Artigo que explica o que são máscaras binárias (bitmask) e como podem ser usadas através dos operadores binários do PHP.

Introdução

Máscara binária (máscara de bits ou "bit mask") é um mecanismo usado para armazenar vários valores booleanos em um único campo inteiro e recuperá-los individualmente ou em grupo.

Normalmente, as linguagens de programação que possuem o tipo booleano (bool ou boolean) utilizam um byte (oito bits) para armazená-lo. Apesar de um booleano só precisar de um bit, o byte é usado porque ele é a unidade base das arquiteturas de computadores. Normalmente utiliza-se o valor 00000000 para representar "falso" e o 00000001 para representar "verdadeiro", ou seja, apenas o último bit tem significância.

Logo, para armazenar (por exemplo) 14 valores booleanos, seriam necessários no mínimo 14 bytes. Se estes 14 booleanos fossem armazenados em um inteiro (que normalmente possui 32 bits, que são 4 bytes), usaria apenas 4 bytes.

Numa arquitetura de 32 bits, o número 5 é representado através dos seguintes bits:

00000000 00000000 00000000 00000101

O último e o anti-penúltimo bit valem 1, e os demais valem 0.

Para recuperar o valor de um bit individualmente ou um conjunto de bits, é preciso usar os operadores binários. Para checar o valor do último bit, o que fazemos é utilizar o operador & ("binary and") com o valor cujo último bit vale 1 e os demais valem 0 (ou seja, o 1 decimal). Se o resultado for zero, significa que o bit vale zero, mas se o resultado for diferente de zero, significa que o bit vale 1:

$a = 5;
$b = 1;
$c = $a & $b;

// $a = 00000000 00000000 00000000 00000101
// $b = 00000000 00000000 00000000 00000001
// $c = 00000000 00000000 00000000 00000001

O resultado de $a & $b é calculado bit a bit. Se o primeiro bit de $a e o primeiro bit de $b valem 1, então o primeiro bit do resultado valerá 1 também, caso contrário, valerá 0. Isso é feito bit a bit.

Para saber o valor do penúltimo bit, precisamos usar o mesmo operador sobre um valor cujo penúltimo bit vale 1 e os demais valem 0 (ou seja, o valor 2 decimal):

$a = 5;
$b = 2;
$c = $a & $b;

// $a = 00000000 00000000 00000000 00000101
// $b = 00000000 00000000 00000000 00000010
// $c = 00000000 00000000 00000000 00000000

Neste caso, detectamos que o valor do penúltimo bit vale 0, já que $c foi igual a 0.

Para obter o anti-penúltimo bit, seria necessário usar o valor 4 e não o 3. Isso porque o valor cujo anti-penúltimo bit vale 1 e os demais valem 0 é o 4. Para obtê-lo de forma facilitada, usa-se o operador "shift para esquerda":

$b = 1 << 0;  // 00000000 00000000 00000000 00000001
$b = 1 << 1;  // 00000000 00000000 00000000 00000010
$b = 1 << 2;  // 00000000 00000000 00000000 00000100
$b = 1 << 3;  // 00000000 00000000 00000000 00001000
...
$b = 1 << 31; // 10000000 00000000 00000000 00000000

Ou seja, deslocando o valor que possui o último bit valendo 1 (que é o decimal 1) para a esquerda N vezes. Note que o deslocamento só funciona até 31 (em arquiteturas de 32 bits).


Máscaras de Bits na Prática

Na prática, a máscara de bits ajuda principalmente funções que deveriam receber vários valores booleanos. Ao invés disso, ela recebe um valor inteiro, cujos bits serão avaliados individualmente.

Por exemplo, queremos uma função que devolva uma string com números ("0" a "9") e/ou letras minúsculas ("a" a "z") e/ou letras maiúsculas ("A" a "Z").

Da forma tradicional, seria possível implementá-la da seguinte forma:

/**
 * Obtem uma string com numeros, letras minusculas e/ou letras maiusculas
 * @param bool $numeros: Indica se deseja obter os numeros
 * @param bool $letras_minusculas: Indica se deseja obter letras minusculas
 * @param bool $letras_maiusculas: Indica se deseja obter letras maiusculas
 * @return string
 */
function get_simbolos($numeros, $letras_minusculas, $letras_maiusculas) {
    $resultado = '';
    if ($numeros) {
        $resultado .= '0123456789';
    }
    if ($letras_minusculas) {
        $resultado .= 'abcdefghijklmnopqrstuvwxyz';
    }
    if ($letras_maiusculas) {
        $resultado .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
    }
    return $resultado;
}

Para fazer a mesma função com máscara de bits, seria algo assim:

define('NUMEROS',           1 << 0); // 00000001
define('LETRAS_MINUSCULAS', 1 << 1); // 00000010
define('LETRAS_MAIUSCULAS', 1 << 2); // 00000100
define('LETRAS',            LETRAS_MINUSCULAS | LETRAS_MAIUSCULAS); // 00000110
define('TODOS',             NUMEROS | LETRAS_MINUSCULAS | LETRAS_MAIUSCULAS); 00000111

/**
 * Obtem uma string com numeros, letras minusculas e/ou letras maiusculas
 * @param int $flag: Flag binaria indicando os elementos a serem obtidos
 * @return string
 */
function get_simbolos($flag) {
    $resultado = '';
    if ($flag & NUMEROS) {
        $resultado .= '0123456789';
    }
    if ($flag & LETRAS_MINUSCULAS) {
        $resultado .= 'abcdefghijklmnopqrstuvwxyz';
    }
    if ($flag & LETRAS_MAIUSCULAS) {
        $resultado .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
    }
    return $resultado;
}

Para usar a função, deve-se usar as constantes binárias:

$s = get_simbolos(NUMEROS); // Obtem apenas numeros
$s = get_simbolos(NUMEROS | LETRAS_MINUSCULAS); // Obtem numeros e letras minusculas
$s = get_simbolos(LETRAS); // Obtem letras maiusculas e minusculas
$s = get_simbolos(TODOS); // Obtem todos os simbolos

Observe que as constantes LETRAS e TODOS são atalhos para vários bits marcados. Para tanto, foi usado o operador bit-a-bit "binary or", representado pelo símbolo "|". Isso também é uma vantagem das máscaras de bits: permitir definir grupos de bits.

Para conhecer todos os operadores bit-a-bit, consulte o manual do PHP: Operadores bit-a-bit.

6 comentários

Anônimo disse...

Bela postagem...
Belo post artigo...

Mesmo assim, não entendi totalmente a usabilidade dos operadores binários.

Por que:
NUMEROS 1 << 0
LETRA MAISCULAS 1 << 1
LETRA MINUSCULAS 1 << 2

Por que não zero e um??

Grato antecipdamente.,

Rubens Takiguti Ribeiro (autor do blog) disse...

Olá, Anônino

Foi apenas uma forma de escrever "um bit deslocado N vezes para a esquerda.
No caso:
1 << 0 vale "1" (binário 00000001)
1 << 1 vale "2" (binário 00000010)
1 << 2 vale "4" (binário 00000100)
1 << 3 vale "8" (binário 00001000)

Caso fossem muitas constantes, não precisaria ter que fazer a contas manualmente.

Note que não são usados os valores 3, 5, 6 e 7, pois são números que em binário possuem mais de um bit valendo 1. Logo, não seria possível fazer as operações binárias como gostaríamos.

Atualmente, é mais fácil representar números pela notação binária colocando o "0b" na frente do número binário:
0b00000001 vale "1"
0b00000010 vale "2"
0b00000100 vale "4"
0b00001000 vale "8"