Tipos escalares: inteiros, floats, bool e char

1. Introdução aos tipos escalares em Rust

Tipos escalares representam um único valor. Em Rust, eles são a base sobre a qual tipos mais complexos são construídos. Diferentemente de tipos compostos (arrays, tuplas, structs), que agrupam múltiplos valores, os escalares armazenam exatamente um valor por vez.

Rust possui quatro tipos escalares fundamentais:
- Inteiros: números inteiros com ou sem sinal
- Floats: números de ponto flutuante
- Bool: valores lógicos verdadeiro/falso
- Char: caracteres Unicode

Cada tipo tem características específicas de tamanho, faixa de valores e comportamento, que veremos em detalhes.

2. Inteiros: representação de números inteiros

Rust oferece uma rica variedade de tipos inteiros, divididos em duas categorias:

Com sinal (i = signed): podem ser positivos ou negativos
- i8, i16, i32, i64, i128, isize

Sem sinal (u = unsigned): apenas não-negativos
- u8, u16, u32, u64, u128, usize

O número no nome indica a quantidade de bits. isize e usize dependem da arquitetura da máquina (32 ou 64 bits).

fn main() {
    let pequeno: i8 = -128;
    let grande: u64 = 18_446_744_073_709_551_615;
    let arquitetura: usize = 42; // 64 bits em sistemas modernos
}

Faixas de valores:
- i8: -128 a 127
- u8: 0 a 255
- i32: -2.147.483.648 a 2.147.483.647
- u64: 0 a 18.446.744.073.709.551.615

Literais inteiros podem ser escritos de várias formas:

fn main() {
    let decimal = 98_222;        // separador visual _
    let hex = 0xff;              // 255 em decimal
    let octal = 0o77;            // 63 em decimal
    let binario = 0b1111_0000;   // 240 em decimal
    let byte = b'A';             // 65 (apenas para u8)
}

3. Comportamentos especiais dos inteiros

O tratamento de overflow é um dos aspectos mais distintos de Rust:

fn main() {
    // Em debug: panic!
    // Em release: wrapping (volta ao início)
    let mut x: u8 = 255;
    // x = x + 1; // panic em debug, vira 0 em release

    // Alternativas seguras:
    let (resultado, houve_overflow) = 255u8.overflowing_add(1);
    println!("Overflow: {}, resultado: {}", houve_overflow, resultado);

    let satura = 255u8.saturating_add(1);
    println!("Saturado: {}", satura); // 255

    let checked = 255u8.checked_add(1);
    match checked {
        Some(valor) => println!("OK: {}", valor),
        None => println!("Overflow detectado!"),
    }

    let wrapped = 255u8.wrapping_add(1);
    println!("Wrapping: {}", wrapped); // 0
}

Conversões entre tipos inteiros:

fn main() {
    // Conversão com 'as' (pode truncar)
    let grande: u32 = 300;
    let pequeno: u8 = grande as u8; // 44 (truncado)
    println!("Truncado: {}", pequeno);

    // Conversão segura com TryFrom
    let resultado = u8::try_from(300u32);
    match resultado {
        Ok(valor) => println!("Conversão OK: {}", valor),
        Err(e) => println!("Erro: {}", e),
    }

    // From funciona apenas quando não há perda
    let valor: u32 = u32::from(255u8); // sempre OK
}

4. Floats: números de 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 por oferecer maior precisão.

fn main() {
    let precisao_simples: f32 = 3.14;
    let precisao_dupla = 3.141592653589793; // f64 por padrão

    // Notação científica
    let grande = 1.5e10;   // 15.000.000.000
    let pequeno = 2.5e-4;  // 0.00025

    println!("f32: {}", precisao_simples);
    println!("f64: {}", precisao_dupla);
    println!("Científico: {} e {}", grande, pequeno);
}

Operações aritméticas e funções matemáticas:

fn main() {
    let a = 10.5;
    let b = 3.2;

    // Operações básicas
    println!("Soma: {}", a + b);
    println!("Subtração: {}", a - b);
    println!("Multiplicação: {}", a * b);
    println!("Divisão: {}", a / b);
    println!("Resto: {}", a % b);

    // Métodos matemáticos
    println!("Raiz quadrada: {}", (25.0_f64).sqrt());
    println!("Potência: {}", (2.0_f64).powi(3)); // 8.0
    println!("Valor absoluto: {}", (-3.5_f64).abs());
    println!("Arredondamento: {}", (3.7_f64).round());

    // Constantes especiais
    println!("PI: {}", std::f64::consts::PI);
    println!("Infinito: {}", f64::INFINITY);
    println!("NaN: {}", f64::NAN);
}

5. Bool: o tipo lógico

O tipo bool representa valores lógicos com apenas dois estados possíveis: true e false.

fn main() {
    let verdadeiro: bool = true;
    let falso: bool = false;

    // Operadores booleanos
    let e_logico = verdadeiro && falso;  // false
    let ou_logico = verdadeiro || falso; // true
    let negacao = !verdadeiro;           // false

    println!("AND: {}", e_logico);
    println!("OR: {}", ou_logico);
    println!("NOT: {}", negacao);

    // Uso em expressões condicionais
    let idade = 18;
    let pode_votar = idade >= 16;

    if pode_votar {
        println!("Pode votar!");
    } else {
        println!("Não pode votar");
    }

    // Comparações retornam bool
    let x = 10;
    let y = 20;
    println!("x == y: {}", x == y);  // false
    println!("x < y: {}", x < y);    // true
    println!("x != y: {}", x != y);  // true
}

6. Char: o tipo caractere Unicode

Diferente de C/C++, onde char tem 1 byte, em Rust char tem 4 bytes e pode representar qualquer caractere Unicode.

fn main() {
    let letra: char = 'a';
    let emoji = '😊';
    let escape = '\n';  // nova linha
    let unicode = '\u{1F600}'; // 😀 em hexadecimal

    println!("Letra: {}", letra);
    println!("Emoji: {}", emoji);
    println!("Unicode: {}", unicode);

    // Escapes especiais
    let tab = '\t';
    let barra_invertida = '\\';
    let aspas_simples = '\'';
    let aspas_duplas = '"'; // não precisa de escape em char

    println!("Tab{}aqui", tab);
    println!("Barra: {}", barra_invertida);
    println!("Aspas: {}", aspas_simples);

    // Tamanho em bytes (4 bytes cada)
    println!("Tamanho de 'a': {} bytes", std::mem::size_of_val(&letra));
    println!("Tamanho de '😊': {} bytes", std::mem::size_of_val(&emoji));
}

7. Comparações e operações entre tipos escalares

Rust é fortemente tipada e não permite operações entre tipos diferentes sem conversão explícita:

fn main() {
    // Erro comum: misturar tipos
    let inteiro: i32 = 10;
    let flutuante: f64 = 3.14;

    // let soma = inteiro + flutuante; // ERRO! Tipos diferentes

    // Correto: conversão explícita
    let soma = inteiro as f64 + flutuante;
    println!("Soma: {}", soma);

    // Outro erro comum
    let a: u8 = 200;
    let b: u16 = 100;
    // let c = a + b; // ERRO! u8 + u16

    // Soluções:
    let c1 = a as u16 + b;
    let c2 = a + b as u8; // cuidado com overflow!
    println!("c1: {}, c2: {}", c1, c2);

    // Comparações entre tipos diferentes
    let x: i32 = 5;
    let y: f64 = 5.0;
    // println!("{}", x == y); // ERRO! Não pode comparar
    println!("{}", (x as f64) == y); // true
}

Boas práticas para conversões:
- Use as apenas quando tem certeza que não há perda
- Prefira From/TryFrom para conversões seguras
- Evite misturar inteiros com sinal e sem sinal em operações aritméticas

8. Resumo e dicas de uso prático

Quando usar cada tipo escalar:

Tipo Uso principal
i32 Padrão para inteiros, melhor performance em CPUs modernas
u8 Dados binários, bytes, valores pequenos (0-255)
u64/i64 Valores muito grandes, timestamps
usize Indexação de arrays, tamanhos de coleções
f64 Padrão para floats, cálculos científicos
f32 Gráficos 3D, quando memória é crítica
bool Flags, condições lógicas
char Caracteres Unicode individuais

Escolha inteligente de tipos:

fn main() {
    // Para a maioria dos casos, i32 é suficiente
    let contador: i32 = 42;

    // Para loops com coleções, use usize
    let indices: Vec<usize> = vec![0, 1, 2, 3];

    // Para economizar memória em grandes volumes
    let pixels: Vec<u8> = vec![255, 128, 64]; // cores RGB

    // Para cálculos financeiros, considere bibliotecas especializadas
    // (f64 tem problemas de precisão para dinheiro)
}

Referência rápida para literais e conversões:

fn main() {
    // Literais com sufixo de tipo
    let x = 42i32;      // i32
    let y = 3.14f32;    // f32
    let z = 100u8;      // u8

    // Conversões comuns
    let de_int_para_float = 42i32 as f64;      // 42.0
    let de_float_para_int = 3.99_f64 as i32;   // 3 (trunca)
    let de_bool_para_int = true as i32;        // 1
    let de_int_para_char = 65u8 as char;       // 'A'

    println!("{} {} {} {} {}", x, y, z, de_int_para_float, de_float_para_int);
}

Lembre-se: em Rust, a segurança de tipos é uma característica fundamental. Aproveite o sistema de tipos para evitar erros comuns de programação que outras linguagens permitem.

Referências