Perguntas de entrevista sobre Rust
Estas são perguntas que aparecem o tempo todo em entrevistas de Rust, de vagas júnior até sênior. Cada resposta é curta o bastante para ser dita em voz alta e tem um link para a lição que explica o assunto a fundo.
Não decore as respostas palavra por palavra. Quem entrevista percebe, e geralmente faz uma pergunta de acompanhamento para ver se você entendeu de verdade. Use esta página para conferir o que você já sabe e depois vá até a lição indicada em tudo que parecer meio frágil. Se você consegue explicar uma resposta com suas próprias palavras e escrever um exemplo pequeno, está pronto.
O básico
Seção intitulada “O básico”P: O que é o Cargo?
Seção intitulada “P: O que é o Cargo?”O Cargo é a ferramenta oficial de build e o gerenciador de pacotes do Rust. Ele já vem junto com o Rust. Com uma única ferramenta você consegue:
- criar um projeto (
cargo new), - compilar e rodar o projeto (
cargo build,cargo run), - rodar os testes (
cargo test), - adicionar bibliotecas, chamadas de crates, do crates.io (
cargo add serde).
As configurações e dependências de um projeto ficam no Cargo.toml. As versões exatas ficam travadas no Cargo.lock, então os builds podem ser repetidos com o mesmo resultado. Um ponto extra que conta a seu favor: outras linguagens muitas vezes precisam de várias ferramentas para isso, mas em Rust todo mundo usa o Cargo, então qualquer projeto parece familiar.
Saiba mais: Cargo e crates, Começando com Rust.
P: Qual é a diferença entre shadowing e mut?
Seção intitulada “P: Qual é a diferença entre shadowing e mut?”mut deixa você alterar o valor da mesma variável. O tipo precisa continuar o mesmo.
Shadowing (sombreamento) é escrever let de novo com o mesmo nome. Isso cria uma variável nova, que esconde a antiga. A nova pode até ter um tipo diferente.
fn main() { let spaces = " "; let spaces = spaces.len(); // shadowing: uma variável nova, agora um número println!("{spaces}");
let mut count = 1; count += 1; // mut: a mesma variável recebe um valor novo println!("{count}");}Saída:
32O shadowing é prático para transformar um valor passo a passo (como converter um texto em número) sem inventar nomes como input_str e input_num. E a variável final continua imutável.
Saiba mais: Variáveis em Rust.
P: Qual é a diferença entre uma macro e uma função?
Seção intitulada “P: Qual é a diferença entre uma macro e uma função?”Uma função é chamada em tempo de execução, com um número fixo de argumentos com tipo. Uma macro roda em tempo de compilação: ela recebe código e escreve mais código antes de o compilador verificar tudo. Dá para reconhecer uma macro pelo !, como em println!, vec! e format!.
Macros fazem coisas que funções não fazem, como receber um número variável de argumentos (println!("{} {}", a, b)) ou verificar uma string de formatação em tempo de compilação. As macros do Rust também trabalham com a estrutura do código, e não com texto puro, então elas evitam os bugs clássicos das macros de C:
macro_rules! square { ($x:expr) => { $x * $x };}
fn main() { // Em C, uma macro baseada em texto daria 2 + 1 * 2 + 1 = 5. println!("{}", square!(2 + 1));}Saída:
9O lado ruim, sendo sincero: macros são mais difíceis de ler, escrever e depurar. Use uma função quando uma função der conta do recado.
Saiba mais: Macros em Rust.
P: O que significa “zero-cost abstractions”?
Seção intitulada “P: O que significa “zero-cost abstractions”?”Zero-cost abstractions (abstrações de custo zero) quer dizer que recursos de alto nível não deveriam deixar seu programa mais lento do que o código de baixo nível que você escreveria à mão. “Você não paga pelo que não usa, e o que você usa, não conseguiria escrever melhor sozinho.”
Por exemplo, uma cadeia de iteradores como .iter().filter(...).map(...).sum() costuma virar o mesmo código de máquina de um laço escrito à mão. Os genéricos são outro exemplo: o compilador cria uma cópia especializada para cada tipo que você usa (isso se chama monomorfização), então não há nenhuma busca em tempo de execução.
Seja sincero sobre a parte do “custo”: é custo zero em tempo de execução. Você ainda paga em tempo de compilação, e os genéricos podem deixar o arquivo do programa maior.
Saiba mais: Iteradores em Rust, Genéricos em Rust.
P: Os iteradores do Rust são preguiçosos (lazy)?
Seção intitulada “P: Os iteradores do Rust são preguiçosos (lazy)?”Sim. Métodos como .map() e .filter() não fazem trabalho nenhum sozinhos. Eles montam uma descrição do trabalho. Nada acontece até alguém consumir o iterador, como .collect(), .sum() ou um laço for.
fn main() { let numbers = vec![1, 2, 3]; let doubled = numbers.iter().map(|n| { println!("dobrando {n}"); n * 2 });
println!("Nada rodou ainda."); let result: Vec<i32> = doubled.collect(); println!("{:?}", result);}Saída:
Nada rodou ainda.dobrando 1dobrando 2dobrando 3[2, 4, 6]É também por isso que o compilador avisa se você escrever numbers.iter().map(...); e nunca usar o resultado: isso não faria nada.
Saiba mais: Iteradores em Rust.
Ownership (posse) e borrowing (empréstimo)
Seção intitulada “Ownership (posse) e borrowing (empréstimo)”P: Quais são as regras de ownership?
Seção intitulada “P: Quais são as regras de ownership?”São três:
- Todo valor em Rust tem um dono (owner), que é uma variável.
- Só pode existir um dono por vez.
- Quando o dono sai de escopo, o valor é descartado (dropped), ou seja, a memória dele é liberada.
Essas regras deixam o Rust liberar a memória exatamente na hora certa, sem garbage collector (coletor de lixo) e sem você chamar free à mão. O compilador verifica as regras, então erros como “use after free” (usar depois de liberar) ou “double free” (liberar duas vezes) não acontecem em código seguro.
Saiba mais: Ownership (posse), Escopo em Rust.
P: Qual é a diferença entre move, copy e clone?
Seção intitulada “P: Qual é a diferença entre move, copy e clone?”- Move: atribuir ou passar um valor transfere o ownership. A variável antiga não pode mais ser usada. Esse é o comportamento padrão para tipos como
StringeVec. Nada é copiado, a não ser um pequeno ponteiro, o tamanho e a capacidade. - Copy: para tipos pequenos e simples, como
i32,f64,boolechar, a atribuição faz uma cópia barata, bit a bit, e as duas variáveis continuam utilizáveis. - Clone:
.clone()é uma cópia profunda explícita. Em umaString, ele copia todo o texto para uma memória nova. Isso pode sair caro, e é por isso que o Rust obriga você a escrever.
fn main() { let x = 5; let y = x; // Copy: x continua utilizável println!("{x} {y}");
let a = String::from("oi"); let b = a.clone(); // Clone: uma cópia completa do texto let c = a; // Move: `a` não pode ser usada depois desta linha println!("{b} {c}");}Saída:
5 5oi oiSaiba mais: Ownership (posse).
P: O que é o trait Copy e quais tipos o têm?
Seção intitulada “P: O que é o trait Copy e quais tipos o têm?”Copy é um trait marcador que diz “este tipo pode ser duplicado só copiando os bits dele”. Tipos que são Copy são copiados em vez de movidos na atribuição.
Inteiros, números de ponto flutuante, bool, char, referências compartilhadas (&T) e tuplas ou arrays formados só por tipos Copy são todos Copy. String, Vec e Box não são, porque são donos de uma memória em outro lugar, e copiar o ponteiro criaria dois donos.
Você pode derivá-lo para seus próprios tipos, mas só se todos os campos forem Copy:
#[derive(Clone, Copy)]struct User { name: String, age: u32,}error[E0204]: the trait `Copy` cannot be implemented for this type --> src/main.rs:2:8 |1 | #[derive(Clone, Copy)] | ---- in this derive macro expansion2 | struct User { | ^^^^3 | name: String, | ------------ this field does not implement `Copy`O compilador diz que o trait Copy não pode ser implementado para esse tipo, porque o campo name (uma String) não implementa Copy.
Copy sempre exige Clone também. E um tipo que implementa Drop não pode ser Copy.
Saiba mais: Traits comuns, Ownership (posse).
P: Qual é a diferença entre String e &str?
Seção intitulada “P: Qual é a diferença entre String e &str?”Stringé um texto com dono (owned), que pode crescer e fica guardado na heap. Você pode alterá-lo, por exemplo compush_str.&str(um “string slice”) é uma visão emprestada de algum texto. É só um ponteiro e um tamanho. Literais como"olá"são&str.
Uma boa regra: funções que só leem texto devem receber &str. Assim elas aceitam os dois, porque uma &String vira &str automaticamente.
fn greet(name: &str) { println!("Olá, {name}!");}
fn main() { let owned: String = String::from("Ana"); let literal: &str = "Ben";
greet(&owned); // &String funciona onde se espera &str greet(literal);
let mut full = owned; full.push_str(" Silva"); // só uma String pode crescer greet(&full);}Saída:
Olá, Ana!Olá, Ben!Olá, Ana Silva!Saiba mais: Strings em Rust, Slices em Rust.
P: Quais são as regras de borrowing?
Seção intitulada “P: Quais são as regras de borrowing?”Em qualquer momento, para um mesmo valor, você pode ter ou:
- qualquer número de referências compartilhadas (
&T), ou - exatamente uma referência mutável (
&mut T),
mas nunca as duas coisas ao mesmo tempo. E toda referência precisa sempre apontar para dados válidos.
Por quê? Se alguém pudesse alterar os dados enquanto outra pessoa está lendo, você teria bugs como um vetor mudando a memória de lugar enquanto você guarda um ponteiro para dentro dele. Com threads, isso é exatamente o que chamamos de data race (condição de corrida nos dados). O Rust impede isso em tempo de compilação. Desde a edição 2018, o compilador é esperto sobre quando um empréstimo termina (isso se chama non-lexical lifetimes): um empréstimo só dura até o último uso, e não até o fim do bloco.
Saiba mais: Borrowing (empréstimo).
P: O que são lifetimes?
Seção intitulada “P: O que são lifetimes?”Um lifetime (tempo de vida) é o nome que o compilador dá para “por quanto tempo uma referência é válida”. Toda referência tem um. Na maior parte do tempo o compilador descobre sozinho (isso se chama lifetime elision, ou elisão de lifetimes), então você nem os vê.
Você só precisa escrevê-los quando o compilador não consegue saber como a referência de saída se relaciona com as de entrada, como em uma função que devolve uma de duas strings emprestadas:
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str { if a.len() >= b.len() { a } else { b }}
fn main() { let first = String::from("borrow checker"); let second = String::from("lifetimes"); println!("{}", longest(&first, &second));}Saída:
borrow checker(As strings ficaram em inglês porque o resultado depende do tamanho delas com .len(), e letras com acento ocupam mais bytes.)
Um ponto importante para dizer na entrevista: as anotações de lifetime não mudam quanto tempo nada vive. Elas só descrevem relações, para que o compilador possa verificar que nenhuma referência vive mais do que os dados para os quais aponta.
Saiba mais: Lifetimes em Rust.
Tipos e traits
Seção intitulada “Tipos e traits”P: Rust não tem null. O que ele usa no lugar?
Seção intitulada “P: Rust não tem null. O que ele usa no lugar?”Option<T>, um enum com duas variantes: Some(valor) ou None. Como ele é um tipo diferente de T, o compilador obriga você a tratar o caso None antes de usar o valor. Não tem como esquecer uma verificação de null, porque o código nem compila.
fn find_user(id: u32) -> Option<&'static str> { match id { 1 => Some("Ana"), 2 => Some("Ben"), _ => None, }}
fn main() { for id in [1, 3] { match find_user(id) { Some(name) => println!("Encontrei {name}"), None => println!("Nenhum usuário com id {id}"), } }}Saída:
Encontrei AnaNenhum usuário com id 3Um detalhe extra legal: Option<&T> e Option<Box<T>> não ocupam nenhuma memória a mais do que um ponteiro comum. Por dentro, o Rust usa o padrão de bits do “null” para representar None, então a segurança aqui não custa nada.
Saiba mais: Option em Rust, If Let / While Let em Rust.
P: O que é um trait? É a mesma coisa que uma interface?
Seção intitulada “P: O que é um trait? É a mesma coisa que uma interface?”Um trait define um conjunto de métodos que um tipo pode implementar. Ele é parecido com uma interface em Java, C# ou Go, e esse é um bom jeito de começar a resposta. Depois, cite as diferenças:
- Traits podem ter implementações padrão de métodos.
- Você pode implementar um trait para um tipo depois que o tipo foi escrito, até para tipos que não são seus, como implementar seu próprio trait para
i32. (A “orphan rule”, ou regra do órfão, diz que ou o trait ou o tipo precisa ser seu.) - Traits são usados como restrições em genéricos (
T: Display), resolvidas em tempo de compilação, e também como trait objects (dyn Display) em tempo de execução. - Rust não tem herança de classes. Traits mais composição fazem esse papel.
Saiba mais: Traits em Rust, Traits comuns.
P: Qual é a diferença entre genéricos e trait objects?
Seção intitulada “P: Qual é a diferença entre genéricos e trait objects?”Os dois deixam uma função trabalhar com vários tipos. A diferença é quando o tipo é decidido:
- Genéricos (
fn f<T: Speak>(x: &T)) usam despacho estático (static dispatch). O compilador cria uma cópia separada da função para cada tipo. As chamadas são tão rápidas quanto chamadas de função normais, mas o programa pode ficar maior. - Trait objects (
&dyn Speak,Box<dyn Speak>) usam despacho dinâmico (dynamic dispatch). Existe uma única cópia da função, e o método certo é procurado em tempo de execução por meio de uma tabela (a “vtable”). É um pouquinho mais lento, mas deixa você misturar tipos diferentes em uma mesma coleção.
trait Speak { fn speak(&self) -> String;}
struct Dog;struct Cat;
impl Speak for Dog { fn speak(&self) -> String { String::from("Au au") }}
impl Speak for Cat { fn speak(&self) -> String { String::from("Miau") }}
fn speak_static<T: Speak>(animal: &T) -> String { animal.speak()}
fn speak_dynamic(animal: &dyn Speak) -> String { animal.speak()}
fn main() { println!("{}", speak_static(&Dog)); println!("{}", speak_dynamic(&Cat));
// Só trait objects conseguem misturar tipos em um único Vec: let animals: Vec<Box<dyn Speak>> = vec![Box::new(Dog), Box::new(Cat)]; for animal in &animals { println!("{}", animal.speak()); }}Saída:
Au auMiauAu auMiauRegra prática: comece com genéricos. Use trait objects quando precisar de uma coleção com tipos diferentes, ou quando quiser manter o tempo de compilação e o tamanho do binário menores.
Saiba mais: Genéricos em Rust, Trait objects (dyn).
P: Qual é a diferença entre Box<dyn Trait> e impl Trait?
Seção intitulada “P: Qual é a diferença entre Box<dyn Trait> e impl Trait?”Os dois permitem dizer “algum tipo que implementa este trait” sem dar nome a ele. A diferença está em quantos tipos concretos são permitidos:
impl Traitsignifica um tipo específico, que o compilador conhece mas não obriga você a escrever. É despacho estático (static dispatch): sem alocação no heap, sem vtable, tão rápido quanto escrever o nome do tipo. Na posição de argumento (fn f(x: impl Display)), é só um jeito mais curto de escrever um genérico. Na posição de retorno, é como você devolve coisas com tipos longos ou impossíveis de nomear, como closures e cadeias de iteradores.Box<dyn Trait>é um trait object no heap. O tipo real pode ser diferente a cada vez, decidido em tempo de execução, e o método é encontrado por uma vtable. Isso custa uma alocação e uma chamada indireta, mas é o único jeito de devolver um entre vários tipos, ou de guardá-los num mesmoVec.
use std::fmt::Display;
// impl Trait: um único tipo escondido, fixado na compilação.fn evens(limit: u32) -> impl Iterator<Item = u32> { (0..limit).filter(|n| n % 2 == 0)}
// Box<dyn Trait>: o tipo real pode mudar em tempo de execução.fn label(as_number: bool) -> Box<dyn Display> { if as_number { Box::new(42) } else { Box::new("quarenta e dois") }}
fn main() { let v: Vec<u32> = evens(10).collect(); println!("{v:?}"); println!("{}", label(true)); println!("{}", label(false));}Saída:
[0, 2, 4, 6, 8]42quarenta e doisUm bom detalhe para comentar: se você mudar label para devolver impl Display e tirar os Box, o código não compila. O if devolve um i32 e o else devolve um &str, e impl Trait precisa ser um único tipo. Regra prática: use impl Trait por padrão e passe para Box<dyn Trait> quando você realmente precisar escolher entre tipos em tempo de execução.
Saiba mais: Trait objects (dyn), Genéricos em Rust.
P: Por que o match precisa ser exaustivo?
Seção intitulada “P: Por que o match precisa ser exaustivo?”Um match precisa tratar todos os valores possíveis, senão o código não compila. Isso significa que, quando você adiciona uma variante nova a um enum, o compilador mostra todos os lugares que precisam ser atualizados. Em outras linguagens, um case esquecido em um switch simplesmente não faz nada, sem avisar.
Você pode usar _ para pegar todo o resto, mas cuidado: esse “pega tudo” também esconde as variantes novas que você adicionar depois, e você perde essa rede de segurança.
Saiba mais: Match em Rust, Pattern matching, Enums em Rust.
Tratamento de erros
Seção intitulada “Tratamento de erros”P: Como o Rust trata erros sem exceções?
Seção intitulada “P: Como o Rust trata erros sem exceções?”Erros recuperáveis são valores normais do tipo Result<T, E>: ou Ok(valor) ou Err(erro). Uma função que pode falhar diz isso no tipo de retorno, e quem chama precisa lidar com isso. Nada sobe pela pilha de forma invisível, como acontece com uma exceção.
As vantagens: você vê pela assinatura da função que ela pode falhar, e o compilador avisa se você ignorar um Result. O custo é digitar um pouco mais, e o operador ? elimina quase todo esse trabalho.
Saiba mais: Result em Rust.
P: Quando usar panic! em vez de devolver um Result?
Seção intitulada “P: Quando usar panic! em vez de devolver um Result?”- Devolva um
Resultquando a falha é esperada e quem chama poderia razoavelmente lidar com ela: um arquivo que não existe, uma entrada inválida do usuário, um timeout de rede. - Use panic quando algo é um bug, ou seja, o programa chegou a um estado que “deveria ser impossível” e continuar seria errado. Acessar um índice depois do fim de um vetor causa panic por esse motivo.
Panics também são aceitáveis em testes, exemplos e protótipos rápidos. Em uma biblioteca, evite entrar em panic por causa de uma entrada que quem chama controla. Devolva um erro e deixe essa pessoa decidir.
Saiba mais: Panic em Rust, Result em Rust.
P: Qual é a diferença entre unwrap, expect e ??
Seção intitulada “P: Qual é a diferença entre unwrap, expect e ??”unwrap()entrega o valor de dentro doOk/Some, ou entra em panic com uma mensagem genérica.expect("msg")faz o mesmo, mas entra em panic com a sua mensagem. Use para explicar por que você acredita que aquilo não pode falhar.?não entra em panic. ComErr/None, ele devolve o erro da função atual, para que quem chamou possa tratá-lo. A própria função precisa devolver umResultou umOption.
use std::num::ParseIntError;
fn double(text: &str) -> Result<i32, ParseIntError> { let n: i32 = text.parse()?; // se der erro, devolve para quem chamou Ok(n * 2)}
fn main() { println!("{:?}", double("21")); println!("{:?}", double("abc"));
let n: i32 = "7".parse().expect("um número fixo no código sempre deve ser convertido"); println!("{n}");}Saída:
Ok(42)Err(ParseIntError { kind: InvalidDigit })7InvalidDigit (dígito inválido) é o nome que o Rust dá para o erro: "abc" tem caracteres que não são dígitos.
Em código de produção, prefira o ?. Use expect quando uma falha seria um bug, e deixe o unwrap para testes e experimentos rápidos.
Saiba mais: Result em Rust, Option em Rust.
P: Como criar seus próprios tipos de erro?
Seção intitulada “P: Como criar seus próprios tipos de erro?”Normalmente com um enum que lista as formas como seu código pode falhar, mais as implementações de std::fmt::Display e std::error::Error. Implementar From para outros tipos de erro permite que o ? faça a conversão automaticamente.
Na prática, muitos projetos usam crates auxiliares: thiserror para definir tipos de erro em bibliotecas e anyhow para aplicações que só precisam repassar os erros com contexto. Para programas rápidos, Box<dyn Error> consegue guardar qualquer erro.
Saiba mais: Erros personalizados.
P: Quando usar anyhow e quando usar thiserror?
Seção intitulada “P: Quando usar anyhow e quando usar thiserror?”Eles resolvem as duas metades do mesmo problema, então a resposta sincera é “muitas vezes, os dois”:
thiserroré para bibliotecas, ou para qualquer código cujos chamadores precisem reagir a erros específicos. Você escreve o seu próprio enum de erro e o#[derive(Error)]preenche o código repetitivo deDisplayeError. Quem chama recebe um tipo de verdade, em que dá para fazermatch, comoConfigError::MissingField.anyhowé para aplicações, em que a maioria dos erros só precisa ser informada, e não tratada um por um. Toda função devolveanyhow::Result<T>, o?aceita quase qualquer tipo de erro, e.context("ao ler a configuração")acrescenta uma explicação amigável por cima. A desvantagem: quem chama já não consegue fazermatchno erro com facilidade.
Uma combinação comum é thiserror dentro dos crates de biblioteca e anyhow no main e no resto do binário. Nenhum dos dois crates é mágico. Ambos só eliminam código repetitivo em cima do trait padrão std::error::Error, então você sempre pode escrever a mesma coisa à mão.
Saiba mais: anyhow e thiserror, Erros personalizados.
Memória e smart pointers
Seção intitulada “Memória e smart pointers”P: O Rust tem garbage collector?
Seção intitulada “P: O Rust tem garbage collector?”Não. A memória é liberada quando o dono dela sai de escopo, em um ponto que o compilador conhece de antemão (isso se chama RAII, e o método de limpeza é o Drop). Isso dá um desempenho previsível, sem pausas de GC.
Quando você realmente precisa de ownership compartilhado, você escolhe usar contagem de referências (Rc ou Arc), que libera o valor quando o último dono deixa de existir.
Saiba mais: Ownership (posse), Ponteiros inteligentes.
P: Quando usar Box, Rc, Arc e RefCell?
Seção intitulada “P: Quando usar Box, Rc, Arc e RefCell?”Box<T>: um dono, valor na heap. Use para tipos recursivos (como uma árvore ou lista ligada), valores grandes ou trait objects (Box<dyn Trait>).Rc<T>: vários donos em uma única thread. Ele conta as referências e libera o valor quando a contagem chega a zero. Os dados são só de leitura.Arc<T>: como oRc, mas a contagem é atômica, então é seguro compartilhar entre threads. Um pouco mais lento que oRc.RefCell<T>: deixa você alterar dados por meio de uma referência compartilhada (“mutabilidade interior”). As regras de borrowing são verificadas em tempo de execução em vez de em tempo de compilação, e quebrá-las causa panic.
Eles se combinam. Rc<RefCell<T>> é “compartilhado e alterável, em uma thread”. Arc<Mutex<T>> é a versão segura para threads.
use std::cell::RefCell;use std::rc::Rc;
fn main() { let shared = Rc::new(RefCell::new(vec![1, 2])); let other_owner = Rc::clone(&shared);
other_owner.borrow_mut().push(3);
println!("{:?}", shared.borrow()); println!("donos: {}", Rc::strong_count(&shared));}Saída:
[1, 2, 3]donos: 2Saiba mais: Ponteiros inteligentes.
P: Dá para vazar memória em Rust seguro?
Seção intitulada “P: Dá para vazar memória em Rust seguro?”Sim, e é honesto admitir isso. O Rust garante segurança de memória (nada de use-after-free, nada de ponteiros soltos, nada de data races), mas vazar memória não é considerado inseguro. Dois jeitos comuns:
- Um ciclo de
Rc: dois valores que apontam um para o outro comRcnunca chegam a uma contagem zero. A solução é fazer uma das direções ser uma referênciaWeak. - Funções como
Box::leakestd::mem::forget, que vazam de propósito.
No código do dia a dia, vazamentos são raros, porque o ownership libera quase tudo automaticamente.
Saiba mais: Ponteiros inteligentes.
P: O que é unsafe e quando ele é usado?
Seção intitulada “P: O que é unsafe e quando ele é usado?”unsafe marca um bloco (ou uma função) onde você pode fazer cinco coisas extras que o compilador não consegue verificar, como desreferenciar ponteiros brutos (raw pointers), chamar funções unsafe (incluindo código C por FFI) ou acessar variáveis estáticas mutáveis. Lá dentro, você promete que as regras estão sendo respeitadas.
Importante: unsafe não desliga o borrow checker nem as outras verificações. Ele só libera essas poucas operações extras.
Ele é usado para conversar com bibliotecas em C, escrever estruturas de dados de baixo nível e espremer desempenho onde o compilador não consegue provar que algo é seguro. A própria biblioteca padrão usa unsafe por dentro de APIs seguras como Vec. Boa prática: mantenha os blocos unsafe pequenos, envolva-os em uma API segura e escreva um comentário // SAFETY: explicando por que está correto. A maioria do código de aplicação nunca precisa disso.
Saiba mais: Unsafe em Rust.
Concorrência e async
Seção intitulada “Concorrência e async”P: O que significam Send e Sync?
Seção intitulada “P: O que significam Send e Sync?”São traits marcadores que o compilador usa para manter as threads seguras:
Send: um valor desse tipo pode ser movido para outra thread.Sync: um valor desse tipo pode ser compartilhado por referência entre threads (&TéSend).
A maioria dos tipos é as duas coisas, e o compilador implementa esses traits automaticamente. A exceção famosa é o Rc, cuja contagem não é segura para threads. Tente mandar um para uma thread, e o código não compila:
use std::rc::Rc;use std::thread;
fn main() { let data = Rc::new(5); let handle = thread::spawn(move || { println!("{data}"); }); handle.join().unwrap();}error[E0277]: `Rc<i32>` cannot be sent between threads safely ... = help: within `{closure@src/main.rs:6:32: 6:39}`, the trait `Send` is not implemented for `Rc<i32>`O erro diz que um Rc<i32> não pode ser enviado entre threads com segurança, porque ele não implementa Send.
Troque Rc por Arc e funciona. Isso é boa parte do que as pessoas querem dizer com a “fearless concurrency” (concorrência sem medo) do Rust: erros de segurança entre threads viram erros de compilação.
Saiba mais: Concorrência em Rust.
P: Como compartilhar dados alteráveis entre threads?
Seção intitulada “P: Como compartilhar dados alteráveis entre threads?”A resposta clássica é Arc<Mutex<T>>. O Arc deixa várias threads serem donas do valor. O Mutex garante que só uma thread por vez pode alterá-lo: você chama .lock() e recebe uma guarda (guard) que destrava sozinha quando sai de escopo.
use std::sync::{Arc, Mutex};use std::thread;
fn main() { let counter = Arc::new(Mutex::new(0)); let mut handles = Vec::new();
for _ in 0..4 { let counter = Arc::clone(&counter); handles.push(thread::spawn(move || { for _ in 0..1000 { *counter.lock().unwrap() += 1; } })); }
for handle in handles { handle.join().unwrap(); }
println!("Total: {}", *counter.lock().unwrap());}Saída:
Total: 4000Cite também a alternativa: em vez de compartilhar memória, mande mensagens entre as threads com canais (std::sync::mpsc). E, para contadores simples, tipos atômicos como AtomicUsize dispensam o lock.
Saiba mais: Concorrência em Rust.
P: Como os canais se comparam com estado compartilhado usando Mutex?
Seção intitulada “P: Como os canais se comparam com estado compartilhado usando Mutex?”São dois estilos de dividir trabalho entre threads:
- Canais (
std::sync::mpsc) passam mensagens. Um valor é movido para dentro do canal e sai do outro lado, então só uma thread é dona dele por vez. Eles combinam com pipelines e grupos de workers: “os workers produzem resultados, uma thread junta tudo”. Costumam ser mais fáceis de entender, porque duas threads nunca mexem nos mesmos dados. - Estado compartilhado (
Arc<Mutex<T>>) significa que várias threads usam os mesmos dados, revezando por meio de um lock. Combina quando muitas threads precisam ler e atualizar uma mesma estrutura, como um cache ou uma tabela de contadores. Os riscos são deadlocks e lentidão quando as threads ficam esperando o lock.
Aqui está o contador da resposta anterior, reescrito com um canal. Cada worker conta sozinho, sem lock, e envia o seu resultado:
use std::sync::mpsc;use std::thread;
fn main() { let (tx, rx) = mpsc::channel();
for _ in 0..4 { let tx = tx.clone(); thread::spawn(move || { let mut count = 0; for _ in 0..1000 { count += 1; } tx.send(count).unwrap(); }); } drop(tx); // descarte o sender original, senão o rx.iter() nunca termina
let total: i32 = rx.iter().sum(); println!("Total: {total}");}Saída:
Total: 4000A linha drop(tx) é um ótimo detalhe para comentar: o rx.iter() fica esperando até todos os senders sumirem, então esquecer dela faz o programa travar. Nos dois estilos, o Rust verifica a segurança entre threads em tempo de compilação. A escolha é sobre qual design fica mais claro para o seu problema, e muitos programas usam os dois.
Saiba mais: Canais (channels), Concorrência em Rust.
P: O Rust impede todos os bugs de concorrência?
Seção intitulada “P: O Rust impede todos os bugs de concorrência?”Não, e dizer isso mostra que você entende de verdade. O Rust seguro impede data races (duas threads mexendo na mesma memória ao mesmo tempo, com pelo menos uma escrevendo). Ele não impede:
- Deadlocks, por exemplo duas threads, cada uma esperando um lock que a outra está segurando.
- Condições de corrida na sua lógica, por exemplo verificar um valor e agir com base nele depois que outra thread já o alterou.
Então o Rust elimina uma classe inteira de bugs difíceis, mas você ainda precisa planejar seus locks com cuidado.
Saiba mais: Concorrência em Rust.
P: Qual é a diferença entre async e threads?
Seção intitulada “P: Qual é a diferença entre async e threads?”- Threads são gerenciadas pelo sistema operacional. Cada uma tem sua própria pilha, e o sistema operacional alterna entre elas. São ótimas para trabalho pesado de CPU. Mas milhares de threads gastam muita memória.
- Tarefas async são bem mais leves. Uma
async fndevolve umFuture, e um runtime (normalmente o Tokio) roda muitos futures em poucas threads. Quando uma tarefa está esperando (pela rede, por um timer, por um arquivo), o.awaitdeixa outras tarefas rodarem. É ideal para programas que esperam muito, como servidores web atendendo milhares de conexões.
use tokio::time::{sleep, Duration};
async fn fetch(name: &str, ms: u64) -> String { sleep(Duration::from_millis(ms)).await; // finge que está esperando a rede format!("{name} terminou")}
#[tokio::main]async fn main() { // As duas rodam ao mesmo tempo, então isso leva uns 200ms, e não 300ms. let (a, b) = tokio::join!(fetch("primeira", 200), fetch("segunda", 100)); println!("{a}"); println!("{b}");}Saída:
primeira terminousegunda terminouDois detalhes sinceros que quem entrevista gosta de ouvir: os futures do Rust são preguiçosos e não fazem nada até receberem .await, e a biblioteca padrão não inclui um runtime, então você escolhe um. Além disso, código bloqueante (como um cálculo demorado ou std::thread::sleep) dentro de uma tarefa async bloqueia as outras tarefas daquela thread.
Saiba mais: Async/Await em Rust, Concorrência em Rust.
Dicas para responder na entrevista
Seção intitulada “Dicas para responder na entrevista”- Comece simples e depois aprofunde. Dê primeiro uma resposta de uma frase (“
Rcé um ponteiro com contagem de referências para ownership compartilhado em uma thread”). Depois acrescente detalhes se a pessoa parecer interessada. - Explique o porquê. As regras do Rust existem para impedir bugs específicos. Dizer “as regras de borrowing impedem data races e ponteiros soltos” é mais forte do que só recitar as regras.
- Use um exemplo pequeno. Escrever três linhas de código, ou descrevê-las, muitas vezes explica mais do que um discurso longo.
- Seja sincero sobre os prós e contras. Rust tem custos: compilação mais lenta, uma curva de aprendizado, mais digitação no tratamento de erros. Admitir isso mostra experiência, não fraqueza.
- Diga “não sei” quando não souber, e depois raciocine em voz alta. “Nunca usei
Pindiretamente, mas sei que tem a ver com futures que não podem mudar de lugar na memória” é uma ótima resposta. - Tenha uma história pronta. É comum perguntarem: “Conte de uma vez em que o borrow checker barrou você.” Pense em um exemplo real do seu próprio código e no que você aprendeu com ele.
- Pratique escrevendo código. A página de Exercícios de Rust e os projetos Jogo de adivinhação e Lista de tarefas são bons aquecimentos.
Teste o que aprendeu
Seção intitulada “Teste o que aprendeu”Teste o que aprendeu
1. Qual smart pointer você usaria para compartilhar dados só de leitura entre threads?
2. O que o operador ? faz quando encontra um Err?
3. Qual destes problemas o Rust seguro impede em tempo de compilação?
4. As anotações de lifetime mudam quanto tempo um valor vive?
Pontuação: 0 / 4
Para saber mais
Seção intitulada “Para saber mais”Todos os links abaixo estão em inglês.
- The Rust Programming Language (o Rust Book): o livro oficial. A maioria das respostas desta página vem direto dos capítulos dele sobre ownership, traits, erros e concorrência.
- Rust by Example: exemplos curtos que você pode rodar, ótimos para relembrar a sintaxe logo antes de uma entrevista.
- The Rust Reference: as regras exatas da linguagem, para quando o entrevistador perguntar “mas exatamente quando isso acontece?”
- The Rustonomicon: o guia de Rust
unsafe, com mais contexto sobreSend,Synce layout de memória para perguntas de nível sênior. - Asynchronous Programming in Rust: como futures,
async/.awaite runtimes se encaixam, no livro oficial de async do projeto Rust.