Clase 081 — Semántica de movimiento y préstamo (Rust)

Parte 5 — Funciones y modularidad · ⏱️ Duración estimada: 90 min · Nivel: IntermedioClase construida — 10 implementaciones del núcleo verificadas contra casos.json.


🎯 Objetivo

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».

🧩 Situació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.

🧮 Modelo

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

📖 Definiciones y características

📐 Algoritmo (pseudocódigo neutral)

LEER w ; len <- longitud(prestar w)
mostrar(mover w)
ESCRIBIR "movido=" w " longitud=" len

🌐 Implementaciones idiomáticas — el código a la vista

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 · python/main.py · python main.py

import 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 · javascript/main.mjs · node main.mjs

import { 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 · typescript/main.ts · pnpm exec tsx main.ts

import { 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 · java/Main.java · java Main.java

import 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

C# · csharp/Program.cs · dotnet run

using 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 · go/main.go · go run main.go

package 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 · rust/main.rs · rustc main.rs -o main && ./main

use 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 · 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 · 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 · 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.

🔬 Ejemplo trabajado — del stdin a la salida

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).

🔬 Comparación

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.

🧬 El concepto en la familia

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.

✅ Prueba común

Los mismos casos para todas las implementaciones: casos.json. Verifica la equivalencia:

python scripts/verificar_equivalencia.py 081

🧪 Reto de transferencia

Detalle en reto.md.

⚠️ Errores comunes

❓ Preguntas frecuentes

🔗 Referencias

Libros de la parte:

Libros de los lenguajes del núcleo:


⏮️ Clase 080 · 📂 Parte · 📚 Índice · 🌐 Atlas · Clase 082 ⏭️