Swift y Objective-C

[Swift avanzado #8] ~Copyable, propiedad y tipos no copiables

Los recursos que solo tienen sentido cuando son únicos, como los descriptores de archivo o los bloqueos, no deben copiarse. Aquí se explica cómo marcar la no copiabilidad en un tipo con ~Copyable de Swift 5.9, junto con borrowing, consuming e inout y sus usos.

6 min de lectura
Imagen de portada de [Swift avanzado #8] ~Copyable, propiedad y tipos no copiables

En el capítulo sobre tipos por valor resumimos el comportamiento predeterminado de Swift como «copiar»: asignar copia y pasar a una función también copia.

Este artículo continúa el anterior Swift avanzado #7.

Pero ¿qué ocurre si hay valores que no deben copiarse? Hablamos de recursos que solo tienen sentido cuando existe «exactamente uno», como descriptores de archivo, bloqueos mutex y tokens de transferencias bancarias.

El tema de esta entrega de la serie avanzada es la propiedad (ownership). ~Copyable de Swift 5.9 (tipos no copiables), borrowing y consuming abren esa puerta. SE-0390: Noncopyable Structs and Enums

Si conoces Rust, te resultará familiar. Exacto: Swift ha incorporado las ideas centrales de Rust a su manera.

Pero la dirección es distinta. Rust hace que la propiedad sea lo predeterminado y no admite excepciones, mientras que Swift parte de la copia y ofrece la propiedad como herramienta opcional.

El hueco de un mundo donde copiar es lo predeterminado

Todos los tipos de Swift son Copyable de forma predeterminada. Aunque no lo declares, el compilador hace que adopten el protocolo implícitamente.

De ahí surge la semántica de copia de la asignación y la transferencia que vimos en el capítulo de tipos por valor. Es el valor predeterminado perfecto para la mayoría de valores: números, cadenas y coordenadas.

El problema son los tipos que representan recursos. Pensemos en un struct que envuelve un descriptor de archivo.

struct FileHandle {
    let fd: Int32
    func close() { /* fd Cerrar */ }
}

let a = FileHandle(fd: open("data.txt"))
let b = a          // Copiar: ahora dos conocen el mismo fd
a.close()
b.close()          // Cerrar de nuevo un fd ya cerrado: comportamiento indefinido

En cuanto se copia el descriptor, «¿quién es responsable de cerrarlo?» se vuelve ambiguo. Las copias no autorizadas originan errores de recursos como la doble liberación y el uso de descriptores cerrados.

La solución habitual hasta ahora era usar una clase y cerrar en deinit, unificando la gestión mediante una sola referencia.

Funciona, pero tiene los costes vistos en el capítulo de ARC (Automatic Reference Counting, conteo automático de referencias): el heap y el conteo de referencias.

Además, el compilador sigue sin conocer la regla «no se debe copiar».

~Copyable: marcar la no copiabilidad en el tipo

La respuesta de Swift 5.9 son los tipos no copiables (propuesta Swift Evolution SE-0390). El ~Copyable con tilde declara que «no adopta Copyable». SE-0390: Noncopyable Structs and Enums

struct FileHandle: ~Copyable {
    let fd: Int32

    consuming func close() { /* fd Cerrar */ }
    deinit { /* Cerrar aquí si todavía no se ha cerrado */ }
}

let a = FileHandle(fd: open("data.txt"))
let b = a          // No es una copia, sino un movimiento: la propiedad pasa a b
// print(a.fd)     // Error de compilación: a ya se ha consumido

El comportamiento cambia por completo. La asignación se convierte en un movimiento (move), no en una copia, y la variable cuya propiedad se transfirió no puede usarse desde ese momento.

La infracción produce un error de compilación, no un fallo en tiempo de ejecución. El compilador lleva un registro de la regla «este valor tiene exactamente un propietario en todo momento».

Al igual que Optional convirtió nil y Sendable las carreras en problemas de tipos, la unicidad de los recursos pasa a ser un problema de tipos.

Además, un struct puede declarar deinit, por lo que la limpieza determinista se ejecuta cuando su propietario sale del ámbito.

Esto permite gestionar recursos al estilo RAII (Resource Acquisition Is Initialization) sin clases.

Diagrama que compara los errores de doble liberación causados por copias con una estructura que los evita mediante movimientos
La clase de errores llamada doble liberación se convierte en un error de compilación

borrowing y consuming: tres formas de pasar valores a funciones

Al pasar un valor no copiable a una función aparece una nueva pregunta. Como no puedes copiarlo, debes decidir si lo prestas o transfieres su propiedad.

Los modificadores de parámetros expresan esa decisión.

borrowing significa préstamo. La función solo lee el valor y la propiedad permanece en el llamador.

El llamador puede seguir usando el valor después de que la función devuelva el control. func checksum(of handle: borrowing FileHandle) -> Int Es el lugar para operaciones de solo lectura como esta.

consuming significa transferencia. La propiedad pasa a la función y el llamador ya no puede usar el valor. El consuming func close() anterior tiene exactamente este significado.

Usar el descriptor después de llamar a close se convierte en un error de compilación: la clase de errores «usar otra vez un descriptor cerrado» desaparece del nivel sintáctico.

También sirve para modelar conceptos de dominio que deben desaparecer al usarse, como tokens de transferencias bancarias y tickets de un solo uso.

inout conserva el comportamiento conocido: prestarlo y modificarlo. Juntos completan el vocabulario de propiedad de los parámetros: solo leer (borrowing), llevarse (consuming) y modificar (inout).

Estos modificadores también pueden aplicarse a tipos Copyable. En ese caso son una indicación de rendimiento, no una diferencia semántica.

Sustituyen las operaciones retain/release o las copias que puede generar la convención predeterminada por préstamos y movimientos para reducir el tráfico de ARC. Exigir un movimiento explícito con el operador consume (let b = consume a) pertenece a la misma familia.

Aun así, es un ámbito de microoptimización en el que primero hay que medir. La advertencia sobre la comodidad del dispatch también se aplica aquí.

Criterio práctico: dónde usarlo y dónde no

Es importante ubicar con precisión la posición actual de esta función.

Encaja con la propiedad única de recursos. Por ejemplo, wrappers de descriptores de archivos y sockets, tokens de bloqueo, protectores de transacciones y derechos de acceso al hardware.

Swift embebido fue uno de los principales impulsores de esta función. En microcontroladores, el heap y ARC resultan costosos, así que hacía falta gestionar recursos de forma segura sin clases.

Los valores que entrega Mutex de la biblioteca estándar y tipos recientes como Span pertenecen a esta línea.

El valor predeterminado del código de aplicaciones normales sigue siendo un struct Copyable. El principio orientado a valores permanece intacto.

Aplicar ~Copyable por adelantado a los datos de dominio es sobrediseño. También hay fricción con el ecosistema genérico: los tipos no copiables no se mezclan directamente con genéricos y colecciones existentes que asumen Copyable, y el lenguaje todavía lo está resolviendo gradualmente.

Por ahora, debe reservarse como herramienta especializada para tipos en los que la respuesta a «¿copiarlo sería un error?» sea afirmativa.

Para cerrar con una comparación con Rust, Rust es un lenguaje de «propiedad predeterminada» que impone reglas de propiedad a todos los valores y exige incluso anotaciones de lifetime.

Swift mantiene un mundo donde copiar es lo predeterminado y traslada a la propiedad solo los tipos que lo necesitan: «propiedad opcional».

Es un ejemplo clásico de Progressive Disclosure. El código de quien no conoce la propiedad no contiene este concepto; solo se abre el siguiente nivel para quien lo necesita.

Diagrama que compara borrowing, consuming e inout con tres ventanillas
Solo leer (borrowing), llevarse (consuming), modificar (inout)

Resumen

  • Todos los tipos son implícitamente Copyable, y la copia no autorizada de tipos de recursos es el origen de los errores de doble liberación y similares.
  • ~Copyable (SE-0390) prohíbe copiar, convierte la asignación en un movimiento y hace que usar una variable consumida sea un error de compilación. También permite deinit en structs para una limpieza determinista.
  • Vocabulario de propiedad de parámetros: borrowing (solo leer, préstamo), consuming (llevarse, transferencia) e inout (modificar). Los métodos consuming incorporan a la sintaxis el significado de «desaparece al usarse».
  • Su lugar son los recursos de propiedad única (descriptores, bloqueos, tokens y sistemas embebidos); para los datos normales, el valor predeterminado sigue siendo un struct Copyable. A diferencia de la propiedad universal de Rust, Swift usa propiedad opcional.

La próxima entrega es el último tema de la serie avanzada y de toda la serie Swift: la historia de cómo «la aplicación se redujo de repente en Swift 5», la estabilidad de ABI (Application Binary Interface). SE-0390: Noncopyable Structs and Enums

Fuentes y criterios de verificación

  • SE-0390: Noncopyable Structs and Enums — Swift Evolution · texto original de estándares y especificaciones · verificado el 2026-08-17 · base: ~Copyable, consuming, borrowing y reglas de tipos no copiables de Swift 5.9

Seguir leyendo