Tipos escalares
Esta aula aborda os tipos escalares em Rust: inteiros (com overflow), ponto flutuante, booleanos e caracteres Unicode. Inclui exemplos práticos e exercícios para fixação.
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
- The Rust Book: Data Types
- Documentação do tipo i32
- Documentação do tipo f64
- Documentação do tipo bool
- Documentação do tipo char
- Rust by Example: Primitives
- Rust Reference: Types
Exercícios
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.
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"); } }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); } }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); }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.