Tipos Numéricos em Rust: i32, u64, f64 e Mais

Guia completo dos tipos numéricos em Rust: inteiros i8-i128, u8-u128, isize, usize, floats f32 e f64, aritmética segura e conversão com as e TryFrom.

Os tipos numéricos são os tipos mais fundamentais em Rust. A linguagem oferece inteiros com e sem sinal em tamanhos de 8 a 128 bits, dois tipos de ponto flutuante, e um conjunto robusto de operações aritméticas que previnem erros comuns como overflow silencioso. Entender esses tipos é essencial para escrever código Rust correto e eficiente.

Tabela de Tipos Inteiros

TipoTamanhoFaixa de ValoresUso Típico
i88 bits-128 a 127Dados compactos, C FFI
i1616 bits-32.768 a 32.767Áudio, C FFI
i3232 bits-2.147.483.648 a 2.147.483.647Padrão para inteiros
i6464 bits-9,2 x 10^18 a 9,2 x 10^18Timestamps, IDs grandes
i128128 bits-1,7 x 10^38 a 1,7 x 10^38Criptografia, cálculos grandes
u88 bits0 a 255Bytes, pixels, ASCII
u1616 bits0 a 65.535Portas de rede, Unicode
u3232 bits0 a 4.294.967.295Contadores, IDs
u6464 bits0 a 1,8 x 10^19Hashes, tamanhos de arquivo
u128128 bits0 a 3,4 x 10^38UUIDs, criptografia
isizeptr sizeDepende da plataformaÍndices de ponteiro
usizeptr sizeDepende da plataformaÍndices, tamanhos

Tabela de Tipos de Ponto Flutuante

TipoTamanhoPrecisãoFaixaUso Típico
f3232 bits~7 dígitos+/-3,4 x 10^38Gráficos, GPU, quando memória importa
f6464 bits~15 dígitos+/-1,8 x 10^308Padrão, cálculos científicos

Literais Numéricos

fn main() {
    // Literais inteiros
    let decimal = 1_000_000;         // separador _ para legibilidade
    let hexadecimal = 0xFF;          // 255
    let octal = 0o77;                // 63
    let binario = 0b1111_0000;       // 240
    let byte = b'A';                 // u8, valor 65

    // Sufixo de tipo
    let x = 42i32;                   // i32
    let y = 100u64;                  // u64
    let z = 3.14f32;                 // f32

    // Literais de ponto flutuante
    let pi = 3.14159265;             // f64 por padrão
    let avogadro = 6.022e23;         // notação científica
    let pequeno = 1.6e-19_f64;      // com sufixo

    // Tipo padrão
    let inteiro = 42;                // i32 (padrão)
    let flutuante = 3.14;            // f64 (padrão)

    println!("decimal: {}", decimal);
    println!("hex: {:#X}", hexadecimal);
    println!("oct: {:#o}", octal);
    println!("bin: {:#010b}", binario);
    println!("byte: {}", byte);
    println!("avogadro: {:.3e}", avogadro);
}

Exemplos Práticos

1. Limites e Constantes dos Tipos

fn main() {
    // Constantes MIN e MAX
    println!("i8:    {} a {}", i8::MIN, i8::MAX);
    println!("i16:   {} a {}", i16::MIN, i16::MAX);
    println!("i32:   {} a {}", i32::MIN, i32::MAX);
    println!("i64:   {} a {}", i64::MIN, i64::MAX);
    println!("i128:  {} a {}", i128::MIN, i128::MAX);
    println!();
    println!("u8:    {} a {}", u8::MIN, u8::MAX);
    println!("u16:   {} a {}", u16::MIN, u16::MAX);
    println!("u32:   {} a {}", u32::MIN, u32::MAX);
    println!("u64:   {} a {}", u64::MIN, u64::MAX);
    println!();
    println!("usize: {} a {} ({} bits)", usize::MIN, usize::MAX, usize::BITS);
    println!("isize: {} a {} ({} bits)", isize::MIN, isize::MAX, isize::BITS);
    println!();
    println!("f32 epsilon: {}", f32::EPSILON);
    println!("f64 epsilon: {}", f64::EPSILON);
    println!("f64 infinity: {}", f64::INFINITY);
    println!("f64 NaN: {}", f64::NAN);
    println!("f64 pi: {}", std::f64::consts::PI);
    println!("f64 e: {}", std::f64::consts::E);

    // Tamanho em bytes
    println!("\nTamanhos:");
    println!("  i32: {} bytes", std::mem::size_of::<i32>());
    println!("  u64: {} bytes", std::mem::size_of::<u64>());
    println!("  f64: {} bytes", std::mem::size_of::<f64>());
    println!("  usize: {} bytes", std::mem::size_of::<usize>());
}

2. Aritmética Segura — checked, wrapping, saturating, overflowing

fn main() {
    let a: u8 = 250;
    let b: u8 = 10;

    // Em modo debug, overflow causa panic!
    // let resultado = a + b; // PANIC em debug!

    // checked: retorna None em overflow
    let checked = a.checked_add(b);
    println!("checked_add: {:?}", checked); // None

    let checked_ok = 100u8.checked_add(50);
    println!("checked_add (ok): {:?}", checked_ok); // Some(150)

    // wrapping: faz wrap around (como C)
    let wrapped = a.wrapping_add(b);
    println!("wrapping_add: {}", wrapped); // 4 (256 + 4 = 260, mod 256 = 4)

    // saturating: para no MAX ou MIN
    let saturated = a.saturating_add(b);
    println!("saturating_add: {}", saturated); // 255 (u8::MAX)

    let saturated_sub = 5u8.saturating_sub(10);
    println!("saturating_sub: {}", saturated_sub); // 0 (u8::MIN)

    // overflowing: retorna (resultado, overflow?)
    let (resultado, overflow) = a.overflowing_add(b);
    println!("overflowing_add: {} (overflow: {})", resultado, overflow);
    // 4 (overflow: true)

    // Multiplicação segura
    let grande = 1_000_000_000i32;
    match grande.checked_mul(3) {
        Some(r) => println!("Multiplicação ok: {}", r),    // 3_000_000_000 > i32::MAX
        None => println!("Overflow na multiplicação!"),
    }

    // Potência segura
    let base: u64 = 2;
    println!("2^10 = {}", base.pow(10));         // 1024
    println!("2^63 checked = {:?}", base.checked_pow(63)); // Some(...)
    println!("2^64 checked = {:?}", base.checked_pow(64)); // None (overflow)
}

3. Conversão entre Tipos — as, From, TryFrom

use std::convert::TryFrom;

fn main() {
    // 'as' — conversão explícita (pode perder dados!)
    let grande: i64 = 1_000_000;
    let pequeno: i32 = grande as i32; // OK, cabe em i32
    println!("i64 -> i32: {}", pequeno);

    let muito_grande: i64 = 5_000_000_000;
    let truncado: i32 = muito_grande as i32; // PERDA DE DADOS!
    println!("5 bilhões as i32: {} (TRUNCADO!)", truncado);

    // float -> int trunca a parte decimal
    let pi = 3.99;
    let inteiro = pi as i32; // 3, não 4!
    println!("3.99 as i32: {} (truncado, não arredondado)", inteiro);

    // int -> float pode perder precisão
    let grande_int: i64 = (1i64 << 53) + 1;
    let como_f64 = grande_int as f64;
    println!("2^53 + 1 como f64: {} (pode perder precisão)", como_f64);

    // From — conversão segura (nunca perde dados)
    let x: i32 = 42;
    let y: i64 = i64::from(x);       // i32 -> i64 sempre cabe
    let z: f64 = f64::from(x);       // i32 -> f64 sempre cabe
    println!("From: i32({}) -> i64({}) -> f64({})", x, y, z);

    let byte: u8 = 200;
    let maior: u32 = u32::from(byte); // u8 -> u32 sempre cabe
    println!("u8({}) -> u32({})", byte, maior);

    // TryFrom — conversão que pode falhar
    let valor: i64 = 300;
    match u8::try_from(valor) {
        Ok(v) => println!("i64({}) -> u8({}) ok!", valor, v),
        Err(e) => println!("i64({}) -> u8 falhou: {}", valor, e),
    }

    let ok: i64 = 100;
    let resultado = u8::try_from(ok);
    println!("i64({}) -> u8 = {:?}", ok, resultado); // Ok(100)

    // Conversão de string para número
    let numero: i32 = "42".parse().unwrap();
    let flutuante: f64 = "3.14".parse().unwrap();
    println!("parse: {} e {}", numero, flutuante);

    // Conversão com tratamento de erro
    match "abc".parse::<i32>() {
        Ok(n) => println!("Convertido: {}", n),
        Err(e) => println!("Erro ao converter 'abc': {}", e),
    }
}

4. Métodos Úteis dos Tipos Numéricos

fn main() {
    // Métodos de inteiros
    let n: i32 = -42;
    println!("abs: {}", n.abs());           // 42
    println!("signum: {}", n.signum());     // -1
    println!("pow: {}", 2i32.pow(10));      // 1024
    println!("min: {}", 10i32.min(20));     // 10
    println!("max: {}", 10i32.max(20));     // 20
    println!("clamp: {}", 150i32.clamp(0, 100)); // 100

    // Contagem de bits
    let bits: u32 = 0b1010_1100;
    println!("\nBits de {:08b}:", bits);
    println!("  count_ones: {}", bits.count_ones());     // 4
    println!("  count_zeros: {}", bits.count_zeros());   // 28
    println!("  leading_zeros: {}", bits.leading_zeros()); // 24
    println!("  trailing_zeros: {}", bits.trailing_zeros()); // 2

    // Métodos de ponto flutuante
    let x: f64 = 2.7;
    println!("\nf64 métodos:");
    println!("  floor: {}", x.floor());     // 2.0
    println!("  ceil: {}", x.ceil());       // 3.0
    println!("  round: {}", x.round());     // 3.0
    println!("  trunc: {}", x.trunc());     // 2.0
    println!("  fract: {}", x.fract());     // 0.7
    println!("  sqrt: {}", x.sqrt());       // 1.643...
    println!("  cbrt: {}", x.cbrt());       // 1.392...
    println!("  ln: {}", x.ln());           // 0.993...
    println!("  log2: {}", x.log2());       // 1.432...
    println!("  log10: {}", x.log10());     // 0.431...
    println!("  sin: {}", x.sin());
    println!("  cos: {}", x.cos());

    // Verificações de ponto flutuante
    let inf = f64::INFINITY;
    let nan = f64::NAN;
    println!("\nVerificações:");
    println!("  INFINITY.is_infinite: {}", inf.is_infinite());   // true
    println!("  INFINITY.is_finite: {}", inf.is_finite());       // false
    println!("  NAN.is_nan: {}", nan.is_nan());                  // true
    println!("  3.14.is_normal: {}", 3.14f64.is_normal());       // true
    println!("  0.0.is_normal: {}", 0.0f64.is_normal());         // false

    // CUIDADO: NaN != NaN
    println!("  NaN == NaN: {}", nan == nan); // false!
}

5. Padrões Comuns e Boas Práticas

fn main() {
    // Evitar overflow com tipos maiores
    let fatorial_10: u64 = (1..=10u64).product();
    println!("10! = {}", fatorial_10); // 3628800

    // Formatar números
    let valor = 1_234_567.89;
    println!("Padrão: {}", valor);
    println!("2 decimais: {:.2}", valor);
    println!("Científico: {:.3e}", valor);
    println!("Hexadecimal: {:X}", 255);
    println!("Binário: {:08b}", 42);
    println!("Octal: {:o}", 255);
    println!("Com sinal: {:+}", 42);
    println!("Padding: {:>10}", 42);
    println!("Zero-pad: {:010}", 42);

    // Comparar floats com epsilon
    let a = 0.1 + 0.2;
    let b = 0.3;
    println!("\n0.1 + 0.2 == 0.3? {}", a == b);     // false!
    println!("0.1 + 0.2 = {:.20}", a);

    // Comparação correta com epsilon
    let epsilon = 1e-10;
    println!("Aproximadamente igual? {}", (a - b).abs() < epsilon); // true

    // Usar usize para índices
    let dados = vec![10, 20, 30, 40, 50];
    let indice: usize = 3; // índices são sempre usize
    println!("\ndados[{}] = {}", indice, dados[indice]);

    // Converter entre isize/usize cuidadosamente
    let tamanho: usize = dados.len();
    let diferenca: isize = 2 - tamanho as isize; // pode ser negativo
    println!("Diferença: {}", diferenca);

    // Prevenir divisão por zero
    fn dividir_seguro(a: f64, b: f64) -> Option<f64> {
        if b == 0.0 {
            None
        } else {
            Some(a / b)
        }
    }

    match dividir_seguro(10.0, 3.0) {
        Some(r) => println!("10 / 3 = {:.4}", r),
        None => println!("Divisão por zero!"),
    }
}

Características de Desempenho

OperaçãoInteirosFloats
Adição/SubtraçãoO(1), 1 cicloO(1), 1-3 ciclos
MultiplicaçãoO(1), 1-3 ciclosO(1), 3-5 ciclos
DivisãoO(1), 20-40 ciclosO(1), 10-20 ciclos
ComparaçãoO(1), 1 cicloO(1), 1-3 ciclos
Conversão int->floatO(1), 3-5 ciclos-
checked_add vs add+1-2 ciclos overhead-
sqrt/sin/cos/ln-O(1), 10-30 ciclos

Tamanho e alinhamento: Os tipos numéricos são alinhados ao seu próprio tamanho. Um i32 ocupa 4 bytes com alinhamento de 4 bytes. Um i128 ocupa 16 bytes com alinhamento de 16 bytes em muitas plataformas.

Escolha de tipo:

  • Use i32 como padrão para inteiros.
  • Use u8 para bytes e dados binários.
  • Use usize para índices e tamanhos.
  • Use f64 como padrão para ponto flutuante.
  • Use f32 apenas quando memória/bandwidth importa (GPU, grandes arrays).

Tabela Comparativa: Segurança Aritmética

MétodoOverflow BehaviorTipo de RetornoQuando Usar
+ (operador)Panic em debug, wrap em releaseTQuando overflow é um bug
checked_addRetorna NoneOption<T>Quando overflow é possível
wrapping_addWrap aroundTHashing, criptografia
saturating_addPara no MAX/MINTÁudio, processamento de sinais
overflowing_addWrap + flag(T, bool)Quando precisa saber se houve overflow

Veja Também