Neste tutorial, vamos explorar os blocos fundamentais de qualquer programa Rust: variáveis, tipos de dados e funções. Se você vem de outras linguagens, vai notar que Rust tem algumas particularidades interessantes, especialmente em relação à imutabilidade por padrão.
Variáveis e Imutabilidade
Em Rust, variáveis são declaradas com a palavra-chave let. Uma característica marcante do Rust é que variáveis são imutáveis por padrão:
fn main() {
let x = 5;
println!("O valor de x é: {}", x);
// x = 10; // ERRO! Variáveis são imutáveis por padrão
}
Se você tentar reatribuir um valor a x, o compilador vai retornar um erro:
error[E0384]: cannot assign twice to immutable variable `x`
Isso pode parecer estranho se você vem de linguagens como Python ou JavaScript, mas é uma decisão de design intencional. A imutabilidade por padrão ajuda a prevenir bugs, torna o código mais previsível e facilita a programação concorrente.
Variáveis Mutáveis
Quando você precisa que uma variável mude de valor, use mut:
fn main() {
let mut contador = 0;
println!("Contador: {}", contador);
contador += 1;
println!("Contador: {}", contador);
contador += 1;
println!("Contador: {}", contador);
}
Saída:
Contador: 0
Contador: 1
Contador: 2
A regra de ouro é: use let por padrão e só adicione mut quando realmente precisar mudar o valor. Isso documenta sua intenção no código.
Constantes
Constantes são declaradas com const e devem ter o tipo anotado explicitamente. Elas são sempre imutáveis e são avaliadas em tempo de compilação:
const VELOCIDADE_DA_LUZ: f64 = 299_792_458.0; // metros por segundo
const HORAS_POR_DIA: u32 = 24;
const PI: f64 = 3.14159265358979;
fn main() {
println!("Velocidade da luz: {} m/s", VELOCIDADE_DA_LUZ);
println!("Horas por dia: {}", HORAS_POR_DIA);
println!("Pi: {}", PI);
}
Note o uso de _ como separador visual em números grandes (299_792_458). Isso é uma conveniência do Rust que torna números longos mais legíveis.
Diferenças entre let e const:
constexige anotação de tipoconstsó aceita expressões que podem ser calculadas em tempo de compilaçãoconstpode ser declarada no escopo global- Por convenção, constantes usam
UPPER_SNAKE_CASE
Shadowing (Sombreamento)
Rust permite declarar uma nova variável com o mesmo nome de uma anterior. Isso é chamado de shadowing:
fn main() {
let x = 5;
let x = x + 1; // x agora é 6
let x = x * 2; // x agora é 12
println!("O valor de x é: {}", x); // Imprime: 12
// Shadowing permite mudar o tipo!
let espacos = " "; // &str
let espacos = espacos.len(); // usize (número)
println!("Número de espaços: {}", espacos); // Imprime: 3
}
O shadowing é diferente de mut porque:
- Cria uma nova variável (pode mudar o tipo)
- O valor anterior deixa de ser acessível
- A nova variável continua imutável
Tipos Primitivos
Rust é uma linguagem estaticamente tipada — todos os tipos são conhecidos em tempo de compilação. Na maioria dos casos, o compilador consegue inferir o tipo, mas você sempre pode anotá-lo explicitamente.
Tipos Inteiros
| Tamanho | Com sinal | Sem sinal |
|---|---|---|
| 8 bits | i8 | u8 |
| 16 bits | i16 | u16 |
| 32 bits | i32 | u32 |
| 64 bits | i64 | u64 |
| 128 bits | i128 | u128 |
| Arquitetura | isize | usize |
O tipo padrão para inteiros é i32. Os tipos isize e usize têm o tamanho do ponteiro da arquitetura (32 ou 64 bits).
fn main() {
let idade: u8 = 25; // 0 a 255
let temperatura: i16 = -10; // -32768 a 32767
let populacao: u64 = 7_900_000_000;
let indice: usize = 0; // usado para indexação
// Literais em diferentes bases
let decimal = 98_222;
let hexadecimal = 0xff;
let octal = 0o77;
let binario = 0b1111_0000;
let byte = b'A'; // u8 apenas
println!("Idade: {}", idade);
println!("Temperatura: {}°C", temperatura);
println!("População: {}", populacao);
println!("Hex: {}, Oct: {}, Bin: {}", hexadecimal, octal, binario);
println!("Byte de 'A': {}", byte);
}
Tipos de Ponto Flutuante
fn main() {
let pi: f64 = 3.14159265358979; // 64 bits (padrão)
let e: f32 = 2.71828; // 32 bits
let resultado = 10.0 / 3.0; // f64 por padrão
println!("10 / 3 = {:.4}", resultado); // 4 casas decimais
println!("Pi = {}", pi);
println!("e = {}", e);
}
O tipo padrão para ponto flutuante é f64. Na maioria dos processadores modernos, f64 tem performance similar a f32, mas com muito mais precisão.
Booleanos
fn main() {
let ativo: bool = true;
let maior_de_idade = 18 >= 18; // true
println!("Ativo: {}", ativo);
println!("Maior de idade: {}", maior_de_idade);
if ativo && maior_de_idade {
println!("Usuário ativo e maior de idade.");
}
}
Caracteres
O tipo char em Rust representa um valor escalar Unicode de 4 bytes, diferente de muitas linguagens onde char é apenas 1 byte:
fn main() {
let letra: char = 'A';
let emoji = '🦀'; // Ferris, o mascote do Rust!
let acento = 'ç';
let japones = '日';
println!("{} {} {} {}", letra, emoji, acento, japones);
}
Strings: &str e String
Rust tem dois tipos principais de strings:
fn main() {
// &str - string slice (referência, imutável, tamanho fixo)
let saudacao: &str = "Olá, mundo!";
// String - string heap (mutável, tamanho dinâmico)
let mut nome = String::from("Rust");
nome.push_str(" Brasil");
println!("{}", saudacao);
println!("{}", nome);
// Convertendo entre &str e String
let s1: String = "texto".to_string();
let s2: String = String::from("texto");
let s3: &str = &s1; // String para &str
println!("{}, {}, {}", s1, s2, s3);
// Formatação
let linguagem = "Rust";
let versao = 2021;
let mensagem = format!("{} edição {}", linguagem, versao);
println!("{}", mensagem);
// Métodos úteis de String
let frase = String::from(" Olá, Rust Brasil! ");
println!("Tamanho: {}", frase.len());
println!("Maiúsculas: {}", frase.to_uppercase());
println!("Trim: '{}'", frase.trim());
println!("Contém 'Rust': {}", frase.contains("Rust"));
println!("Substituir: {}", frase.replace("Rust", "Ferrugem"));
}
Tuplas
Tuplas agrupam valores de tipos diferentes com tamanho fixo:
fn main() {
let pessoa: (String, u32, f64) = (
String::from("Maria"),
30,
1.65,
);
// Acessando por índice
println!("Nome: {}", pessoa.0);
println!("Idade: {}", pessoa.1);
println!("Altura: {}m", pessoa.2);
// Desestruturação
let (nome, idade, altura) = &pessoa;
println!("{} tem {} anos e {:.2}m", nome, idade, altura);
// Tupla unitária
let _vazio: () = (); // tipo unit, similar a void
}
Arrays
Arrays têm tamanho fixo e todos os elementos devem ser do mesmo tipo:
fn main() {
// Tipo explícito: [tipo; tamanho]
let notas: [f64; 5] = [7.5, 8.0, 9.2, 6.8, 7.0];
println!("Primeira nota: {}", notas[0]);
println!("Última nota: {}", notas[4]);
println!("Total de notas: {}", notas.len());
// Array com valor repetido
let zeros = [0; 10]; // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
println!("Primeiro zero: {}", zeros[0]);
// Iterando sobre um array
let mut soma = 0.0;
for nota in ¬as {
soma += nota;
}
let media = soma / notas.len() as f64;
println!("Média das notas: {:.1}", media);
}
Para coleções de tamanho dinâmico, use Vec<T>:
fn main() {
let mut numeros: Vec<i32> = Vec::new();
numeros.push(1);
numeros.push(2);
numeros.push(3);
// Macro vec! para criação rápida
let frutas = vec!["maçã", "banana", "laranja"];
println!("Números: {:?}", numeros);
println!("Frutas: {:?}", frutas);
}
Funções
Funções em Rust são declaradas com fn. Por convenção, usam snake_case:
fn saudacao() {
println!("Olá! Bem-vindo ao Rust!");
}
fn main() {
saudacao();
}
Parâmetros
Cada parâmetro deve ter o tipo declarado:
fn cumprimentar(nome: &str, idade: u32) {
println!("Olá, {}! Você tem {} anos.", nome, idade);
}
fn main() {
cumprimentar("Ana", 25);
cumprimentar("Carlos", 30);
}
Retorno de Valores
O tipo de retorno é declarado com ->. Em Rust, a última expressão de uma função (sem ponto e vírgula) é o valor de retorno:
fn somar(a: i32, b: i32) -> i32 {
a + b // sem ponto e vírgula = retorno implícito
}
fn calcular_area_circulo(raio: f64) -> f64 {
std::f64::consts::PI * raio * raio
}
// Também pode usar return explicitamente
fn valor_absoluto(x: i32) -> i32 {
if x < 0 {
return -x; // return explícito para saída antecipada
}
x // retorno implícito
}
fn main() {
println!("2 + 3 = {}", somar(2, 3));
println!("Área (r=5): {:.2}", calcular_area_circulo(5.0));
println!("|-7| = {}", valor_absoluto(-7));
}
Retornando Múltiplos Valores com Tuplas
fn dividir(dividendo: f64, divisor: f64) -> (f64, f64) {
let quociente = (dividendo / divisor).floor();
let resto = dividendo % divisor;
(quociente, resto)
}
fn min_max(numeros: &[i32]) -> (i32, i32) {
let mut menor = numeros[0];
let mut maior = numeros[0];
for &num in &numeros[1..] {
if num < menor {
menor = num;
}
if num > maior {
maior = num;
}
}
(menor, maior)
}
fn main() {
let (quociente, resto) = dividir(17.0, 5.0);
println!("17 / 5 = {} com resto {}", quociente, resto);
let numeros = [3, 7, 1, 9, 4, 6, 2, 8, 5];
let (menor, maior) = min_max(&numeros);
println!("Menor: {}, Maior: {}", menor, maior);
}
Expressões vs. Declarações
Em Rust, quase tudo é uma expressão (retorna um valor). Isso permite construções elegantes:
fn main() {
// if como expressão
let numero = 7;
let paridade = if numero % 2 == 0 { "par" } else { "ímpar" };
println!("{} é {}", numero, paridade);
// Bloco como expressão
let resultado = {
let base = 5;
let expoente = 2;
base * expoente // última expressão do bloco
};
println!("Resultado: {}", resultado);
// Loop com retorno
let mut contador = 0;
let valor = loop {
contador += 1;
if contador == 10 {
break contador * 2; // retorna 20
}
};
println!("Valor do loop: {}", valor);
}
Exemplo Prático: Conversor de Temperatura
Vamos juntar tudo em um programa prático:
fn celsius_para_fahrenheit(celsius: f64) -> f64 {
celsius * 9.0 / 5.0 + 32.0
}
fn fahrenheit_para_celsius(fahrenheit: f64) -> f64 {
(fahrenheit - 32.0) * 5.0 / 9.0
}
fn classificar_temperatura(celsius: f64) -> &'static str {
if celsius < 0.0 {
"Congelante"
} else if celsius < 15.0 {
"Frio"
} else if celsius < 25.0 {
"Agradável"
} else if celsius < 35.0 {
"Quente"
} else {
"Muito quente"
}
}
fn exibir_conversao(celsius: f64) {
let fahrenheit = celsius_para_fahrenheit(celsius);
let classificacao = classificar_temperatura(celsius);
println!(
"{:.1}°C = {:.1}°F - {}",
celsius, fahrenheit, classificacao
);
}
fn main() {
println!("=== Conversor de Temperatura ===\n");
let temperaturas = [-10.0, 0.0, 10.0, 20.0, 25.0, 30.0, 40.0];
for &temp in &temperaturas {
exibir_conversao(temp);
}
println!("\n=== Conversão Inversa ===\n");
let fahrenheit_valores = [32.0, 72.0, 100.0, 212.0];
for &f in &fahrenheit_valores {
let c = fahrenheit_para_celsius(f);
println!("{:.1}°F = {:.1}°C", f, c);
}
}
Saída:
=== Conversor de Temperatura ===
-10.0°C = 14.0°F - Congelante
0.0°C = 32.0°F - Congelante
10.0°C = 50.0°F - Frio
20.0°C = 68.0°F - Agradável
25.0°C = 77.0°F - Quente
30.0°C = 86.0°F - Quente
40.0°C = 104.0°F - Muito quente
=== Conversão Inversa ===
32.0°F = 0.0°C
72.0°F = 22.2°C
100.0°F = 37.8°C
212.0°F = 100.0°C
Próximos Passos
Agora que você domina variáveis, tipos e funções, é hora de aprender o conceito mais importante e único do Rust: o sistema de ownership e borrowing. Este é o coração da linguagem e o que torna Rust tão seguro.
Acesse o tutorial Ownership e Borrowing para continuar.