wandres.dev
OWNERSHIP · borrowing y consuming

Comparación con Rust: lo compartido y la divergencia deliberada

Qué toma Swift del borrow checker, dónde invierte la polaridad por omisión, por qué renuncia a los lifetimes con nombre y qué precio paga cada lenguaje por su respuesta a la pregunta de quién financia la seguridad.

⏱ 20 min

Es imposible leer borrowing, consuming y ~Copyable sin pensar en Rust, y la genealogía es real: varias de las personas que diseñaron el modelo de propiedad de Swift conocían el borrow checker en detalle y el manifiesto de 2017 cita el parentesco sin rodeos. Pero quedarse en el parecido superficial impide entender lo importante, que son las divergencias, y ninguna de ellas es accidental. Swift no intentó implementar Rust dentro de un lenguaje que ya tenía diez años de código escrito y una promesa de accesibilidad para principiantes. Eligió otra cosa: un modelo de propiedad que se puede ignorar por completo, verificado con el mismo rigor allí donde se usa, y con un techo de expresividad deliberadamente más bajo.

🎯 Al terminar esta lección sabrás
  • Enumerar los conceptos que ambos lenguajes comparten literalmente y no solo por analogía.
  • Explicar la inversión de polaridad por omisión y sus consecuencias sobre el código existente.
  • Contrastar los lifetimes con nombre de Rust con el modelo de fronteras de llamada de Swift.
  • Situar el aliasing, las referencias contadas y la exclusividad en cada uno de los dos sistemas.

Lo que comparten de verdad

El sustrato teórico es el mismo: tipos afines, en los que un valor puede usarse a lo sumo una vez, más un sistema de préstamos que permite acceso temporal sin transferir la propiedad. Las tres convenciones de Swift se corresponden pieza a pieza con las de Rust, y no por casualidad sino por identidad de problema.

fn inspeccionar(r: &Registro) -> usize { r.len() }      // prestamo compartido
fn ajustar(r: &mut Registro) { r.push(0) }              // prestamo exclusivo
fn archivar(r: Registro) { /* se queda con el */ }      // transferencia
func inspeccionar(_ r: borrowing Registro) -> Int { r.count }
func ajustar(_ r: inout Registro) { r.append(0) }
func archivar(_ r: consuming Registro) { }

También coinciden el destructor determinista atado al valor, la verificación estática sin coste en ejecución, la imposibilidad de consumir parcialmente un agregado con destructor y hasta la vía de escape para renunciar a él, que en Rust es olvidar el valor a propósito y en Swift es discard self. El vocabulario difiere; la máquina conceptual, no.

Coincide incluso la forma del análisis. Los dos compiladores construyen el grafo de flujo de la función y comprueban que ningún camino llegue a un uso posterior a un consumo, y los dos formulan sus errores señalando dos puntos del programa, el que consumió y el que usó después. Quien haya discutido con uno reconoce de inmediato la estructura del reproche del otro.

Hay además una coincidencia de fondo que suele pasar desapercibida y que explica por qué las dos soluciones se parecen tanto: los dos lenguajes rechazan el recolector de basura por la misma razón, que no es el rendimiento bruto sino la imposibilidad de dar garantías sobre recursos que no son memoria. Un recolector puede prometer que ningún puntero apuntará a memoria liberada; no puede prometer que un fichero se cierre, que un cerrojo se suelte o que una transacción se confirme, porque no sabe qué significa ninguna de esas cosas. De esa renuncia común nace todo lo demás.

Conviene precisar una equivalencia que casi todo el mundo traduce mal. El Copyable de Swift no corresponde al Copy de Rust, que significa duplicación bit a bit sin destructor, sino más bien a un Clone que se aplicase de forma implícita y automática en cada asignación. Una copia de un tipo Swift puede retener referencias, ejecutar lógica de copia al escribir y tocar el montículo. Por eso suprimirla aporta tanto.

La inversión de polaridad

Aquí está la divergencia fundacional, y de ella se deducen casi todas las demás. En Rust, mover es lo normal y duplicar exige un gesto explícito. En Swift, copiar es lo normal y renunciar a la copia exige el gesto. La misma decisión, tomada al revés.

Rust puede permitírselo porque nació con esa decisión y no arrastraba código previo que invalidar. Swift no podía: cualquier reparto distinto habría invalidado todo lo escrito desde 2014 y habría trasladado el modelo entero al programador que solo quiere una lista de tareas en pantalla. La consecuencia es que el modelo de propiedad de Swift es un dialecto opcional dentro del lenguaje, no su gramática básica. Un programador de aplicaciones puede pasar una carrera entera sin escribir una virgulilla; uno que escriba un controlador de dispositivo la escribirá en la primera hora.

Ese reparto tiene un coste que conviene nombrar con honestidad: dos dialectos conviviendo en un mismo lenguaje, con reglas distintas para lo que parece el mismo código, y una frontera que hay que aprender a reconocer. Rust paga el precio contrario: un impuesto de entrada uniforme para todo programa, incluido el que jamás lo habría necesitado, a cambio de que no haya dos formas de razonar sobre un valor.

La consecuencia sobre las bibliotecas es aún más marcada que sobre el código de aplicación. En Rust, la firma de una función pública comunica su modelo de propiedad a todo el que la lea, y ese contrato es uniforme en el ecosistema entero. En Swift, una biblioteca que quiera admitir valores no copiables debe revisar cada firma genérica y demostrar que su implementación nunca dependía de la copia; por eso la adopción avanza de forma desigual, y por eso la respuesta a si un tipo cualquiera cabe dentro de otro depende de la versión y del cuidado que haya puesto quien escribió el contenedor.

🔁

Polaridad

Rust mueve por omisión y duplica bajo petición. Swift copia por omisión y suprime la copia bajo petición.

🏷️

Duración

Rust nombra las vidas con parámetros genéricos. Swift las ata a la llamada y expresa dependencias sin darles nombre.

🧯

Escape

Rust sale por el bloque inseguro y la mutabilidad interior. Swift sale por la clase y su recuento de referencias.

Lifetimes con nombre frente a fronteras de llamada

La diferencia más visible al escribir código es que Swift no tiene parámetros de duración. En Rust, una referencia es un tipo de primera clase parametrizado por una vida, y eso permite devolver una referencia a un dato ajeno, guardarla en una estructura o relacionar entre sí las duraciones de varios argumentos. Es la fuente de la potencia del sistema y también la de su curva de aprendizaje.

Swift ata los préstamos al ámbito de la llamada: un parámetro borrowing vale mientras dura la función, y no existe forma de devolverlo como tal. La necesidad real que eso deja fuera, ofrecer una vista sobre memoria ajena que no la posea, se resolvió por otra vía: una segunda supresión que marca a un tipo como no escapable, junto con anotaciones que declaran de qué argumento depende el valor devuelto. Es más estrecho que un sistema de lifetimes con nombre y también mucho más pequeño de aprender, y esa proporción es exactamente la apuesta de diseño.

La segunda diferencia estructural es el tratamiento del aliasing. El borrow checker de Rust impone globalmente que haya un préstamo exclusivo o muchos compartidos, nunca ambos, y lo verifica de forma estática sobre todo el programa. Swift tiene la ley de exclusividad, que enuncia una regla emparentada pero se aplica a accesos a variables, y admite comprobación dinámica en los casos donde el análisis estático no alcanza, señaladamente en propiedades de clase y en variables globales. Ahí Swift acepta un fallo en ejecución donde Rust exige una demostración en compilación.

flowchart TB
p[Como se garantiza la seguridad de memoria]
p --> r[Rust]
p --> s[Swift]
r --> r1[Mover por omision]
r --> r2[Lifetimes con nombre]
r --> r3[Aliasing verificado de forma estatica]
r --> r4[Escape mediante bloque inseguro]
s --> s1[Copiar por omision]
s --> s2[Prestamos atados a la llamada]
s --> s3[Exclusividad estatica y a veces dinamica]
s --> s4[Escape mediante clases y recuento]
r3 --> u[Ambos evitan uso despues de liberar]
s4 --> u
⚠️
La exclusividad dinámica no es un detalle menor

Cuando Swift no puede demostrar estáticamente que dos accesos no se solapan, inserta una comprobación en ejecución que aborta el proceso si se viola la regla. Eso significa que una clase de errores que en Rust es imposible compilar, en Swift es un fallo en producción, con el coste añadido de la comprobación. La contrapartida es que el programador nunca queda bloqueado ante un análisis que no entiende, y ese fue el intercambio elegido de forma consciente.

La tercera divergencia es la existencia misma de las clases. Rust ofrece el recuento de referencias como tipos de biblioteca que el programador elige de forma consciente, y la mutación a través de ellos exige mutabilidad interior con reglas propias. Swift lleva el recuento incorporado en el lenguaje y lo aplica a toda clase, lo que significa que la vía de escape del modelo de propiedad está siempre disponible y no requiere ninguna ceremonia: si un valor único te complica la vida, existe la opción de envolverlo en una clase y volver al mundo compartido. Esa puerta abierta es la razón por la que adoptar el dialecto de propiedad en Swift resulta gradual, y también la razón por la que resulta fácil abandonarlo por comodidad sin advertir lo que se pierde.

Cuatro asimetrías que se notan al escribir

La primera aparece en la firma. En Rust, la convención está en el tipo del parámetro y por tanto la ve el llamante con solo mirar la declaración; en Swift está en un modificador delante del tipo, y sobre un tipo copiable el llamante puede ignorarla porque el compilador insertará lo que haga falta. Eso hace las firmas de Swift más legibles y menos informativas a la vez.

La segunda aparece en los errores. El borrow checker de Rust explica su rechazo en términos de duraciones que se solapan, y esa explicación puede ser densa pero es completa. Swift, al no tener duraciones con nombre, formula sus diagnósticos en términos de consumos y usos, con un vocabulario más corto que a veces deja al programador sin la información necesaria para entender por qué el análisis se rindió.

La tercera aparece al construir estructuras de datos. Una lista enlazada, un árbol con punteros al padre o un grafo obligan en Rust a elegir de forma consciente entre recuento, índices en un vector o punteros crudos dentro de un bloque inseguro. En Swift el mismo problema se resuelve con una clase y no exige ninguna decisión, lo que es una ventaja de productividad y a la vez la razón por la que el modelo de propiedad de Swift se abandona con tanta facilidad.

La cuarta aparece en la interoperabilidad, y ahí las dos filosofías por fin coinciden. Cuando la propiedad de un puntero cruza la frontera con C, ambos lenguajes ofrecen el mismo remedio: envolverlo en un tipo con dueño único y destructor, de modo que la convención documentada en la cabecera se convierta en una regla verificada.

struct Contexto: ~Copyable {
    private let crudo: OpaquePointer
    init?(config: Int32) {
        guard let p = crear(config) else { return nil }
        crudo = p
    }
    deinit { destruir(crudo) }
}
struct Contexto(*mut ffi::Ctx);

impl Drop for Contexto {
    fn drop(&mut self) { unsafe { ffi::destruir(self.0) } }
}

En ambos casos el tipo envolvente no añade ni un byte al puntero que guarda, y en ambos el destructor se ejecuta en un punto conocido sin consultar contador alguno. La diferencia se reduce a dónde vive la declaración de intención: en Swift, en la supresión escrita junto al nombre del tipo; en Rust, en el hecho de que no exista una implementación de duplicado.

Los dos fragmentos dicen exactamente lo mismo y el parecido no es superficial: son la misma idea con dos gramáticas. Quien haya escrito uno sabe leer el otro, y esa transferencia de conocimiento es probablemente el argumento más práctico para aprender los dos modelos aunque solo se vaya a usar uno.

💡
Traducción falsa que conviene evitar

Traducir mentalmente borrowing como referencia compartida y consuming como paso por valor funciona en las firmas sencillas y engaña en cuanto aparecen tipos copiables. En Swift, sobre un tipo copiable, el compilador puede satisfacer cualquiera de las dos convenciones insertando una copia, de modo que la firma deja de ser un contrato y pasa a ser una preferencia. En Rust esa salida no existe nunca.

Quién financia la seguridad

Detrás de cada divergencia técnica hay una sola pregunta de economía del lenguaje, y formularla bien explica más que cualquier tabla comparativa: ¿quién paga por la seguridad de memoria y cuándo? Rust responde que la paga todo programa por adelantado y en la misma moneda. Cada valor tiene un dueño, cada referencia una duración, cada programador conoce el borrow checker desde el primer ejercicio, y a cambio el lenguaje ofrece una garantía uniforme sin regiones privilegiadas y sin dos maneras de razonar sobre un dato. Swift responde que la paga quien la necesita, en el momento en que la necesita. El programa corriente vive con recuento de referencias y copias implícitas, que son seguras aunque no gratis, y solo el código que exige control determinista adopta el dialecto de propiedad, con verificación tan estricta como la de Rust dentro de su ámbito. Las dos respuestas son coherentes y ninguna es superior en abstracto: dependen de qué distribución de programas espera servir el lenguaje. La de Rust hace excelente el código de sistemas y cara la aplicación trivial. La de Swift hace accesible la aplicación trivial y deja el código de sistemas con un techo de expresividad más bajo, porque sin lifetimes con nombre hay estructuras de datos que no se pueden describir sin salir a punteros no seguros. Lo que sí es evaluable es la coherencia de cada apuesta con su objetivo declarado, y ahí ambos lenguajes salen bien parados. Lo que no funciona, y es el error que comete casi todo el que llega de un lenguaje al otro, es juzgar cada decisión con la vara del contrario: exigirle a Swift lifetimes con nombre es no haber entendido que su objetivo es que la mayoría nunca los vea, y exigirle a Rust copias implícitas es no haber entendido que su objetivo es que nadie pueda ignorar el coste de duplicar.

📝
Lo esencial de la comparación

Ambos lenguajes comparten tipos afines, préstamos, destructor determinista y verificación estática sin coste en ejecución. Swift invierte la polaridad por omisión, no tiene lifetimes con nombre y ata los préstamos a la llamada, acepta exclusividad dinámica donde Rust exige demostración estática, y mantiene siempre abierta la vía de escape de las clases con recuento. Su Copyable se parece a un Clone implícito, no al Copy de Rust.

⚔️ Traduce en ambos sentidos
  1. Traduce a Rust una estructura Swift no copiable con destructor y anota qué pieza necesita cada lenguaje que la otra no.
  2. Escribe en Rust una función que devuelva una referencia a un campo de su argumento e intenta expresar lo mismo en Swift.
  3. Compara los diagnósticos de uso después de mover en ambos compiladores sobre el mismo error conceptual.
  4. Construye un caso donde Swift detecte una violación de exclusividad en ejecución y explica por qué Rust la habría rechazado antes.
  5. Toma una estructura de datos con referencias cruzadas y argumenta qué vía de escape usarías en cada lenguaje y qué garantía pierdes con ella.