wandres.dev
SWIFT COMPARADO · Rust, Kotlin, Go

Swift y Rust: la memoria como contrato

Dos maneras de garantizar seguridad de memoria sin recolector de basura: el conteo automático de referencias frente al sistema de propiedad y préstamos. Qué asegura cada uno, qué deja fuera de su garantía, cuánto cuesta, y por qué Swift lleva años importando ideas del segundo.

⏱ 20 min

Swift y Rust se presentaron en público con pocos meses de diferencia y con el mismo enemigo declarado: el recolector de basura. Ninguno de los dos aceptaba pausas impredecibles ni un tiempo de ejecución pesado, y ninguno estaba dispuesto a devolverle al programador el malloc y el free. La respuesta de Swift fue automatizar el conteo de referencias en tiempo de compilación y pagar el precio en tiempo de ejecución; la de Rust fue convertir la propiedad en un rasgo del tipo y pagar el precio en la cabeza de quien escribe. Comparar ambos lenguajes por su sintaxis es perder el tiempo: la diferencia real está en dónde cae la carga de la prueba, y esa única decisión explica casi todo lo demás.

🎯 Al terminar esta lección sabrás
  • Explicar ARC y el sistema de propiedad y préstamos como dos soluciones al mismo problema.
  • Delimitar con precisión qué garantiza cada lenguaje y qué queda deliberadamente fuera.
  • Cuantificar el precio de cada elección en rendimiento, ergonomía y tiempo de compilación.
  • Reconocer las piezas de Rust que Swift ha ido incorporando desde 2022 y con qué motivo.

Dos respuestas a la misma pregunta

Swift decide cuándo liberar mirando un contador. Cada referencia fuerte a una instancia de clase suma uno, cada referencia que muere resta uno, y el objeto se destruye en el instante exacto en que el contador llega a cero. El compilador inserta esas operaciones por ti; el programador solo interviene cuando el grafo de referencias tiene ciclos, porque un ciclo mantiene vivos mutuamente a dos objetos que ya nadie más alcanza.

final class Nodo {
    var valor: Int
    var siguiente: Nodo?        // referencia fuerte
    weak var anterior: Nodo?    // debil: rompe el ciclo
    init(_ v: Int) { valor = v }
}

var a: Nodo? = Nodo(1)          // cuenta = 1
let b = a                       // cuenta = 2
a = nil                         // cuenta = 1, el objeto sigue vivo

Rust decide quién libera mirando el tipo. Cada valor tiene un dueño único, asignarlo a otro nombre transfiere la propiedad, y el destructor corre al final del ámbito del dueño actual. No hay contador porque no hace falta: la respuesta ya está escrita estáticamente en el programa.

struct Nodo { valor: i32 }

fn consumir(n: Nodo) { /* n se destruye al terminar la funcion */ }

let a = Nodo { valor: 1 };
consumir(a);
// println!("{}", a.valor);   // error de compilacion: valor movido

Sobre esa base se levanta el borrow checker, cuya regla central cabe en una frase: en cada instante puedes tener muchos lectores o un único escritor, nunca ambos. Ese invariante —alias o mutación, jamás los dos a la vez— es lo que permite a Rust afirmar cosas que Swift no puede afirmar por la misma vía.

Lo que garantiza cada uno

Las garantías se parecen menos de lo que sugiere la etiqueta compartida de «lenguaje seguro en memoria».

Propiedad Swift Rust
Uso después de liberar Imposible en código seguro; el contador lo impide en ejecución Imposible; el borrow checker lo impide en compilación
Fugas por ciclos Posibles; hay que anotar con weak o unowned Imposibles con referencias; posibles al usar Rc
Carreras de datos Prohibidas desde Swift 6 vía aislamiento y Sendable Prohibidas desde el día uno vía Send y Sync
Acceso exclusivo Ley de exclusividad; parte estática, parte dinámica Enteramente estático
Puerta de escape Familia UnsafePointer Bloques unsafe

La cuarta fila esconde una asimetría que casi nadie nota. Swift también prohíbe el acceso simultáneo exclusivo y no exclusivo a la misma variable, exactamente el invariante de Rust, pero solo puede demostrarlo estáticamente para variables locales; para propiedades de clase y globales inserta una comprobación en ejecución que aborta el proceso al detectar la violación.

var puntuaciones = [1, 2, 3]

func duplicar(_ v: inout [Int], usando otro: [Int]) { /* ... */ }

duplicar(&puntuaciones, usando: puntuaciones)
// acceso exclusivo y de lectura a la vez: error en compilacion si es local,
// captura en ejecucion si la variable es una propiedad

La fila decisiva es la tercera, porque revela que los dos lenguajes llegan al mismo destino por caminos distintos. Rust obtiene la ausencia de carreras gratis, como corolario de la regla de alias: si nadie puede tener un puntero mutable y otro lector a la vez, tampoco puede haberlos en dos hilos. Swift no dispone de ese teorema, así que construyó un sistema paralelo —dominios de aislamiento, actores y el protocolo Sendable— que verifica lo mismo con otra álgebra. La consecuencia práctica es que en Swift la seguridad de memoria y la seguridad frente a concurrencia son dos mecanismos separados que hay que aprender por separado, mientras que en Rust son una sola idea vista desde dos ángulos.

ℹ️
La fuga no es un fallo de memoria

Que Swift permita ciclos no rompe su garantía: un objeto filtrado sigue siendo válido, nadie lo lee liberado, no hay corrupción. Perder memoria es un fallo de recursos, no de seguridad. Rust tampoco lo prohíbe del todo, y su documentación lo dice sin rodeos al describir Rc::new con ciclos como comportamiento seguro pero indeseable.

El precio de cada elección

Swift paga en ejecución. Cada retain y cada release son operaciones atómicas sobre un contador compartido, y aunque el optimizador de ARC elimina buena parte del tráfico y las convenciones de llamada distinguen argumentos prestados de argumentos consumidos, el coste residual es real y, peor aún, invisible: una línea inocente que pasa una clase a una clausura puede añadir dos operaciones atómicas que no aparecen en el código fuente. Por eso los perfiles de Swift en bucles calientes están tan a menudo dominados por swift_retain y swift_release.

Rust paga en compilación y en aprendizaje. El sistema de préstamos rechaza programas correctos que no sabe demostrar correctos —las estructuras autorreferenciales, los grafos, los patrones de observador— y obliga a reformularlos con índices, arenas o Rc con RefCell, que reintroducen comprobaciones en ejecución. Esa es la ironía central de la comparación: cuando un programa de Rust necesita de verdad propiedad compartida, la respuesta idiomática es Rc o Arc, es decir, exactamente el conteo de referencias que Swift aplica por defecto a todas sus clases. La diferencia no es el mecanismo, es quién elige y cuándo.

use std::rc::Rc;
use std::cell::RefCell;

// Propiedad compartida y mutacion: ARC manual, con coste explicito
let compartido = Rc::new(RefCell::new(vec![1, 2, 3]));
let otro = Rc::clone(&compartido);          // cuenta = 2
otro.borrow_mut().push(4);                  // comprobacion en ejecucion

Swift está importando el modelo

Desde 2022 el lenguaje incorpora sistemáticamente piezas del vocabulario de Rust, y no por moda: las necesita para bajar al terreno donde ARC no es aceptable. Los tipos no copiables ~Copyable traen la semántica de movimiento única; los modificadores borrowing y consuming hacen explícita la convención de paso que antes era un detalle del compilador; el operador consume marca el último uso de un valor; Span y las dependencias de vida útil permiten pasar vistas sobre memoria sin copiar y sin que el compilador pierda de vista quién es el dueño.

struct Fichero: ~Copyable {
    let descriptor: Int32
    init(abriendo ruta: String) throws { descriptor = try abrir(ruta) }
    consuming func cerrar() { close(descriptor) }
    deinit { close(descriptor) }
}

let f = try Fichero(abriendo: "/tmp/log")
f.cerrar()          // consume el valor
// let g = f        // error: no se puede copiar ni usar tras consumir

La dirección contraria del intercambio también existe, aunque se comente menos: la biblioteca estándar de Rust ofrece Rc y Arc porque el modelo de propiedad única resulta insuficiente para grafos y observadores, y buena parte de su ergonomía moderna —inferencia de vidas útiles, préstamos no léxicos— consiste precisamente en pedir menos anotaciones al programador, que es la dirección en la que Swift partió.

El resultado es un lenguaje de dos velocidades: ARC como opción por defecto cómoda para el noventa por ciento del código, y propiedad explícita como opción para el diez por ciento que la necesita. Rust hace lo contrario: propiedad explícita por defecto y conteo de referencias como excepción declarada. Ambas curvas convergen en el mismo espacio de diseño, pero desde extremos opuestos, y esa asimetría se nota en qué resulta fácil escribir mal en cada uno.

flowchart TD
P[Liberar sin recolector de basura] --> S[Swift con ARC]
P --> R[Rust con propiedad]
S --> S1[Contador en tiempo de ejecucion]
S --> S2[Ciclos posibles, se anotan a mano]
S --> S3[Concurrencia segura por aislamiento]
R --> R1[Verificacion en compilacion]
R --> R2[Alias o mutacion, nunca ambos]
R --> R3[Concurrencia segura como corolario]
S1 --> C[Sin uso despues de liberar]
R1 --> C
style P fill:#f9e2af,color:#11111b
style C fill:#a6e3a1,color:#11111b
⚖️

Dónde cae la prueba

Swift prueba en ejecución con un contador. Rust prueba en compilación con un sistema de tipos afín.

🔁

El ciclo es el hueco

Es lo único que ARC no resuelve solo, y la razón de que weak y unowned sean vocabulario obligatorio.

🧩

Convergencia real

~Copyable, borrowing, consuming y Span traen a Swift el modelo de propiedad allí donde ARC estorba.

La seguridad no se elige, se paga en alguna moneda

Lo que revela esta comparación no es que un lenguaje sea mejor, sino que la seguridad de memoria es un teorema y todo teorema exige premisas que alguien debe suministrar. Rust pide las premisas por adelantado: exige que el programador exprese la estructura de propiedad en el sistema de tipos, y a cambio verifica el teorema antes de generar una sola instrucción, obteniendo ausencia de carreras como corolario gratuito. Swift pide las premisas en diferido: acepta cualquier grafo de referencias, difiere la decisión al momento de ejecutar, y para no perder la seguridad frente a la concurrencia tuvo que construir un segundo sistema formal —aislamiento, Sendable, actores— que demuestra por otra vía lo que Rust obtiene de una sola regla. La enseñanza transferible es que en todo diseño de lenguaje existe una conservación estricta: la información necesaria para probar la corrección no desaparece nunca, solo se traslada entre el compilador, el tiempo de ejecución y el cerebro del programador. Un lenguaje sin anotaciones de vida útil y sin recolector paga con contadores atómicos; uno sin contadores y sin recolector paga con anotaciones; uno sin ninguna de las dos cosas paga con fallos de segmentación. Cuando Swift incorpora tipos no copiables no está imitando a Rust por prestigio, está reconociendo que hay dominios —controladores, códecs, firmware, bucles de audio— donde la moneda del tiempo de ejecución no tiene curso legal y hay que pagar con la otra. Y cuando Rust recomienda Rc para grafos compartidos está reconociendo lo simétrico: hay dominios donde la moneda de la demostración estática resulta prohibitivamente cara. Quien entiende esto deja de preguntar cuál lenguaje es seguro y empieza a preguntar qué está dispuesto a pagar y en qué momento, que es la única forma madura de plantear la elección.

📝
Lo esencial

ARC libera de forma determinista contando referencias en ejecución, y su único hueco son los ciclos. El borrow checker demuestra en compilación que nunca coexisten alias y mutación, y de ahí deriva también la ausencia de carreras. Swift alcanza esa segunda garantía con un sistema aparte de aislamiento y Sendable. Los tipos no copiables y las anotaciones borrowing y consuming acercan Swift al modelo de Rust donde ARC resulta demasiado caro.

⚔️ Mide la moneda que pagas
  1. Escribe un ciclo de retención entre dos clases, obsérvalo con el depurador de memoria y arréglalo con weak, explicando por qué no bastaría unowned.
  2. Perfila un bucle que pase una clase a una función mil millones de veces y localiza el coste de swift_retain en el perfil.
  3. Traduce ese mismo bucle a un struct y a un tipo ~Copyable con borrowing, y compara el código máquina generado.
  4. Implementa una lista doblemente enlazada en Swift y en Rust, y documenta qué te obliga a cambiar el borrow checker.
  5. Argumenta en un párrafo por qué Rc con RefCell en Rust y una clase en Swift ofrecen garantías equivalentes, y en qué se diferencian.