Em Rust, tipos escalares representam um único valor. Eles são os blocos fundamentais para construir dados mais complexos. Nesta aula, exploraremos os quatro tipos escalares primários: inteiros, ponto flutuante, booleanos e caracteres. Compreender suas características, limites e comportamentos é essencial para escrever código seguro e eficiente em Rust.

Rust é uma linguagem fortemente tipada, o que significa que o tipo de cada variável deve ser conhecido em tempo de compilação. Os tipos escalares são definidos de forma precisa, evitando ambiguidades e ajudando o compilador a otimizar o código. Vamos detalhar cada um deles.

Inteiros e overflow

Inteiros em Rust podem ser com sinal (i8, i16, i32, i64, i128, isize) ou sem sinal (u8, u16, u32, u64, u128, usize). O número após o tipo indica a quantidade de bits. Por exemplo, i32 é um inteiro com sinal de 32 bits, capaz de armazenar valores de -2^31 a 2^31-1. isize e usize dependem da arquitetura da máquina (32 ou 64 bits).

Overflow de inteiros ocorre quando uma operação aritmética produz um valor fora do intervalo representável. Em Rust, o comportamento padrão em modo debug é causar um pânico (panic) em tempo de execução, enquanto em modo release o overflow é ignorado e o valor é truncado (wrap-around). Para evitar surpresas, você pode usar métodos explícitos como wrapping_add, checked_add, overflowing_add ou saturating_add. Exemplo:

fn main() {
    let a: u8 = 255;
    // Em debug, isso causa panic:
    // let b = a + 1;

    // Maneira segura:
    let b = a.wrapping_add(1); // resulta em 0
    println!("wrapping: {}", b);

    let c = a.checked_add(1); // retorna Option: None
    match c {
        Some(val) => println!("checked: {}", val),
        None => println!("overflow!")
    }

    let (d, overflowed) = a.overflowing_add(1);
    println!("overflowing: {}, overflowed: {}", d, overflowed);

    let e = a.saturating_add(1); // satura no máximo: 255
    println!("saturating: {}", e);
}

Ponto flutuante

Rust oferece dois tipos de ponto flutuante: f32 (precisão simples, 32 bits) e f64 (precisão dupla, 64 bits). O padrão é f64, pois em CPUs modernas ele é tão rápido quanto f32 e oferece maior precisão. Os literais de ponto flutuante seguem a notação usual: 3.14, 2.0, 1e5.

Operações com ponto flutuante seguem o padrão IEEE 754. Cuidado com comparações de igualdade devido a erros de arredondamento. É preferível usar uma margem de tolerância (epsilon) ou métodos como is_subnormal. Exemplo:

fn main() {
    let x: f64 = 2.0;
    let y: f32 = 3.0;
    let z = x / y as f64; // precisa converter
    println!("divisão: {}", z);

    // Comparação segura
    let a = 0.1 + 0.2;
    let b = 0.3;
    if (a - b).abs() < f64::EPSILON {
        println!("aproximadamente iguais");
    }
}

Booleano

O tipo booleano em Rust é bool, com valores true e false. Ele ocupa 1 byte. Booleanos são usados principalmente em expressões condicionais, como if, while e operadores lógicos (&&, ||, !). Rust não converte automaticamente outros tipos para bool; você deve usar comparações explícitas.

Exemplo de uso:

fn main() {
    let ativo: bool = true;
    if ativo {
        println!("Está ativo");
    } else {
        println!("Inativo");
    }

    // Operadores lógicos
    let a = true;
    let b = false;
    println!("a AND b: {}", a && b); // false
    println!("a OR b: {}", a || b);  // true
    println!("NOT a: {}", !a);       // false
}

Caractere (char e Unicode)

O tipo char representa um caractere Unicode de 4 bytes, o que permite representar qualquer caractere Unicode, incluindo emojis e caracteres de diferentes alfabetos. Diferente de C, onde char é um byte, em Rust char é sempre 4 bytes. Literais char são escritos entre aspas simples: 'a', 'π', '😊'.

Internamente, char é um valor numérico de 32 bits que corresponde ao ponto de código Unicode. Você pode converter entre char e u32 usando as. Exemplo:

fn main() {
    let letra: char = 'R';
    let emoji: char = '🦀'; // caranguejo, símbolo do Rust
    println!("Letra: {}, Emoji: {}", letra, emoji);

    // Converter char para u32
    let codigo = letra as u32;
    println!("Código Unicode de 'R': {}", codigo);

    // Converter u32 para char (cuidado com valores inválidos)
    let de_codigo = char::from_u32(0x1F980).unwrap(); // 🦀
    println!("De código: {}", de_codigo);
}

Strings em Rust são coleções de bytes UTF-8, não de chars. Portanto, iterar sobre uma string como chars pode ser necessário para acessar caracteres Unicode completos.

Boas práticas e observações finais

Ao trabalhar com inteiros, prefira tipos com sinal (i32) como padrão, a menos que precise de números negativos ou de otimizações específicas. Use usize para indexação de coleções. Para ponto flutuante, evite comparar igualdade diretamente. Para booleanos, evite comparações redundantes como if x == true, prefira if x. Caracteres Unicode são poderosos, mas lembre-se de que operações como length em strings contam bytes, não caracteres.

Referências

Exercícios

  1. Crie uma variável do tipo u8 com valor 200. Some 100 a ela usando o método wrapping_add e imprima o resultado. Explique o que acontece.

    ✓ Resposta:
    fn main() {
        let a: u8 = 200;
        let b = a.wrapping_add(100); // 200+100=300, mas u8 máximo é 255, então wrapping: 300-256=44
        println!("Resultado: {}", b); // imprime 44
    }

    O método wrapping_add faz a soma com wrap-around: como 300 excede o limite de u8 (255), o valor é reduzido módulo 256, resultando em 44.

  2. Declare duas variáveis f64: uma com valor 0.1 e outra com 0.2. Some-as e compare com 0.3. Use uma margem de tolerância (epsilon) para verificar se são aproximadamente iguais.

    ✓ Resposta:
    fn main() {
        let a = 0.1_f64;
        let b = 0.2_f64;
        let soma = a + b;
        let alvo = 0.3_f64;
        if (soma - alvo).abs() < f64::EPSILON {
            println!("Aproximadamente iguais");
        } else {
            println!("Diferentes");
        }
    }
  3. Escreva um programa que verifica se um número inteiro (i32) é par ou ímpar usando um booleano. Imprima o resultado.

    ✓ Resposta:
    fn main() {
        let numero: i32 = 42;
        let eh_par = numero % 2 == 0;
        if eh_par {
            println!("{} é par", numero);
        } else {
            println!("{} é ímpar", numero);
        }
    }
  4. Crie uma variável char que armazene o caractere 'Ω' (Omega maiúsculo, ponto Unicode U+03A9). Converta-o para u32 e imprima o código. Depois, converta de volta para char usando from_u32.

    ✓ Resposta:
    fn main() {
        let omega: char = 'Ω';
        let codigo = omega as u32;
        println!("Código Unicode de Ω: {}", codigo); // 937
        let de_volta = char::from_u32(codigo).unwrap();
        println!("De volta: {}", de_volta);
    }
  5. Escreva um programa que declare uma variável do tipo i8 com valor -128 e subtraia 1 usando wrapping_sub. Imprima o resultado e explique.

    ✓ Resposta:
    fn main() {
        let a: i8 = -128;
        let b = a.wrapping_sub(1); // -128 - 1 = -129, mas i8 mínimo é -128, então wrapping: -129 + 256 = 127
        println!("Resultado: {}", b); // 127
    }

    O método wrapping_sub faz a subtração com wrap-around: como -129 está abaixo do limite inferior de i8, o valor é ajustado módulo 256, resultando em 127.