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. Cada um tem características específicas de tamanho, sinal e comportamento, especialmente em relação a operações aritméticas e representação de dados.

Entender esses tipos é crucial para escrever código seguro e eficiente, pois Rust é uma linguagem fortemente tipada que oferece controle refinado sobre a memória e prevenção de erros comuns, como overflow.

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 indica a quantidade de bits. Por exemplo, i32 ocupa 32 bits e pode representar valores de -2^31 a 2^31-1. O tipo isize depende da arquitetura da máquina (32 ou 64 bits).

Overflow ocorre quando o resultado de uma operação ultrapassa os limites do tipo. Em compilação debug, Rust detecta overflow e causa pânico (panic). Em release, o comportamento é de wrapping (envoltório), ou seja, o valor é truncado. Para lidar com overflow explicitamente, existem métodos como wrapping_add, checked_add, overflowing_add e saturating_add.

fn main() {
    let x: u8 = 255;
    // Em debug, isso causa pânico:
    // let y = x + 1;

    // Wrapping: 255 + 1 = 0
    let y = x.wrapping_add(1);
    println!("wrapping_add: {}", y); // 0

    // Checked: retorna Option
    match x.checked_add(1) {
        Some(val) => println!("checked: {}", val),
        None => println!("overflow!")
    }

    // Overflowing: retorna (resultado, indicador de overflow)
    let (result, overflowed) = x.overflowing_add(1);
    println!("overflowing: {} (overflow: {})", result, overflowed);

    // Saturating: satura no valor máximo
    let z = x.saturating_add(1);
    println!("saturating: {}", z); // 255
}

Ponto flutuante

Rust possui dois tipos de ponto flutuante: f32 (precisão simples) e f64 (precisão dupla). O padrão é f64, pois oferece maior precisão e desempenho similar em CPUs modernas. Eles seguem o padrão IEEE 754.

Operações com ponto flutuante podem resultar em valores especiais como infinito (INFINITY) e NaN (Not a Number). Comparações com NaN sempre retornam false, exceto quando usando o método is_nan().

fn main() {
    let a: f64 = 2.0;
    let b: f32 = 3.0;

    let soma = a + b as f64; // precisa converter
    println!("soma: {}", soma);

    let inf = 1.0 / 0.0;
    println!("infinito: {}", inf); // inf

    let nan = 0.0 / 0.0;
    println!("NaN: {}", nan); // NaN
    println!("NaN == NaN? {}", nan == nan); // false
    println!("is_nan? {}", nan.is_nan()); // true
}

Booleano

O tipo booleano em Rust é bool, com valores true e false. Ocupa 1 byte. É usado em condições, loops e expressões lógicas. Conversão para inteiro não é automática; deve-se usar as keyword as (ex: true as u8 retorna 1).

fn main() {
    let ativo: bool = true;
    let desligado = false;

    if ativo {
        println!("Ativo!");
    }

    // Conversão para inteiro
    let numero = ativo as u8;
    println!("true como u8: {}", numero); // 1

    // Operadores lógicos
    let e = ativo && desligado; // false
    let ou = ativo || desligado; // true
    let nao = !ativo; // false
}

Caractere (char e Unicode)

O tipo char em Rust representa um caractere Unicode de 4 bytes (32 bits). Diferente de outras linguagens onde char é ASCII, Rust suporta qualquer caractere Unicode, incluindo emojis e caracteres acentuados. Literais char são escritos entre aspas simples: 'a', 'ç', '😊'.

Internamente, char é um valor numérico de 0 a 0x10FFFF. Pode-se converter para u32 e vice-versa, desde que o valor seja válido. A função char::from_u32 retorna Option.

fn main() {
    let letra: char = 'R';
    let emoji: char = '🚀';
    let acento: char = 'ã';

    println!("Letra: {}", letra);
    println!("Emoji: {}", emoji);
    println!("Acento: {}", acento);

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

    // De u32 para char
    let val: u32 = 0x1F680; // 🚀
    match char::from_u32(val) {
        Some(c) => println!("Caractere: {}", c),
        None => println!("Valor inválido")
    }

    // Tamanho em bytes
    println!("Tamanho de char: {} bytes", std::mem::size_of::()); // 4
}

Boas práticas

  • Prefira tipos inteiros com sinal (i32) para valores gerais, a menos que precise de otimização ou representação específica.
  • Use usize para indexação de coleções e tamanhos.
  • Para ponto flutuante, f64 é padrão e suficiente na maioria dos casos; use f32 apenas se precisar economizar memória ou em computação gráfica.
  • Evite comparar floats diretamente com ==; use uma margem de erro (epsilon).
  • Char é ideal para caracteres Unicode; para processamento de texto, use &str ou String.

Referências

Exercícios

  1. Escreva uma função que recebe um u8 e retorna o resultado de wrapping_add com 200. Teste com 100 e 200.

    ✓ Resposta:
    fn wrapping_add_example(x: u8) -> u8 {
        x.wrapping_add(200)
    }
    
    fn main() {
        println!("{}", wrapping_add_example(100)); // 44 (300 - 256)
        println!("{}", wrapping_add_example(200)); // 144 (400 - 256)
    }
  2. Crie uma função que calcula a média de dois números f64 e retorna o resultado. Teste com 3.5 e 4.5.

    ✓ Resposta:
    fn media(a: f64, b: f64) -> f64 {
        (a + b) / 2.0
    }
    
    fn main() {
        println!("{}", media(3.5, 4.5)); // 4.0
    }
  3. Escreva uma expressão que retorne true se um booleano for verdadeiro e um número for maior que 10. Teste com (true, 15) e (false, 5).

    ✓ Resposta:
    fn check(b: bool, x: i32) -> bool {
        b && x > 10
    }
    
    fn main() {
        println!("{}", check(true, 15)); // true
        println!("{}", check(false, 5)); // false
    }
  4. Converta o caractere 'Ω' (Omega maiúsculo, U+03A9) para seu código u32 e depois de volta para char. Imprima ambos.

    ✓ Resposta:
    fn main() {
        let omega: char = 'Ω';
        let codigo = omega as u32;
        println!("Código: {}", codigo); // 937
        let de_volta = char::from_u32(codigo).unwrap();
        println!("Caractere: {}", de_volta); // Ω
    }
  5. Escreva um programa que some dois números i32, mas use o método checked_add e trate o overflow imprimindo "Overflow!" caso ocorra. Teste com (2147483647, 1) e (100, 200).

    ✓ Resposta:
    fn checked_add_example(a: i32, b: i32) {
        match a.checked_add(b) {
            Some(soma) => println!("Soma: {}", soma),
            None => println!("Overflow!")
        }
    }
    
    fn main() {
        checked_add_example(2147483647, 1); // Overflow!
        checked_add_example(100, 200); // Soma: 300
    }