Capítulo 08: Slices, Strings e Vetores Contíguos na Memória

Especialização em Sistemas com Rust • Rust 2021 Edition • Ownership, Concorrência Segura e Sistemas de Alta Performance


🗺️ Mapa Conceitual do Layout de Memória: String, &str e Vec<T>

flowchart TD
    subgraph STACK_LAYOUT["Memória Stack (Estrutura de Controle em 64-bit)"]
        StrStack["String (24 bytes): ptr (0x5000), cap (16), len (8)"]
        SliceStack["&str Fat Pointer (16 bytes): ptr (0x5000), len (4)"]
        VecStack["Vec<T> (24 bytes): ptr (0x7000), cap (8), len (4)"]
    end

    subgraph HEAP_LAYOUT["Memória Heap (Buffers Contíguos em Bytes)"]
        H_Str["['R', 'U', 'S', 'T', 'C', 'O', 'R', 'E', ...] em 0x5000"]
        H_Vec["[0x01, 0x02, 0x03, 0x04, ...] em 0x7000"]
    end

    StrStack -->|"Dona da Alocação (Capacidade e Gestão)"| H_Str
    SliceStack -.->|"Visualização Zero-Copy (&s[0..4])"| H_Str
    VecStack -->|"Buffer Contíguo de Dados Dinâmicos"| H_Vec

    style STACK_LAYOUT fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
    style HEAP_LAYOUT fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
    style SliceStack fill:#fff3e0,stroke:#ff9800,stroke-width:2px
    style H_Str fill:#e8f5e9,stroke:#4caf50,stroke-width:2px

🏛️ 1. O Modelo de Dados Contíguos na Memória

Em programação de sistemas de baixo nível, a afinidade com o hardware e a hierarquia de cache de CPU (L1/L2/L3) são fatores decisivos para a velocidade de processamento. Coleções contíguas na memória garantem que os dados sejam carregados em blocos de linha de cache (Cache Lines de 64 bytes), evitando o terrível desperdício de tempo gerado por Cache Misses.

O Rust oferece três construções contíguas essenciais:

  1. Vec<T>: Um vetor dinâmico homogêneo redimensionável alocado na Heap.
  2. String: Um invólucro especializado em torno de Vec<u8> com a garantia estrita de codificação UTF-8 válida.
  3. Slices (&[T] e &str): Referências para uma sequência contígua de elementos, sem possuir o ownership da coleção.

⚙️ 2. A Dualidade: String vs &str

Compreender a diferença exata entre String e &str é o rito de passagem para todo engenheiro de software em Rust:

Dimensão Técnica String (Heap-Allocated) &str (String Slice / Fat Pointer)
Ownership Proprietária do buffer na Heap Empréstimo sem ownership
Layout na Stack 24 bytes (ptr + len + capacity) 16 bytes (ptr + len)
Mutabilidade Redimensionável (.push_str(), .clear()) Visualização imutável de bytes
Origem do Dado Alocação dinâmica em tempo de execução Pode apontar para Heap, Stack ou binário ('static)
Uso em Parâmetros Quando a função precisa assumir posse Recomendado para 99% das funções que só leem texto

Exemplo de Fatiamento de Strings (String Slicing):

let mut frase = String::from("SISTEMAS_RUST");

// Fatia que aponta para os primeiros 8 bytes da String existente
let fatia_sistemas: &str = &frase[0..8]; // "SISTEMAS"

println!("Fatia extraída sem alocar nova memória: '{}'", fatia_sistemas);

🔤 3. Por que o Rust Proíbe texto[0]? (Segurança UTF-8)

Em linguagens como C ou Python, indexar strings diretamente por número (s[0]) é comum. No entanto, o Rust rejeita a sintaxe texto[i] em tempo de compilação:

  • Cada caractere em UTF-8 pode ocupar de 1 a 4 bytes.
  • Letras latinas simples ocupam 1 byte ('A'), enquanto caracteres acentuados ocupam 2 bytes ('á') e emojis ocupam 4 bytes ('🦀').
  • Permitir indexação numérica por byte levaria a quebras acidentais de caracteres no meio de seus bytes, corrompendo o texto.

A Maneira Correta de Percorrer Strings:

let saudacao = "Olá, 🦀!";

// 1. Percorrendo caracteres Unicode individuais:
for c in saudacao.chars() {
    print!("[{}] ", c);
}
// Saída: [O] [l] [á] [,] [ ] [🦀] [!]

// 2. Percorrendo bytes brutos:
println!("\nTotal de bytes brutos na memória: {}", saudacao.len());

📊 4. Vetores Dinâmicos: Vec<T> e Gerenciamento de Capacidade

O Vec<T> gerencia internamente um ponteiro para a Heap, o comprimento atual (len) e o espaço reservado total (capacity):

// Criação com pré-alocação de capacidade (Evita realocações custosas na Heap)
let mut sensores: Vec<u32> = Vec::with_capacity(100);

for id in 1..=5 {
    sensores.push(id * 10);
}

println!("Comprimento: {} | Capacidade: {}", sensores.len(), sensores.capacity());

// Liberando espaço não utilizado para economizar RAM
sensores.shrink_to_fit();
println!("Capacidade após shrink: {}", sensores.capacity());

⚖️ 5. Fatias de Vetores e Arrays (&[T])

Uma fatia &[T] é agnóstica em relação ao local onde os dados estão alocados: ela funciona identicamente sobre vetores da Heap (Vec<T>) ou arrays estáticos da Stack ([T; N]):

fn somar_bloco_memoria(fatia: &[i32]) -> i32 {
    fatia.iter().sum()
}

fn main() {
    let array_stack: [i32; 4] = [10, 20, 30, 40];
    let vetor_heap: Vec<i32> = vec![1, 2, 3, 4, 5];

    // A mesma função atende ambos os tipos sem nenhuma cópia!
    let soma1 = somar_bloco_memoria(&array_stack[1..3]); // Fatia de array da stack
    let soma2 = somar_bloco_memoria(&vetor_heap[0..2]);   // Fatia de vetor da heap

    println!("Soma 1: {} | Soma 2: {}", soma1, soma2);
}

⚠️ 6. Armadilhas Comuns e Mensagens do Compilador

  1. Tentativa de Indexação Direta de Strings:
    let s = String::from("teste");
    let c = s[0]; // ERRO: `String` cannot be indexed by `{integer}`
    

    Correção: Use s.chars().nth(0).unwrap() ou faça um slice de bytes caso conheça os limites UTF-8: &s[0..1].

  2. Panic por Indexação Fora dos Limites UTF-8: Tentar fatiar uma string no meio de um caractere multibyte (ex: &"Olá"[0..2]) causará um panic em runtime: byte index 2 is not a char boundary.

🛡️ 7. Boas Práticas e Performance de Alta Vazão

  • Use Vec::with_capacity(n) Sempre que Souber o Tamanho Aproximado: Inserir 1.000 itens em um Vec::new() força o alocador a redimensionar e copiar o buffer na Heap diversas vezes. Com with_capacity(1000), a alocação ocorre uma única vez.
  • Assinaturas de Funções com &str e &[T]: Nunca declare parâmetros como &String ou &Vec<T>. O uso de &str e &[T] aceita muito mais tipos de entrada via Deref Coercion e torna suas bibliotecas universalmente flexíveis.

📋 8. Cheat-Sheet: Resumo Executivo

// String proprietária vs Slice
let s = String::from("Rust");
let fatia: &str = &s[0..2]; // "Ru"

// Vetor pré-alocado
let mut v = Vec::with_capacity(64);
v.push(10);

// Assinatura canônica e idiomática
fn processar(texto: &str, lista: &[u8]) { /* Zero Copy */ }