Variáveis, Tipos Primitivos e Funções em Rust

Aprenda sobre variáveis imutáveis e mutáveis, tipos primitivos como i32, f64, bool e String, e como criar funções em Rust.

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:

  • const exige anotação de tipo
  • const só aceita expressões que podem ser calculadas em tempo de compilação
  • const pode 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:

  1. Cria uma nova variável (pode mudar o tipo)
  2. O valor anterior deixa de ser acessível
  3. 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

TamanhoCom sinalSem sinal
8 bitsi8u8
16 bitsi16u16
32 bitsi32u32
64 bitsi64u64
128 bitsi128u128
Arquiteturaisizeusize

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 &notas {
        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.