# Preguntas de Entrevista sobre Rust: Guia Completa 2026 > Las 25 preguntas mas comunes en entrevistas sobre Rust. Ownership, borrowing, lifetimes, traits, async y concurrencia con respuestas detalladas y ejemplos de codigo. - Published: 2026-01-26 - Updated: 2026-04-01 - Author: Anthony Fillion-Maillet - Tags: rust, interview, systems programming, ownership, technical interview - Reading time: 22 min --- Las entrevistas sobre Rust evaluan la comprension del sistema unico de ownership, la gestion de memoria sin recolector de basura y la capacidad de escribir codigo concurrente seguro. Esta guia cubre las preguntas esenciales, desde los fundamentos del ownership hasta patrones avanzados de async y concurrencia. > **Consejo para la entrevista** > > Los entrevistadores valoran las explicaciones que demuestran comprension de las garantias de seguridad de memoria de Rust. Explicar como el compilador previene errores en tiempo de compilacion marca la diferencia. ## Ownership y Borrowing ### Pregunta 1: Explicar el sistema de ownership de Rust El ownership es el concepto central de Rust que permite la gestion de memoria sin recolector de basura, garantizando seguridad de memoria en tiempo de compilacion. ```rust // ownership_basics.rs // The three fundamental rules of ownership fn main() { // Rule 1: Each value has a single owner let s1 = String::from("hello"); // s1 is the owner // Rule 2: Only one variable can own a value at a time let s2 = s1; // s1 is MOVED to s2 // println!("{}", s1); // ERROR: s1 is no longer valid println!("{}", s2); // OK: s2 is now the owner // Rule 3: When the owner goes out of scope, the value is dropped { let s3 = String::from("world"); // s3 is valid here } // s3 goes out of scope, memory is automatically freed // Copy types: simple types are copied, not moved let x = 5; let y = x; // x is COPIED, not moved println!("x = {}, y = {}", x, y); // Both are valid } // Move in action with functions fn take_ownership(s: String) { // s takes ownership of the String println!("{}", s); } // s is dropped here, memory freed fn makes_copy(i: i32) { // i is a copy of the argument println!("{}", i); } // i goes out of scope, nothing special (Copy type) fn ownership_with_functions() { let s = String::from("hello"); take_ownership(s); // s is moved into the function // println!("{}", s); // ERROR: s is no longer valid let x = 5; makes_copy(x); // x is copied println!("{}", x); // OK: x is still valid } ``` El ownership elimina errores comunes de memoria: use-after-free, double-free y memory leaks. El compilador garantiza estas propiedades en tiempo de compilacion. ### Pregunta 2: Cual es la diferencia entre borrowing inmutable y mutable? El borrowing permite utilizar un valor sin tomar ownership, con reglas estrictas para prevenir data races. ```rust // borrowing_rules.rs // Immutable and mutable references fn main() { let mut s = String::from("hello"); // IMMUTABLE REFERENCES (&T) // Can coexist in unlimited numbers let r1 = &s; // immutable reference let r2 = &s; // another immutable reference println!("{} and {}", r1, r2); // OK // MUTABLE REFERENCE (&mut T) // Only one at a time, and no simultaneous immutable references let r3 = &mut s; // mutable reference // let r4 = &s; // ERROR: cannot have both immutable and mutable // let r5 = &mut s; // ERROR: only one mutable reference allowed r3.push_str(" world"); println!("{}", r3); // Reference scopes are limited to their last use let r6 = &s; // OK because r3 is no longer used println!("{}", r6); } // Practical example: modifying a struct struct User { name: String, age: u32, } impl User { // &self: read-only access fn get_name(&self) -> &str { &self.name } // &mut self: modification access fn set_name(&mut self, name: String) { self.name = name; } // self: takes ownership (consumes the instance) fn into_name(self) -> String { self.name // The User instance no longer exists after this } } fn borrowing_with_structs() { let mut user = User { name: String::from("Alice"), age: 30, }; // Reading println!("Name: {}", user.get_name()); // Modifying user.set_name(String::from("Bob")); // Consuming let name = user.into_name(); // user.age; // ERROR: user has been consumed } ``` Estas reglas garantizan la ausencia de data races en tiempo de compilacion. Ningun otro lenguaje ofrece esta garantia sin sacrificar rendimiento. > **Non-Lexical Lifetimes** > > Desde Rust 2018, el compilador utiliza NLL (Non-Lexical Lifetimes) para determinar con mayor precision cuando una referencia ya no se utiliza, permitiendo mayor flexibilidad. ### Pregunta 3: Que son los lifetimes y cuando deben anotarse? Los lifetimes son anotaciones que indican al compilador durante cuanto tiempo las referencias son validas, previniendo referencias colgantes. ```rust // lifetimes.rs // Understanding and annotating lifetimes // ERROR: dangling reference // fn dangling() -> &String { // let s = String::from("hello"); // &s // s is dropped at function end, reference invalid // } // The compiler often infers lifetimes automatically fn first_word(s: &str) -> &str { // Elided lifetime: compiler understands the return // has the same lifetime as the input match s.find(' ') { Some(i) => &s[..i], None => s, } } // Explicit annotation needed with multiple references fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { // 'a means: the return lives at least as long // as the shorter of the two inputs if x.len() > y.len() { x } else { y } } fn lifetime_example() { let string1 = String::from("long string"); let result; { let string2 = String::from("xyz"); result = longest(&string1, &string2); println!("Longest: {}", result); // OK here } // println!("{}", result); // ERROR: string2 is dropped } // Lifetimes in structs struct ImportantExcerpt<'a> { part: &'a str, // Struct cannot outlive part } impl<'a> ImportantExcerpt<'a> { // Method returning a reference with the same lifetime fn level(&self) -> i32 { 3 } // Elided lifetime for &self returning a new reference fn announce_and_return_part(&self, announcement: &str) -> &str { println!("Attention: {}", announcement); self.part // Returns with lifetime 'a } } // Static lifetime: lives for the entire program duration fn static_lifetime() { let s: &'static str = "hello"; // Stored in the binary // Constants have implicit 'static lifetime const MAX_POINTS: u32 = 100_000; } // Combining lifetimes and generics fn longest_with_announcement<'a, T>( x: &'a str, y: &'a str, ann: T, ) -> &'a str where T: std::fmt::Display, { println!("Announcement: {}", ann); if x.len() > y.len() { x } else { y } } ``` Los lifetimes se verifican en tiempo de compilacion. Si el codigo compila, las referencias estan garantizadas como validas. ## Traits y Genericos ### Pregunta 4: Como funcionan los traits en Rust? Los traits definen comportamiento compartido entre diferentes tipos, similar a las interfaces pero con funcionalidades adicionales. ```rust // traits_basics.rs // Defining and implementing traits // Trait definition trait Summary { // Required method (no body) fn summarize(&self) -> String; // Method with default implementation fn summarize_author(&self) -> String { String::from("(Anonymous)") } // Default method that calls a required method fn full_summary(&self) -> String { format!("By {} - {}", self.summarize_author(), self.summarize()) } } // Implementation for different types struct NewsArticle { headline: String, location: String, author: String, content: String, } impl Summary for NewsArticle { fn summarize(&self) -> String { format!("{}, by {} ({})", self.headline, self.author, self.location) } fn summarize_author(&self) -> String { format!("@{}", self.author) } } struct Tweet { username: String, content: String, reply: bool, retweet: bool, } impl Summary for Tweet { fn summarize(&self) -> String { format!("{}: {}", self.username, self.content) } } // Trait bounds: constraining generics fn notify(item: &T) { println!("Breaking news! {}", item.summarize()); } // Alternative syntax with where fn notify_verbose(item: &T) where T: Summary, { println!("Breaking news! {}", item.summarize()); } // Multiple trait bounds fn notify_complex(item: &T) { println!("{}", item); } // Return a type that implements a trait fn create_summarizable() -> impl Summary { Tweet { username: String::from("rust_lang"), content: String::from("Rust 2026 is amazing!"), reply: false, retweet: false, } } ``` Los traits permiten polimorfismo sin herencia de clases, favoreciendo la composicion sobre la herencia. ### Pregunta 5: Explicar la diferencia entre genericidad estatica y dinamica Rust ofrece dos enfoques para el polimorfismo: monomorfizacion (estatica) y objetos trait (dinamica). ```rust // static_vs_dynamic_dispatch.rs // Static vs dynamic dispatch trait Animal { fn speak(&self) -> String; fn name(&self) -> &str; } struct Dog { name: String } struct Cat { name: String } impl Animal for Dog { fn speak(&self) -> String { String::from("Woof!") } fn name(&self) -> &str { &self.name } } impl Animal for Cat { fn speak(&self) -> String { String::from("Meow!") } fn name(&self) -> &str { &self.name } } // STATIC DISPATCH (monomorphization) // Compiler generates a version for each concrete type fn make_speak_static(animal: &T) { // At compile time, becomes make_speak_Dog and make_speak_Cat println!("{} says {}", animal.name(), animal.speak()); } // Advantages: inlining possible, no runtime overhead // Disadvantages: larger binary, type must be known at compile time // DYNAMIC DISPATCH (trait objects) // Uses a vtable to resolve methods at runtime fn make_speak_dynamic(animal: &dyn Animal) { // Resolved via a pointer table (vtable) at runtime println!("{} says {}", animal.name(), animal.speak()); } // Advantages: can store different types, smaller binary // Disadvantages: indirection overhead, no inlining fn main() { let dog = Dog { name: String::from("Rex") }; let cat = Cat { name: String::from("Whiskers") }; // Static: type is known at compile time make_speak_static(&dog); make_speak_static(&cat); // Dynamic: type is resolved at runtime make_speak_dynamic(&dog); make_speak_dynamic(&cat); // Heterogeneous collection (requires dynamic dispatch) let animals: Vec> = vec![ Box::new(Dog { name: String::from("Buddy") }), Box::new(Cat { name: String::from("Luna") }), ]; for animal in animals.iter() { println!("{} says {}", animal.name(), animal.speak()); } } // Object safety: not all traits can become trait objects trait ObjectSafe { fn method(&self); // No Self in return type // No generic parameters } // NOT object safe (cannot be dyn NotObjectSafe) trait NotObjectSafe { fn create() -> Self; // Self in return fn generic(&self, t: T); // Generic } ``` El dispatch estatico es preferible para rendimiento. El dispatch dinamico resulta util para colecciones heterogeneas y flexibilidad. ## Manejo de Errores ### Pregunta 6: Como manejar errores con Result y Option? Rust no tiene excepciones. El manejo de errores se realiza mediante los tipos `Result` y `Option` con pattern matching. ```rust // error_handling.rs // Idiomatic error handling in Rust use std::fs::File; use std::io::{self, Read}; // Option: presence or absence of a value fn find_user(id: u32) -> Option { match id { 1 => Some(String::from("Alice")), 2 => Some(String::from("Bob")), _ => None, // No user found } } // Result: success or error fn divide(a: f64, b: f64) -> Result { if b == 0.0 { Err(String::from("Division by zero")) } else { Ok(a / b) } } fn option_combinators() { let user = find_user(1); // Pattern matching match user { Some(name) => println!("Found: {}", name), None => println!("Not found"), } // unwrap_or: default value let name = find_user(99).unwrap_or(String::from("Unknown")); // map: transform the value if present let upper = find_user(1).map(|n| n.to_uppercase()); // and_then (flatMap): chain Options let first_char = find_user(1).and_then(|n| n.chars().next()); // if let: simplified pattern matching if let Some(name) = find_user(2) { println!("User 2 is {}", name); } } fn result_handling() -> Result<(), Box> { // The ? operator propagates errors automatically let result = divide(10.0, 2.0)?; println!("Result: {}", result); // Equivalent to: // let result = match divide(10.0, 2.0) { // Ok(v) => v, // Err(e) => return Err(e.into()), // }; Ok(()) } // File reading with error propagation fn read_file_contents(path: &str) -> Result { let mut file = File::open(path)?; // Propagates error if failure let mut contents = String::new(); file.read_to_string(&mut contents)?; Ok(contents) } // Custom errors #[derive(Debug)] enum AppError { IoError(io::Error), ParseError(String), NotFound(String), } impl std::fmt::Display for AppError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { AppError::IoError(e) => write!(f, "IO error: {}", e), AppError::ParseError(s) => write!(f, "Parse error: {}", s), AppError::NotFound(s) => write!(f, "Not found: {}", s), } } } impl std::error::Error for AppError {} // Automatic conversion with From impl From for AppError { fn from(error: io::Error) -> Self { AppError::IoError(error) } } fn complex_operation() -> Result { let contents = std::fs::read_to_string("config.txt")?; // Auto-convert if contents.is_empty() { return Err(AppError::NotFound(String::from("Config is empty"))); } Ok(contents) } ``` El operador `?` hace que el codigo sea conciso mientras obliga al manejo explicito de errores. Sin sorpresas en tiempo de ejecucion. > **Evitar unwrap en produccion** > > `unwrap()` y `expect()` generan un panic si el valor es None o Err. Deben reservarse para prototipos o casos donde la falla es imposible. En produccion, es preferible la propagacion con `?` o combinadores. ### Pregunta 7: Como crear errores personalizados con thiserror? El crate `thiserror` simplifica la creacion de errores personalizados ergonomicos. ```rust // custom_errors.rs // Custom errors with thiserror use thiserror::Error; // Error definition with derive macro #[derive(Error, Debug)] pub enum DataStoreError { #[error("connection failed: {0}")] ConnectionFailed(String), #[error("query failed: {query}")] QueryFailed { query: String, source: std::io::Error }, #[error("record not found: id={id}")] NotFound { id: u64 }, #[error("invalid data: {0}")] InvalidData(#[from] serde_json::Error), #[error(transparent)] // Delegates Display to source Other(#[from] anyhow::Error), } // Implementation with rich context pub struct DataStore { connection_string: String, } impl DataStore { pub fn connect(conn_str: &str) -> Result { if conn_str.is_empty() { return Err(DataStoreError::ConnectionFailed( "Empty connection string".into() )); } Ok(Self { connection_string: conn_str.to_string() }) } pub fn get_record(&self, id: u64) -> Result { // Query simulation if id == 0 { return Err(DataStoreError::NotFound { id }); } Ok(Record { id, data: format!("Record {}", id) }) } } pub struct Record { pub id: u64, pub data: String, } // Usage with anyhow for applications use anyhow::{Context, Result}; fn application_code() -> Result<()> { let store = DataStore::connect("postgres://localhost/db") .context("Failed to connect to database")?; let record = store.get_record(42) .context("Failed to fetch user record")?; println!("Got: {}", record.data); Ok(()) } // Pattern: converting errors with context fn read_config() -> Result { let contents = std::fs::read_to_string("config.toml") .context("Failed to read config file")?; let config: Config = toml::from_str(&contents) .context("Failed to parse config file")?; Ok(config) } #[derive(Debug)] struct Config { // ... } ``` `thiserror` es ideal para bibliotecas (errores tipados), mientras que `anyhow` se adapta mejor a aplicaciones (maxima flexibilidad). ## Smart Pointers ### Pregunta 8: Explicar Box, Rc, Arc y RefCell Los smart pointers gestionan la memoria del heap y permiten patrones que el ownership simple no admite. ```rust // smart_pointers.rs // Main smart pointers in Rust use std::rc::Rc; use std::sync::Arc; use std::cell::RefCell; // BOX: heap allocation // Used for: recursive types, large types, trait objects fn box_example() { // Simple heap allocation let b = Box::new(5); println!("b = {}", b); // Recursive type (impossible without Box) #[derive(Debug)] enum List { Cons(i32, Box), Nil, } let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Cons(3, Box::new(List::Nil)))))); println!("{:?}", list); } // RC: Reference Counting (single-threaded) // Multiple owners for the same data fn rc_example() { let data = Rc::new(vec![1, 2, 3]); // Clone increments the reference counter let data_clone1 = Rc::clone(&data); // count = 2 let data_clone2 = Rc::clone(&data); // count = 3 println!("Reference count: {}", Rc::strong_count(&data)); // 3 // Each clone can read the data println!("data_clone1: {:?}", data_clone1); // Data is freed when the last Rc is dropped } // ARC: Atomic Reference Counting (thread-safe) // Like Rc but usable across threads fn arc_example() { use std::thread; let data = Arc::new(vec![1, 2, 3, 4, 5]); let mut handles = vec![]; for i in 0..3 { let data_clone = Arc::clone(&data); let handle = thread::spawn(move || { // Each thread has its own Arc println!("Thread {}: {:?}", i, data_clone); }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } } // REFCELL: Interior Mutability // Allows mutation even with an immutable reference fn refcell_example() { let data = RefCell::new(5); // borrow() returns an immutable reference println!("Value: {}", *data.borrow()); // borrow_mut() returns a mutable reference *data.borrow_mut() += 1; println!("After mutation: {}", *data.borrow()); // Borrowing rules are checked at RUNTIME // Panics if rules are violated // let r1 = data.borrow(); // let r2 = data.borrow_mut(); // PANIC: already borrowed } // Common combination: Rc> // Multiple owners with possible mutation fn rc_refcell_example() { #[derive(Debug)] struct Node { value: i32, children: Vec>>, } let node1 = Rc::new(RefCell::new(Node { value: 1, children: vec![], })); let node2 = Rc::new(RefCell::new(Node { value: 2, children: vec![Rc::clone(&node1)], // node1 is child of node2 })); // Modify node1 from anywhere node1.borrow_mut().value = 10; println!("node2 child value: {}", node2.borrow().children[0].borrow().value); // 10 } // For threads: Arc> or Arc> fn arc_mutex_example() { use std::sync::Mutex; use std::thread; let counter = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let counter = Arc::clone(&counter); let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } println!("Final count: {}", *counter.lock().unwrap()); // 10 } ``` La eleccion del smart pointer correcto depende del contexto: `Box` para heap simple, `Rc`/`Arc` para compartir datos, `RefCell`/`Mutex` para mutabilidad interior. ## Concurrencia ### Pregunta 9: Como garantiza Rust la seguridad entre hilos? El sistema de tipos de Rust previene data races en tiempo de compilacion mediante los traits `Send` y `Sync`. ```rust // thread_safety.rs // Concurrent safety guarantees use std::thread; use std::sync::{Arc, Mutex, mpsc}; // SEND: a type can be transferred to another thread // SYNC: a type can be shared between threads via references // Most types are Send and Sync automatically // Exceptions: Rc (not Send/Sync), RefCell (not Sync), raw pointers fn send_example() { let data = vec![1, 2, 3]; // Vec is Send, so it can be moved to another thread let handle = thread::spawn(move || { println!("Data in thread: {:?}", data); }); handle.join().unwrap(); } // The compiler prevents concurrency errors fn compile_time_safety() { // This would NOT compile: // let data = std::rc::Rc::new(5); // thread::spawn(move || { // println!("{}", data); // ERROR: Rc is not Send // }); // Solution: use Arc let data = Arc::new(5); let data_clone = Arc::clone(&data); thread::spawn(move || { println!("{}", data_clone); // OK: Arc is Send }); } // Mutex for thread-safe shared mutation fn mutex_pattern() { let counter = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let counter = Arc::clone(&counter); let handle = thread::spawn(move || { // lock() blocks until exclusive access is obtained let mut num = counter.lock().unwrap(); *num += 1; // MutexGuard is dropped here, releasing the lock }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } println!("Result: {}", *counter.lock().unwrap()); } // RwLock for multiple reads / exclusive write fn rwlock_example() { use std::sync::RwLock; let data = Arc::new(RwLock::new(vec![1, 2, 3])); let mut handles = vec![]; // Multiple simultaneous readers for i in 0..3 { let data = Arc::clone(&data); handles.push(thread::spawn(move || { let read = data.read().unwrap(); println!("Reader {}: {:?}", i, *read); })); } // Only one writer at a time { let data = Arc::clone(&data); handles.push(thread::spawn(move || { let mut write = data.write().unwrap(); write.push(4); println!("Writer added 4"); })); } for handle in handles { handle.join().unwrap(); } } // Channels for inter-thread communication fn channel_example() { let (tx, rx) = mpsc::channel(); // Multi-producer, single-consumer // Clone the sender for multiple producers let tx1 = tx.clone(); thread::spawn(move || { tx1.send("from thread 1").unwrap(); }); thread::spawn(move || { tx.send("from thread 2").unwrap(); }); // Receive messages for received in rx { println!("Got: {}", received); } } ``` "Fearless concurrency": si el codigo compila, no hay data races. El compilador es la primera linea de defensa. > **Mutex Poisoning** > > Si un hilo entra en panic mientras tiene un Mutex, este se "envenena". Las llamadas posteriores a `lock()` devuelven un error que puede recuperarse con `into_inner()`. ### Pregunta 10: Como funciona async/await en Rust? El async en Rust se basa en Futures de costo cero, sin un runtime integrado en el lenguaje. ```rust // async_await.rs // Asynchronous programming in Rust use tokio::time::{sleep, Duration}; // async fn returns a Future that must be executed async fn fetch_data(url: &str) -> Result { // await suspends execution without blocking the thread let response = reqwest::get(url).await?; let body = response.text().await?; Ok(body) } // Futures are lazy: nothing executes without await or poll async fn lazy_example() { let future = async { println!("This won't print yet"); }; // Nothing happened future.await; // Now it executes } // Parallel execution of futures async fn parallel_execution() { // join! executes multiple futures in parallel let (result1, result2) = tokio::join!( fetch_data("https://api.example.com/1"), fetch_data("https://api.example.com/2"), ); println!("Results: {:?}, {:?}", result1, result2); } // select! for the first completed future async fn race_example() { tokio::select! { result = fetch_data("https://api1.example.com") => { println!("API 1 responded first: {:?}", result); } result = fetch_data("https://api2.example.com") => { println!("API 2 responded first: {:?}", result); } _ = sleep(Duration::from_secs(5)) => { println!("Timeout!"); } } } // Streams: asynchronous iterators use tokio_stream::StreamExt; async fn stream_example() { let mut stream = tokio_stream::iter(vec![1, 2, 3, 4, 5]); while let Some(value) = stream.next().await { println!("Got: {}", value); } } // Spawn for background tasks async fn spawn_tasks() { let handle = tokio::spawn(async { sleep(Duration::from_secs(1)).await; "Task completed" }); println!("Task spawned, doing other work..."); let result = handle.await.unwrap(); println!("Result: {}", result); } // Entry point with tokio #[tokio::main] async fn main() { // The tokio runtime executes futures parallel_execution().await; } // Alternative: multi-threaded or single-threaded runtime #[tokio::main(flavor = "current_thread")] async fn main_single_thread() { // Everything runs on a single thread } #[tokio::main(flavor = "multi_thread", worker_threads = 4)] async fn main_multi_thread() { // Pool of 4 worker threads } ``` Rust async sigue el principio "bring your own runtime": tokio, async-std o smol. Esta flexibilidad permite optimizaciones especificas segun el caso de uso. ## Patrones Avanzados ### Pregunta 11: Explicar el patron Builder en Rust El patron Builder es idiomatico en Rust para construir estructuras complejas con muchos campos opcionales. ```rust // builder_pattern.rs // Idiomatic Builder pattern in Rust #[derive(Debug, Clone)] pub struct Server { host: String, port: u16, max_connections: usize, timeout_seconds: u64, tls_enabled: bool, tls_cert_path: Option, } // Builder with consuming approach (ownership) #[derive(Default)] pub struct ServerBuilder { host: String, port: u16, max_connections: usize, timeout_seconds: u64, tls_enabled: bool, tls_cert_path: Option, } impl ServerBuilder { pub fn new() -> Self { Self { host: String::from("localhost"), port: 8080, max_connections: 100, timeout_seconds: 30, tls_enabled: false, tls_cert_path: None, } } // Each method takes self and returns Self for chaining pub fn host(mut self, host: impl Into) -> Self { self.host = host.into(); self } pub fn port(mut self, port: u16) -> Self { self.port = port; self } pub fn max_connections(mut self, max: usize) -> Self { self.max_connections = max; self } pub fn timeout(mut self, seconds: u64) -> Self { self.timeout_seconds = seconds; self } pub fn enable_tls(mut self, cert_path: impl Into) -> Self { self.tls_enabled = true; self.tls_cert_path = Some(cert_path.into()); self } // build() consumes the builder and creates the final structure pub fn build(self) -> Result { if self.tls_enabled && self.tls_cert_path.is_none() { return Err("TLS enabled but no certificate path provided".into()); } Ok(Server { host: self.host, port: self.port, max_connections: self.max_connections, timeout_seconds: self.timeout_seconds, tls_enabled: self.tls_enabled, tls_cert_path: self.tls_cert_path, }) } } // Fluent usage fn create_server() -> Result { ServerBuilder::new() .host("0.0.0.0") .port(443) .max_connections(1000) .timeout(60) .enable_tls("/etc/ssl/cert.pem") .build() } // Alternative with derive macro (typed-builder crate) // #[derive(TypedBuilder)] // pub struct Config { // #[builder(default = "localhost".to_string())] // host: String, // #[builder(default = 8080)] // port: u16, // } // Pattern with type-level validation (typestate pattern) pub struct Unvalidated; pub struct Validated; pub struct Request { url: String, method: String, headers: Vec<(String, String)>, _state: std::marker::PhantomData, } impl Request { pub fn new(url: &str) -> Self { Self { url: url.to_string(), method: "GET".to_string(), headers: vec![], _state: std::marker::PhantomData, } } pub fn method(mut self, method: &str) -> Self { self.method = method.to_string(); self } // validate() changes the state type pub fn validate(self) -> Result, String> { if self.url.is_empty() { return Err("URL cannot be empty".into()); } Ok(Request { url: self.url, method: self.method, headers: self.headers, _state: std::marker::PhantomData, }) } } impl Request { // send() is only available on validated requests pub async fn send(self) -> Result { // Implementation... todo!() } } struct Response; ``` El patron typestate garantiza en tiempo de compilacion que ciertas operaciones solo pueden invocarse en el estado correcto. ### Pregunta 12: Como implementar un trait para tipos externos? La "regla del huerfano" impide implementar un trait externo para un tipo externo, pero existen soluciones. ```rust // newtype_pattern.rs // The Newtype pattern to work around the orphan rule use std::fmt; // ORPHAN RULE: cannot implement Display (std) for Vec (std) // impl fmt::Display for Vec { ... } // ERROR // SOLUTION 1: Newtype wrapper struct Wrapper(Vec); impl fmt::Display for Wrapper { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "[{}]", self.0.join(", ")) } } fn newtype_example() { let w = Wrapper(vec![ String::from("hello"), String::from("world"), ]); println!("{}", w); // [hello, world] } // Transparent access with Deref use std::ops::Deref; impl Deref for Wrapper { type Target = Vec; fn deref(&self) -> &Self::Target { &self.0 } } fn deref_example() { let w = Wrapper(vec![String::from("test")]); println!("Length: {}", w.len()); // Calls Vec::len via Deref } // SOLUTION 2: Extension trait (to add methods) trait VecExt { fn first_or_default(&self) -> Option<&T>; } impl VecExt for Vec { fn first_or_default(&self) -> Option<&T> { self.first() } } fn extension_trait_example() { let v = vec![1, 2, 3]; println!("First: {:?}", v.first_or_default()); } // Newtype with domain semantics #[derive(Debug, Clone, PartialEq, Eq, Hash)] struct Email(String); impl Email { pub fn new(email: &str) -> Result { if email.contains('@') && email.contains('.') { Ok(Self(email.to_string())) } else { Err("Invalid email format") } } pub fn as_str(&self) -> &str { &self.0 } } impl fmt::Display for Email { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.0) } } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] struct UserId(u64); impl UserId { pub fn new(id: u64) -> Self { Self(id) } } // Newtypes add type safety without runtime overhead fn process_user(id: UserId, email: Email) { println!("Processing user {} with email {}", id.0, email); } fn type_safety_example() { let id = UserId::new(42); let email = Email::new("user@example.com").unwrap(); process_user(id, email); // This would not compile: // process_user(email, id); // Types reversed // process_user(UserId::new(42), "string"); // String instead of Email } ``` Los newtypes no tienen costo en tiempo de ejecucion gracias a la garantia de representacion de memoria identica. ### Pregunta 13: Como usar macros procedurales? Las macros procedurales permiten generar codigo en tiempo de compilacion, como derives personalizados. ```rust // procedural_macros.rs // Understanding procedural macros // Proc macros are defined in a separate crate with proc-macro = true // Crate: my_derive (Cargo.toml: proc-macro = true) use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, DeriveInput}; // DERIVE MACRO: #[derive(MyTrait)] #[proc_macro_derive(MyDebug)] pub fn my_debug_derive(input: TokenStream) -> TokenStream { // Parse input as a type definition let input = parse_macro_input!(input as DeriveInput); let name = input.ident; // Generate implementation code let expanded = quote! { impl std::fmt::Debug for #name { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, stringify!(#name)) } } }; TokenStream::from(expanded) } // ATTRIBUTE MACRO: #[my_attribute] #[proc_macro_attribute] pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream { // attr contains the attribute arguments // item contains the annotated element (function, struct, etc.) let method_path = attr.to_string(); // "GET, /users" let input = parse_macro_input!(item as syn::ItemFn); let fn_name = &input.sig.ident; let expanded = quote! { #input // Additionally generated code fn register_#fn_name() { println!("Registered route: {}", #method_path); } }; TokenStream::from(expanded) } // FUNCTION-LIKE MACRO: my_macro!(...) #[proc_macro] pub fn make_answer(_input: TokenStream) -> TokenStream { "fn answer() -> u32 { 42 }".parse().unwrap() } // --- Usage in client code --- // Derive macro #[derive(MyDebug)] struct Point { x: i32, y: i32, } // Attribute macro #[route("GET", "/users")] fn get_users() -> Vec { vec![] } // Function-like macro make_answer!(); // Generates fn answer() -> u32 { 42 } fn main() { let p = Point { x: 1, y: 2 }; println!("{:?}", p); // Uses our MyDebug println!("Answer: {}", answer()); // 42 } struct User; ``` Las macros procedurales son poderosas para codigo repetitivo: serializacion, enrutamiento web, validacion, entre otros. ## Seguridad de Memoria y Unsafe ### Pregunta 14: Cuando y como usar unsafe? El bloque `unsafe` permite omitir ciertas verificaciones del compilador para codigo de bajo nivel. ```rust // unsafe_rust.rs // Understanding unsafe and its guarantees // The 5 superpowers of unsafe: // 1. Dereference raw pointers // 2. Call unsafe functions // 3. Access/modify mutable static variables // 4. Implement unsafe traits // 5. Access union fields // RAW POINTERS fn raw_pointers() { let mut num = 5; // Creating raw pointers is safe let r1 = &num as *const i32; let r2 = &mut num as *mut i32; // Dereferencing requires unsafe unsafe { println!("r1 is: {}", *r1); *r2 = 10; println!("r2 is: {}", *r2); } } // UNSAFE FUNCTION // The function guarantees safety IF preconditions are met unsafe fn dangerous() { // Code that assumes the caller verified invariants } fn call_dangerous() { // Must be in an unsafe block unsafe { dangerous(); } } // SAFE ABSTRACTION over unsafe code fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) { let len = values.len(); let ptr = values.as_mut_ptr(); assert!(mid <= len); // Runtime check unsafe { // We know the two slices don't overlap ( std::slice::from_raw_parts_mut(ptr, mid), std::slice::from_raw_parts_mut(ptr.add(mid), len - mid), ) } } // FFI: calling C code extern "C" { fn abs(input: i32) -> i32; } fn call_c_function() { unsafe { println!("Absolute value: {}", abs(-3)); } } // Export a function for C #[no_mangle] pub extern "C" fn call_from_c() { println!("Called from C!"); } // MUTABLE STATIC static mut COUNTER: u32 = 0; fn increment_counter() { unsafe { COUNTER += 1; println!("COUNTER: {}", COUNTER); } } // UNSAFE TRAIT unsafe trait Dangerous { // Implementers guarantee invariants } unsafe impl Dangerous for i32 { // Implementer asserts respecting the trait's invariants } // Practical example: structure with internal pointer pub struct MyVec { ptr: *mut T, len: usize, capacity: usize, } impl MyVec { pub fn new() -> Self { Self { ptr: std::ptr::null_mut(), len: 0, capacity: 0, } } pub fn push(&mut self, value: T) { if self.len == self.capacity { self.grow(); } unsafe { std::ptr::write(self.ptr.add(self.len), value); } self.len += 1; } fn grow(&mut self) { // Unsafe allocation/reallocation... } } impl Drop for MyVec { fn drop(&mut self) { unsafe { // Properly free memory for i in 0..self.len { std::ptr::drop_in_place(self.ptr.add(i)); } if self.capacity > 0 { let layout = std::alloc::Layout::array::(self.capacity).unwrap(); std::alloc::dealloc(self.ptr as *mut u8, layout); } } } } ``` > **Regla de oro de unsafe** > > Minimizar la superficie de codigo unsafe. Encapsular el codigo unsafe en abstracciones seguras que garanticen invariantes. El codigo unsafe nunca debe corromper la memoria segura circundante. ### Pregunta 15: Como funciona el borrow checker? El borrow checker es el nucleo del compilador de Rust que verifica las reglas de ownership y borrowing. ```rust // borrow_checker.rs // Understanding how the borrow checker works fn borrow_checker_basics() { let mut v = vec![1, 2, 3]; // RULE 1: Either multiple immutable references or one mutable let r1 = &v; let r2 = &v; println!("{:?} {:?}", r1, r2); // OK: multiple immutable references // From here, r1 and r2 are no longer used (NLL) let r3 = &mut v; // OK thanks to Non-Lexical Lifetimes r3.push(4); } // The borrow checker tracks lifetimes fn lifetime_tracking() { let mut data = String::from("hello"); let slice = &data[..]; // Immutable borrow starts // data.push_str(" world"); // ERROR: cannot mutate during borrow println!("{}", slice); // Last use of slice data.push_str(" world"); // OK: borrow ended } // Common problems and solutions mod common_patterns { // Problem: borrowing two mutable fields struct Data { field1: Vec, field2: Vec, } fn problem(data: &mut Data) { // This sometimes doesn't compile directly: // let f1 = &mut data.field1; // let f2 = &mut data.field2; // Solution: destructuring let Data { field1, field2 } = data; field1.push(1); field2.push(2); } // Problem: iterate and modify fn iterate_and_modify() { let mut v = vec![1, 2, 3, 4, 5]; // Does not compile: // for &x in &v { // if x % 2 == 0 { // v.push(x * 2); // ERROR: borrowed by iterator // } // } // Solution 1: collect indices first let to_add: Vec = v.iter() .filter(|&&x| x % 2 == 0) .map(|&x| x * 2) .collect(); v.extend(to_add); // Solution 2: use explicit indices let len = v.len(); for i in 0..len { if v[i] % 2 == 0 { let new_val = v[i] * 2; v.push(new_val); } } } // Problem: self-referential struct // struct SelfRef { // data: String, // slice: &str, // Reference to data - IMPOSSIBLE // } // Solution: use indices or crates like ouroboros struct SafeSelfRef { data: String, slice_start: usize, slice_end: usize, } impl SafeSelfRef { fn get_slice(&self) -> &str { &self.data[self.slice_start..self.slice_end] } } } // Patterns to work around limitations mod workarounds { use std::cell::RefCell; // Interior mutability when borrow checker is too restrictive struct Graph { nodes: RefCell>, } struct Node { value: i32, } impl Graph { fn add_node(&self, value: i32) { // Mutation possible despite &self self.nodes.borrow_mut().push(Node { value }); } fn get_node(&self, index: usize) -> Option { self.nodes.borrow().get(index).map(|n| n.value) } } } ``` El borrow checker puede parecer restrictivo al principio, pero estas restricciones eliminan categorias enteras de errores presentes en otros lenguajes. ## Conclusion Las entrevistas sobre Rust evaluan la comprension profunda del sistema de ownership, las garantias de seguridad de memoria y la capacidad de escribir codigo concurrente sin data races. Dominar estos conceptos distingue a los desarrolladores que pueden aprovechar las ventajas unicas de Rust. ### Lista de verificacion para la preparacion - Comprender el ownership, borrowing y las tres reglas fundamentales - Saber cuando y como anotar lifetimes - Dominar los traits y la diferencia entre dispatch estatico y dinamico - Manejar errores de forma idiomatica con Result y Option - Elegir el smart pointer adecuado segun el contexto - Escribir codigo concurrente con Arc, Mutex y canales - Comprender async/await y runtimes como tokio - Saber cuando y como usar unsafe de forma segura La preparacion para entrevistas sobre Rust requiere practica con los conceptos unicos de ownership del lenguaje. Los ejercicios en Exercism, proyectos personales y contribuciones al ecosistema de Rust consolidan este conocimiento para las entrevistas tecnicas mas exigentes. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/es/blog/rust/rust-interview-questions