00 · Guía de lectura de código Rust: Sintaxis, funciones, métodos y memoria desde cero#
Si es tu primer contacto con Rust y abres un ejemplo de código, es muy probable que te encuentres con símbolos y palabras clave que parecen jeroglíficos: #[derive(Debug, PartialEq, Eq)], pub struct NonEmptyTitle(String);, impl, &self, &'static str, ::, o mod tests.
Esta guía existe para que nunca tengas que adivinar. Antes de la Lección 01 (o teniéndola abierta a tu lado), aquí aprenderás a descifrar cada símbolo, palabra clave, atributo y mecanismo de memoria de Rust desde primeros principios.
Tabla de rescate rápido (Decodificador de símbolos en 60 segundos)#
| Símbolo o palabra | ¿Qué significa en lenguaje humano? | Ejemplo | ¿Qué pasaría si no estuviera? |
|---|---|---|---|
fn | "Aquí declaro una función o método". | fn add(a: u32) -> u32 | Error sintáctico; el compilador no sabe que empieza una función. |
: Tipo | "Esta variable o parámetro es estrictamente de este tipo". | raw: String | Rust exige tipos explícitos en todos los parámetros de funciones. |
-> Tipo | "Esta función devolverá un valor de este tipo". | -> Result<Self, &'static str> | Si se omite, la función devuelve el tipo vacío unitario (). |
Última línea sin ; | "El resultado de esta expresión es el valor que retorna la función". | { left + right } | Si pones ;, se convierte en sentencia y devuelve (), dando error de tipos. |
pub | "Haz visible y utilizable este elemento fuera de su módulo actual". | pub struct Task | El struct o función solo sería visible dentro del archivo donde nació. |
:: | "Separador de rutas/carpetas de código" (namespace). | std::collections::HashMap, Status::Active | Permite navegar módulos, encontrar variantes de un enum o llamar funciones asociadas (String::from). |
struct | "Molde para empaquetar datos juntos". | struct Task { id: u64 } | Permite crear tus propios tipos de datos en lugar de usar primitivos sueltos. |
struct Nombre(Tipo); | Tuple struct / Newtype: Un struct con un solo campo anónimo accesible como .0. | struct NonEmptyTitle(String); | Permite al compilador distinguir un título validado de cualquier String crudo. |
impl Tipo { ... } | "Aquí adentro defino las funciones y métodos que pertenecen a este tipo". | impl NonEmptyTitle { ... } | En Rust los datos (struct) y el comportamiento (impl) se declaran separados. |
Self (con 'S' mayúscula) | "El tipo actual para el que estoy escribiendo el bloque impl". | fn parse(...) -> Result<Self, ...> | Es un atajo para no repetir el nombre del struct. |
self (con 's' minúscula) | "La instancia concreta sobre la que se está invocando este método". | pub fn as_str(&self) | Igual que this en TypeScript/JavaScript o self en Python. |
&self | "Préstamo compartido de solo lectura de la instancia". | title.as_str() | Crucial: Permite leer la instancia sin destruirla ni quitarle la propiedad al dueño. |
"texto" | Literal de texto: Grabado en el binario .exe, vive todo el programa. | "Title cannot be empty" | Su tipo es &'static str (puntero de 16 bytes a memoria fija de solo lectura). |
String | Texto dinámico alojado en el Heap con dueño único (24 bytes en Stack). | String::from("Rust") | Puede crecer, encogerse o mutar en tiempo de ejecución. |
&str | "Ventana prestada" a bytes de texto (ya sea en el binario o en el Heap). | &self.0 o "hola" | Permite leer texto sin copiar bytes ni reservar memoria nueva. |
#[derive(...)] | "Compilador, genera automáticamente la implementación estándar de estos traits". | #[derive(Debug, PartialEq, Eq)] | Sin esto, tu tipo no se puede imprimir con {:?} ni comparar con ==. |
1. Anatomía visual de una función en Rust#
En Rust, cada parte de una firma de función comunica un contrato exacto al compilador y al desarrollador:
// Visibilidad Nombre Parámetro 1 Parámetro 2 Tipo de retorno
// │ │ │ │ │
pub fn add(left: u32, right: u32) -> u32 {
left + right // <-- ¡Sin punto y coma! Expresión de retorno.
}
La regla de oro del punto y coma (Expresión vs Sentencia)#
En muchos lenguajes (C, Java, JavaScript), las funciones terminan obligatoriamente con la palabra return. En Rust, la palabra return existe, pero casi nunca se usa al final de una función:
- Una expresión sin punto y coma (
left + right) evalúa a un valor y se convierte automáticamente en el valor retornado por el bloque. - Una sentencia con punto y coma (
left + right;) ejecuta la operación, descarta el resultado y evalúa al tipo unitario()(equivalente avoiden otros lenguajes).
El error más común del principiante: Si escribes:
fn add(left: u32, right: u32) -> u32 {
left + right; // <-- ¡ERROR! El ';' descarta el número y devuelve ().
}
El compilador protestará de inmediato: error[E0308]: mismatched types: expected u32, found ()``. Para devolver el valor, simplemente retira el punto y coma de la última línea.
2. Cómo se organiza un archivo y sus pruebas integradas#
Cuando abres un archivo de práctica en Rust, su estructura general sigue este patrón estándar:
// --- CÓDIGO DE PRODUCCIÓN (El dominio) ---
pub fn double(n: u32) -> u32 {
n * 2
}
// --- SUITE DE PRUEBAS INTEGRADA (El arnés) ---
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn doubles_positive_number() {
assert_eq!(double(4), 8);
}
}
Desglosemos cada elemento de esta arquitectura:
El operador :: (Navegador de rutas y espacios de nombres)#
El símbolo :: en Rust cumple la misma función que la barra diagonal / en un sistema de carpetas o el punto . en otros lenguajes:
- Módulos y colecciones:
std::collections::HashMapsignifica "dentro del cratestd, en el módulocollections, toma la estructuraHashMap". - Variantes de enum:
Status::Activesignifica "la varianteActiveque pertenece al enumStatus". - Funciones asociadas:
String::from("hola")significa "llama a la funciónfromasociada al tipoString".
mod tests y use super::*;#
mod tests { ... }crea un módulo hijo (una caja aislada) dentro del mismo archivo para alojar las pruebas.- Por defecto, en Rust los módulos hijos tienen su propio alcance y no ven automáticamente las funciones privadas del módulo padre.
- La línea
use super::*;le dice al módulo de pruebas: "Importa todo (*) lo que esté declarado en el nivel superior inmediato (super), para que pueda probarlo directamente".
Atributos #[cfg(test)] y #[test]#
#[cfg(test)]: Es una directiva de configuración condicional. Le ordena arustc: "Compila este módulo de pruebas ÚNICAMENTE si te llamaron con la bandera--test. Si compilas para producción normal, ignora por completo todo este bloque para no engordar el binario final".#[test]: Marca la función que le sigue como un caso de prueba individual ejecutable por el corredor de pruebas.
Macros de aserción: assert! vs assert_eq!#
assert!(condicion): Comprueba que una expresión booleana sea verdadera. Si esfalse, aborta con pánico.assert_eq!(izq, der): Comprueba queizq == der. Si difieren, imprime en la terminal los dos valores comparados, permitiéndote ver exactamente qué falló.
El proceso de compilación y ejecución en Windows (2 pasos)#
En Windows, cuando trabajas con rustc --test, ocurren dos etapas físicas indispensables:
# Paso 1: rustc SOLO construye el binario ejecutable en disco (no lo ejecuta)
rustc --test .\archivo.rs -o .\archivo-tests.exe
# Paso 2: Invocar el ejecutable es lo que REALMENTE corre las pruebas y muestra los resultados
.\archivo-tests.exe
Ejecutar únicamente rustc --test .\archivo.rs no corre ninguna prueba. Solo crea el archivo ejecutable. Si la terminal no muestra texto, significa que compiló con éxito; debes invocar a continuación .\archivo-tests.exe para ver el reporte de pruebas:
running 1 test
test tests::doubles_positive_number ... ok
test result: ok. 1 passed; 0 failed; finished in 0.00s
3. Estructuras, implementación (impl) y el dilema de self#
En Rust no existen clases como en la programación orientada a objetos tradicional. En su lugar, los datos y los comportamientos se modelan de forma limpia y desacoplada:
struct: Define la forma y los campos de los datos.impl: Define las funciones y métodos asociados a esa estructura.
Structs clásicos vs Tuple structs (Newtypes)#
// 1. Struct con campos nombrados tradicional:
pub struct Task {
pub id: u64,
pub completed: bool,
}
// 2. Tuple struct (o patrón "Newtype"):
// Empaqueta un tipo existente para darle semántica de dominio propia.
pub struct NonEmptyTitle(String);
En NonEmptyTitle(String), no le dimos nombre al campo interno. Rust le asigna automáticamente el índice .0. Por lo tanto:
- Si tienes una instancia
title: NonEmptyTitle, accedes a su contenido interno contitle.0.
Funciones asociadas (Self::...) frente a Métodos (&self)#
Dentro de un bloque impl, una función puede comportarse de dos maneras radicalmente distintas:
impl NonEmptyTitle {
// A) FUNCIÓN ASOCIADA (Constructor):
// No recibe "self". Se invoca sobre el tipo general: NonEmptyTitle::parse(...)
pub fn parse(raw: String) -> Result<Self, &'static str> {
let trimmed = raw.trim();
if trimmed.is_empty() {
Err("Title cannot be empty")
} else {
Ok(Self(trimmed.to_string())) // Self con mayúscula = NonEmptyTitle
}
}
// B) MÉTODO DE INSTANCIA CON &self:
// Recibe "&self". Se invoca sobre una variable existente: title.as_str()
pub fn as_str(&self) -> &str {
&self.0 // Tomamos prestado el String interno como &str
}
}
¿Por qué &self y no self a secas?#
Esta distinción es uno de los superpoderes más importantes de Rust:
| Firma del primer parámetro | Significado en memoria | Efecto sobre la variable que lo llama | ¿Cuándo usarlo? |
|---|---|---|---|
&self | Préstamo inmutable compartido. Solo lee datos. | La variable original permanece viva y puede seguir usándose después. | Lecturas, consultas, formateo (as_str(), len(), is_empty()). |
&mut self | Préstamo mutable exclusivo. Puede modificar campos. | La variable original permanece viva, pero nadie más puede leerla mientras muta. | Modificaciones en el lugar (clear(), set_status()). |
self | Transferencia de propiedad (Move). Consume la instancia. | La variable original se destruye o se transfiere; ya no se puede volver a usar. | Conversiones destructivas (into_inner(), into_bytes()). |
Si as_str recibiera self por valor en lugar de &self, la primera vez que llamaras a title.as_str() para imprimir el título, ¡la variable title quedaría destruida y no podrías volver a usarla jamás! Por eso los métodos de lectura utilizan siempre &self.
4. El misterio de los textos en Rust: "literal", &str, &'static str y String#
¿Por qué Rust tiene más de un tipo de texto si lenguajes como JavaScript o Python solo tienen string? La respuesta está en la física de la memoria del computador.
A) El texto literal: "Title cannot be empty" y &'static str#
Cuando escribes texto entre comillas en tu código fuente, ese texto no se crea cuando el programa corre. El compilador lo graba físicamente dentro del archivo .exe compilado en disco (en una sección de solo lectura llamada .rdata en Windows).
- Cuando ejecutas el programa, Windows carga el
.exeen memoria. Ese texto ya está allí antes de que arranque la primera línea de tu funciónmain. - Como ese texto reside en la memoria de solo lectura durante toda la vida del programa, su tiempo de vida (lifetime) es inmortal:
'static. - Un
&'static stres un fat pointer de 16 bytes en la pila: - 8 bytes: La dirección de memoria donde empieza el texto en el binario.
- 8 bytes: La longitud exacta en bytes de ese texto.
B) El texto dinámico: String#
Un String es un texto creado en tiempo de ejecución. No puede estar grabado en el binario porque su longitud o contenido pueden cambiar (por ejemplo, lo escribió un usuario en una página web o vino de una base de datos).
- El contenido vive en el Heap (la memoria dinámica del sistema).
- En la pila (Stack), la variable
Stringocupa exactamente 24 bytes: ptr(8 bytes): Puntero a la dirección del Heap.cap(8 bytes): Capacidad total de memoria reservada en el Heap.len(8 bytes): Cantidad de bytes UTF-8 válidos utilizados actualmente.- La variable en la pila es la dueña única del bloque en el Heap. Cuando sale de su ámbito (
}), Rust devuelve esa memoria al sistema operativo automáticamente, sin pausas de recolector de basura.
C) La ventana prestada: &str#
Un &str (llamado string slice) es un puntero doble de 16 bytes (puntero + longitud). Es simplemente una ventana de lectura que puede asomarse a:
- Un texto estático grabado en el binario (
&'static stres un&strespecial con vida permanente). - O una porción de un
Stringque vive en el Heap (obtenido mediante&mi_stringo&mi_string[0..4]).
flowchart LR
subgraph STACK["Pila (Stack)"]
S_DESC["title: NonEmptyTitle\nString descriptor (24B)\n[ptr | cap | len]"]
SL_DESC["title.as_str(): &str\nSlice descriptor (16B)\n[ptr | len]"]
LIT_DESC["Err(&'static str)\nStatic slice (16B)\n[ptr | len]"]
end
subgraph HEAP["Montículo (Heap)"]
HEAP_DATA["Bytes UTF-8 en RAM:\n['R','u','s','t',' ','G','u','i','d','e']"]
end
subgraph BINARY["Binario .exe (.rdata)"]
RO_DATA["Texto fijo grabado en disco:\n'Title cannot be empty'"]
end
S_DESC -->|ptr| HEAP_DATA
SL_DESC -.->|ptr prestado| HEAP_DATA
LIT_DESC -.->|ptr permanente| RO_DATA
5. Traits y qué hace exactamente #[derive(Debug, PartialEq, Eq)]#
En Rust, un nuevo struct empieza completamente "en blanco": el compilador no asume ninguna capacidad que tú no hayas declarado explícitamente. No puedes imprimirlo en consola ni puedes compararlo con == a menos que le enseñes cómo hacerlo.
Un Trait es un contrato de capacidades (similar a una interfaz en otros lenguajes). El atributo #[derive(...)] le pide al compilador: "Implementa el código estándar de estos traits por mí, campo por campo".
#[derive(Debug, PartialEq, Eq)]
pub struct NonEmptyTitle(String);
1. Debug (Permite formatear con {:?})#
- Qué hace: Enseña al compilador a transformar la estructura en texto legible para programadores.
- Por qué es indispensable en pruebas: La macro
assert_eq!(left, right)exige que ambos operandos implementenDebug. Si un test falla, el arnés necesita imprimirleft: NonEmptyTitle("A")vsright: NonEmptyTitle("B"). SinDebug, el test no puede compilar.
2. PartialEq (Permite usar los operadores == y !=)#
- Qué hace: Implementa la función de comparación de igualdad. Compara dos instancias comparando cada uno de sus campos internos.
- Por qué lo usa
assert_eq!: Cuando escribesassert_eq!(a, b), la macro ejecuta internamenteif !(a == b) { panic!(...); }. SinPartialEq, el operador==no existe para tu struct.
3. Eq (Certificado formal de igualdad total y reflexiva)#
- Qué hace: Es un marker trait (no añade código nuevo). Le certifica al compilador que la relación de igualdad es total y satisface la propiedad reflexiva: para cualquier valor
a,a == aes SIEMPRE verdadero. - ¿Por qué
PartialEqse llama "parcial"? (El caso de los números flotantes): - En la especificación estándar de números flotantes (IEEE 754, tipos
f32yf64), existe un valor especial llamadoNaN(Not a Number, resultado de operaciones como0.0 / 0.0osqrt(-1.0)). - La especificación matemática exige que:
NaN == NaNsea falso. - Por lo tanto, en los números con coma flotante, ¡un valor puede no ser igual a sí mismo! Su igualdad es solo "parcial". Por eso
f64implementaPartialEq, pero NO puede implementarEq. - Los tipos enteros (
u32,i64), booleanos y cadenas (String,&str) no tienen casos extraños comoNaN. En ellos, cualquier valor es siempre idéntico a sí mismo. Por eso pueden y deben implementar tantoPartialEqcomoEq.
6. Diagrama de relaciones: De la sintaxis al sistema#
El siguiente diagrama unifica la anatomía de tipos, contratos de traits y la distribución de memoria que acabas de aprender:
flowchart TD
subgraph S1["1. Estructuras e Implementación (impl)"]
ST["struct NonEmptyTitle(String)"]
IM["impl NonEmptyTitle"]
FN_ASSOC["Función asociada: Self::parse(raw)\n-> Result<Self, &'static str>"]
FN_MET["Método con préstamo: title.as_str(&self)\n-> &str (No destruye la instancia)"]
ST --> IM
IM --> FN_ASSOC
IM --> FN_MET
end
subgraph S2["2. Contratos de Capacidad: Traits #[derive(...)]"]
TR_DBG["Debug -> Formato {:?}\n(Diagnóstico en aserciones assert_eq!)"]
TR_PEQ["PartialEq -> Operadores == y !=\n(Comparación campo por campo)"]
TR_EQ["Eq -> Igualdad total y reflexiva\n(a == a siempre, sin casos NaN como f64)"]
TR_PEQ --> TR_EQ
end
subgraph S3["3. Modelo de Memoria para Textos"]
BIN["Binario compilado .exe (.rdata en Windows PE)\n'Title cannot be empty' (En disco y RAM fija)"]
STACK["Stack: Descriptor String en NonEmptyTitle\n[ptr: 8B | cap: 8B | len: 8B] (24 bytes)"]
HEAP["Heap (Memoria dinámica)\nBytes UTF-8: ['R','u','s','t',...]"]
VIEW_STATIC["&'static str: [ptr: 8B | len: 8B]\n(Apunta al binario, vive todo el programa)"]
VIEW_HEAP["&str: [ptr: 8B | len: 8B]\n(Ventana prestada a bytes en Heap)"]
VIEW_STATIC -.-> BIN
STACK --> HEAP
VIEW_HEAP -.-> HEAP
end
S1 --> S2
S1 --> S3
7. Ejemplo completo, comentado y ejecutable#
Copia este bloque en un archivo llamado primer_sample.rs en tu carpeta de prácticas y compílalo con las dos órdenes de Windows indicadas abajo. Observa cómo todos los conceptos interactúan armoniosamente:
// 1. Derivamos Debug para poder imprimir con {:?}, PartialEq para usar ==, y Eq porque no usamos floats
#[derive(Debug, PartialEq, Eq)]
pub struct NonEmptyTitle(String); // Tuple struct: el String interno es el campo .0
impl NonEmptyTitle {
// 2. Función asociada: actúa como constructor y valida el invariante
// Retorna Self (un NonEmptyTitle válido) o un &'static str (literal en memoria de solo lectura)
pub fn parse(raw: String) -> Result<Self, &'static str> {
let trimmed = raw.trim();
if trimmed.is_empty() {
Err("Title cannot be empty")
} else {
// Self(...) instancia el tuple struct; la última expresión sin ';' es lo que se retorna
Ok(Self(trimmed.to_string()))
}
}
// 3. Método de instancia: recibe &self para prestar la estructura sin destruirla
pub fn as_str(&self) -> &str {
&self.0 // &self.0 toma prestado el String interno como un slice &str de 16 bytes
}
}
// 4. Módulo de pruebas aislado que solo se compila con rustc --test
#[cfg(test)]
mod tests {
use super::*; // Traemos NonEmptyTitle desde el ámbito superior
#[test]
fn accepts_valid_title() {
let parsed = NonEmptyTitle::parse(String::from(" Rust Guide "));
assert!(parsed.is_ok());
let title = parsed.unwrap();
// Llamamos a as_str(&self): toma prestado title sin destruirlo
assert_eq!(title.as_str(), "Rust Guide");
// Podemos seguir usando title gracias a que as_str usó &self (préstamo)
assert_eq!(title, NonEmptyTitle(String::from("Rust Guide")));
}
#[test]
fn rejects_empty_or_whitespace_only() {
let empty = NonEmptyTitle::parse(String::from(" \n\t "));
// Comparamos directamente contra Err con un literal &'static str
assert_eq!(empty, Err("Title cannot be empty"));
}
}
Ejecución en Windows PowerShell:#
# 1. Compilar el ejecutable de pruebas
rustc --test .\primer_sample.rs -o .\primer_sample-tests.exe
# 2. Ejecutar las pruebas compiladas
.\primer_sample-tests.exe
Salida esperada verificable:
running 2 tests
test tests::accepts_valid_title ... ok
test tests::rejects_empty_or_whitespace_only ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Próximo paso#
Ahora que conoces el vocabulario base de sintaxis, funciones, métodos y memoria de Rust:
- Abre el Laboratorio en Windows para configurar tu carpeta de pruebas y correr tu primera suma.
- O entra directo a la Lección 01: Tipos y flujo de control.