Parte 5 — Funciones y modularidad · ⏱️ Duración estimada: 90 min · Nivel: Intermedio ✅ Clase construida — 10 implementaciones del núcleo verificadas contra
casos.json.
Comprender el modelo con el que Rust gestiona la memoria sin recolector de basura y sin la gestión manual de C: la propiedad (ownership), el movimiento (move) y el préstamo (borrow). La idea central es tan simple de enunciar como profunda en sus consecuencias: en Rust, cada valor tiene en todo momento un único dueño, y cuando el dueño sale de ámbito, el valor se libera automáticamente. Pasar un valor a una función puede moverlo —transferir la propiedad, tras lo cual la variable original deja de ser válida— o prestarlo con & —dar acceso temporal para leerlo sin ceder la propiedad—. Es la tercera vía entre los dos mundos que ya viste: ni la copia defensiva del paso por valor, ni la mutación libre del paso por referencia, sino un sistema donde el compilador demuestra que cada acceso es seguro.
Steve Klabnik y Carol Nichols dedican el capítulo 4 de The Rust Programming Language —disponible gratis en línea— a este modelo, y lo presentan como el rasgo que distingue a Rust de todo lo demás. Su tesis: los lenguajes tradicionales te obligan a elegir entre control (C, con memoria manual y el riesgo de use-after-free o doble liberación) y comodidad (Java, Python, Go, con recolector de basura que cuesta rendimiento y latencia impredecible). Rust rechaza el dilema: mueve la comprobación al compilador. El verificador de préstamos (borrow checker) analiza estáticamente que nunca uses un valor después de moverlo, que no haya dos referencias mutables simultáneas, y que ninguna referencia sobreviva al dato que apunta. Si el programa compila, esas familias enteras de bugs no pueden ocurrir.
El objetivo hondo es ver el movimiento y el préstamo no como sintaxis exótica, sino como una respuesta de ingeniería a una pregunta vieja: ¿quién libera esta memoria y cuándo? C responde «tú, a mano, y reza». Java responde «un recolector, cuando le parezca». Rust responde «el dueño, al final de su ámbito, y lo garantizo en compilación».
Tienes una cadena de texto y quieres dos cosas: medir su longitud y luego mostrarla. En un lenguaje con recolector no lo piensas dos veces: usas la variable las veces que quieras y el GC limpiará después. En C tendrías que decidir tú cuándo hacer free, con el riesgo de liberarla dos veces o de usarla ya liberada. En Rust el compilador te obliga a ser explícito sobre la intención: medir la longitud solo necesita leer, así que la prestas (&s) y la conservas; mostrarla en una función que se vuelve dueña de ella la mueve, y a partir de ahí el compilador no te dejará volver a usar s porque ya no es tuya. Esa disciplina, que al principio incomoda, es exactamente la que impide que uses por accidente un recurso que ya cediste. La situación de esta clase —prestar para leer, mover para consumir— es el «hola mundo» de la propiedad en Rust.
movido=<palabra> longitud=<len>Especificación y verificación en casos.json:
| stdin | esperado |
|---|---|
Ada |
movido=Ada longitud=3 |
Bo |
movido=Bo longitud=2 |
hola |
movido=hola longitud=4 |
String, que posee memoria en el heap) transfiere la propiedad: la variable de origen queda invalidada y el compilador prohíbe seguir usándola. No es una copia; es un traspaso de la responsabilidad de liberar. Evita que dos variables crean ser dueñas del mismo dato y lo liberen dos veces.& para usar un valor sin tomar su propiedad. El dueño sigue siendo el original; el préstamo es un acceso temporal. Un préstamo compartido &T permite leer; un préstamo mutable &mut T permite escribir, con la regla de que no pueden coexistir dos préstamos mutables (ni uno mutable con otros compartidos) al mismo tiempo.Copy vs. Clone — no todo se mueve. Los tipos simples que caben en la pila (enteros, bool, char) implementan el rasgo Copy: al pasarlos se copian en vez de moverse, así que el original sigue válido (como viste en la clase 079). Los tipos que poseen recursos del heap (String, Vec) se mueven por defecto; para duplicarlos de verdad hay que pedirlo explícitamente con .clone(), que copia también el contenido del heap.LEER w ; len <- longitud(prestar w)
mostrar(mover w)
ESCRIBIR "movido=" w " longitud=" len
Mismo algoritmo, forma idiomática en cada lenguaje. Todas producen la salida de casos.json.
Cada bloque es el archivo real de implementaciones/: el enlace de cada lenguaje abre su fuente, y el comando de al lado lo ejecuta.
python/main.py · python main.pyimport sys
s = sys.stdin.readline().strip()
longitud = len(s) # Python comparte la referencia (GC), no hay 'move'.
print(f"movido={s} longitud={longitud}")
🧬 El mismo programa en la familia Scripting dinámico: Ruby · Perl · Lua · Tcl · R
javascript/main.mjs · node main.mjsimport { readFileSync } from "node:fs";
const s = readFileSync(0, "utf8").trim();
const longitud = s.length; // JS usa GC: la cadena sigue disponible.
console.log(`movido=${s} longitud=${longitud}`);
🧬 El mismo programa en la familia JavaScript / web: Dart · ActionScript
typescript/main.ts · pnpm exec tsx main.tsimport { readFileSync } from "node:fs";
const s: string = readFileSync(0, "utf8").trim();
const longitud: number = s.length;
console.log(`movido=${s} longitud=${longitud}`);
🧬 El mismo programa en la familia JavaScript / web: Dart · ActionScript
java/Main.java · java Main.javaimport java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String s = br.readLine().trim();
int longitud = s.length(); // GC: sin propiedad ni move.
System.out.println("movido=" + s + " longitud=" + longitud);
}
}
🧬 El mismo programa en la familia JVM: Kotlin · Scala · Groovy · Clojure
csharp/Program.cs · dotnet runusing System;
string s = Console.In.ReadToEnd().Trim();
int longitud = s.Length; // GC: la cadena permanece.
Console.WriteLine($"movido={s} longitud={longitud}");
🧬 El mismo programa en la familia .NET: F# · VB.NET
go/main.go · go run main.gopackage main
import (
"bufio"
"fmt"
"os"
"strings"
)
func main() {
line, _ := bufio.NewReader(os.Stdin).ReadString('\n')
s := strings.TrimSpace(line)
longitud := len(s) // GC: sin propiedad explícita.
fmt.Printf("movido=%s longitud=%d\n", s, longitud)
}
🧬 El mismo programa en la familia Sistemas: Zig · Nim · D
rust/main.rs · rustc main.rs -o main && ./mainuse std::io::Read;
fn longitud(s: &str) -> usize {
s.len() // préstamo: se lee sin tomar la propiedad
}
fn mostrar(s: String) {
// move: 'mostrar' se vuelve dueña de la cadena
let len = s.len();
println!("movido={s} longitud={len}");
}
fn main() {
let mut buf = String::new();
std::io::stdin().read_to_string(&mut buf).unwrap();
let s = buf.trim().to_string();
let _ = longitud(&s); // se presta
mostrar(s); // se mueve
}
🧬 El mismo programa en la familia Sistemas: Zig · Nim · D
c/main.c · cc main.c -o main && ./main#include <stdio.h>
#include <string.h>
int main(void) {
char s[256];
if (scanf("%255s", s) != 1) return 1;
/* C: gestión manual; aquí no se copia ni se mueve, se usa directamente. */
int longitud = (int) strlen(s);
printf("movido=%s longitud=%d\n", s, longitud);
return 0;
}
🧬 El mismo programa en la familia C / llaves: C++ · Objective-C
sql/main.sql · sqlite3 :memory: < main.sql-- SQL no tiene propiedad de memoria: opera sobre datos.
WITH palabras(s) AS (VALUES ('Ada'), ('Bo'), ('hola'))
SELECT printf('movido=%s longitud=%d', s, length(s)) AS resultado FROM palabras;
🧬 El mismo programa en la familia Lógica y declarativa: Prolog · Datalog
php/main.php · php main.php<?php
$s = trim(fgets(STDIN));
$longitud = strlen($s); // PHP usa GC por conteo de referencias.
echo "movido=$s longitud=$longitud\n";
🧬 El mismo programa en la familia Scripting dinámico: Ruby · Perl · Lua · Tcl · R
SQL es declarativo: no lee de stdin como los demás; su implementación muestra la misma idea sobre una tabla de casos, y el verificador la marca como ilustrativa.
Sigamos el primer caso de casos.json (stdin = "Ada", esperado = "movido=Ada longitud=3"), primero en Rust —donde el concepto es protagonista— y luego en dos lenguajes que lo resuelven sin propiedad.
Rust — préstamo y luego movimiento. En main, tras leer la entrada, let s = buf.trim().to_string() crea un String cuyo dueño es s; ese String posee memoria en el heap con los bytes A, d, a. La línea let _ = longitud(&s) presta la cadena: &s construye una referencia &str que la función longitud recibe como s: &str. Como es un préstamo y no un movimiento, longitud puede leer s.len() —que devuelve 3— pero al terminar no libera nada ni se queda con la propiedad; s en main sigue siendo el dueño, plenamente válido. Justo después, mostrar(s) mueve la cadena: la firma mostrar(s: String) toma un String por valor, así que la propiedad se transfiere de main a mostrar. A partir de esa línea, la variable s de main queda invalidada —si intentaras usarla, el compilador lo rechazaría—. Dentro de mostrar, la función es ahora la dueña: calcula len = 3, imprime movido=Ada longitud=3, y al terminar su ámbito libera la cadena automáticamente (drop). Fíjate en el orden que impone la lógica: se presta primero (para medir) y se mueve al final (para consumir), porque después del movimiento ya no habría cadena que prestar.
Python — sin propiedad, con recolector. s = sys.stdin.readline().strip() deja s apuntando a la cadena "Ada". No hay préstamo ni movimiento: len(s) lee la longitud (3) y s sigue disponible sin ceremonia alguna. El f-string f"movido={s} longitud={3}" produce movido=Ada longitud=3. La memoria de la cadena vive mientras haya referencias a ella, y el recolector de Python (conteo de referencias más un detector de ciclos) la liberará cuando ya nadie la use. El programador nunca decide cuándo; ese es precisamente el coste y la comodidad del GC que Rust evita.
C — gestión manual, sin ninguno de los dos conceptos. scanf("%255s", s) llena el arreglo char s[256] con Ada\0. No hay String con dueño ni referencia prestada: s es un buffer en la pila, y strlen(s) recorre los bytes hasta el \0 para devolver 3. La salida es movido=Ada longitud=3. Como el buffer está en la pila (no en el heap con malloc), aquí ni siquiera hay que liberar nada; pero si lo hubiéramos reservado dinámicamente, sería responsabilidad del programador hacer free en el momento correcto —ni antes (uso tras liberar) ni dos veces (doble liberación)—, exactamente los errores que el modelo de Rust vuelve imposibles. El tercer caso, hola, recorre lo mismo en los tres: cuatro bytes, longitud=4, salida movido=hola longitud=4. Mismo resultado, tres filosofías de memoria: propiedad comprobada (Rust), recolección automática (Python), gestión manual (C).
| Lenguaje | Cómo gestiona la memoria del texto |
|---|---|
| Python | Recolector por conteo de referencias; sin propiedad ni movimiento, la cadena vive mientras se use. |
| JavaScript | Recolector traza-y-marca; la cadena permanece disponible sin gestión explícita. |
| TypeScript | Igual que JS en runtime; los tipos no cambian la gestión de memoria. |
| Java | Recolector de la JVM; las cadenas son objetos, sin propiedad ni move. |
| C# | Recolector del CLR; la cadena es un objeto gestionado que sobrevive mientras haya referencias. |
| Go | Recolector concurrente; sin propiedad explícita, el runtime libera lo inalcanzable. |
| Rust | Propiedad + préstamo + movimiento, comprobados por el borrow checker; sin recolector. |
| C | Gestión manual (malloc/free); ni copia ni movimiento automáticos, control y riesgo totales. |
| SQL | No hay propiedad de memoria del usuario; el motor opera sobre datos de las filas. |
| PHP | Recolector por conteo de referencias con detección de ciclos; sin move. |
La síntesis, siguiendo a Klabnik y Nichols, es que estos tres conceptos son la respuesta de Rust a un compromiso que los demás lenguajes resuelven en extremos opuestos. Los lenguajes con recolector (Python, JS, Java, C#, Go, PHP) regalan comodidad —usa el valor las veces que quieras— a cambio de un coste en tiempo de ejecución y pausas de latencia impredecibles. C regala control total a cambio de poner sobre el programador toda la responsabilidad, con el use-after-free y la doble liberación siempre acechando. Rust rechaza el dilema: consigue seguridad de memoria sin recolector moviendo la comprobación al compilador. El precio no es rendimiento en ejecución, sino una curva de aprendizaje: convencer al borrow checker de que tu programa es correcto.
C++ es el pariente más cercano: tiene semántica de movimiento explícita con std::move y move constructors desde C++11, además de referencias & y punteros inteligentes (unique_ptr, shared_ptr) que modelan propiedad. La diferencia crucial con Rust es que C++ no comprueba en compilación que no uses un objeto tras moverlo: hacerlo es undefined behavior, un bug silencioso, mientras que en Rust es un error de compilación. Swift gestiona la memoria con conteo automático de referencias (ARC), a medio camino entre el GC y la propiedad de Rust. Java, Go y Python se apoyan por completo en recolectores y no exponen el concepto de propiedad al programador. Reconocer dónde cae cada lenguaje —comprobación estática de propiedad, movimiento sin comprobar, o recolección— explica de un vistazo qué garantías te da y qué errores te deja cometer.
Los mismos casos para todas las implementaciones: casos.json. Verifica la equivalencia:
python scripts/verificar_equivalencia.py 081
Detalle en reto.md.
String a una función (que lo mueve) y luego intentar usarlo de nuevo; el compilador responde con «value borrowed here after move» → solución: si necesitas seguir usándolo, préstalo con & en vez de moverlo, o clónalo con .clone() si de verdad necesitas dos dueños..clone() por todas partes para evitar los errores de propiedad, pagando copias caras del heap innecesariamente → solución: preferir préstamos &; reservar .clone() para cuando realmente necesites una copia independiente.&s duplica la cadena → solución: un préstamo es solo una referencia, un puntero con reglas; no copia el dato, por eso es barato y por eso el dueño no cambia.String grande sería caro; mover solo transfiere la propiedad del puntero al heap.&s) es una referencia: un puntero con reglas de seguridad verificadas por el compilador. No duplica el contenido; da acceso temporal mientras el dueño conserva la propiedad.Copy (enteros, bool, char, f64...) se copian al pasarse, así que la variable original sigue válida. El movimiento aplica a los tipos que poseen recursos del heap, como String o Vec, que no son Copy por defecto.Libros de la parte:
Libros de los lenguajes del núcleo:
malloc/free.⏮️ Clase 080 · 📂 Parte · 📚 Índice · 🌐 Atlas · Clase 082 ⏭️