Preguntas
¿Qué son los value types en Swift y cómo se diferencian de los reference types?
Una instancia de un "value-type" mantiene una única copia de sus datos. Cuando se asigna a una variable o se pasa a una función, se crea una copia.
Por otro lado, una sola instancia de un "reference-type" puede ser referenciada por varios punteros. Al asignarse a una variable o pasarse a una función, se pasa una referencia a la misma posición de memoria.
¿Cuándo un struct o enum recibe conformidad implícita a Sendable?
-
structoenumque no seapublicy no esté marcado con@usableFromInline. -
structoenumcongeladas ("frozen"), incluso si son públicas.
La siguiente struct es Sendable por defecto:
// ✅ Conforma Sendable por implícitamente
struct Person {
var name: String
}
Si se la hace public deja de serlo:
// ❌ NO es Sendable de forma implícita
public struct Person {
var name: String
}
Debido a que Swift oculta los detalles internos (como las variables privadas) de los tipos de datos públicos, el compilador es incapaz de verificar automáticamente que sea "thread-safe". Para curarse en salud, obliga al desarrollador a conformar Sendable de forma explícita.
¿Cómo influye @usableFromInline en la conformidad implícita de Sendable?
Cuando el compilador puede garantizar que un struct exista solo dentro de un módulo específico (i.e. no es public), entonces "regala" un Sendable implícito. En caso contrario, el desarrollador debe decidir explícitamente si conforma Sendable o no.
Lo anterior se debe hacer para tener una especie de "contrato" con los clientes del API:
- Si el compilador infiere
Sendableimplícito en unstructpublicy luego el tipo de dato cambia, el contrato se rompe en los clientes que ya lo usaban comoSendable. - Al obligar al desarrollador a conformar
Sendablede forma explícita, si el día de mañana el tipo de dato cambia, el compilador primero se estalla al compilar el API.
Ahora bien, @usableFromInline le permite a otros módulos clientes usar el tipo de dato dentro del código @inlinable. Cuando un código se hace de tipo "inline", el cliente copia el código fuente en tiempo de compilación. Si al principio se usaba el tipo de dato como Sendable y luego cambia, entonces se rompería el contrato en los clientes.
¿Por qué los enums públicos no marcados como @frozen no son implícitamente Sendable?
Cuando un struct o enum es marcado como public, el compilador no lo puede marcar implícitamente como Sendable porque no puede garantizar que el día de mañana se le agregue un miembro que no sea Sendable y rompa el contrato. Por esta razón, el desarrollador debe marcar el tipo de dato con Sendable de forma explícita para que el compilador pueda analizarlo.
Sin embargo, cuando un struct o enum es marcado con @frozen, el desarrollador está asegurando que la interfaz del tipo de dato no va a cambiar en el futuro. En este caso, el compilador puede marcarlo como Sendable de forma implícita, si los miembros también son Sendable.
El "Modelo de Evolución de Bibliotecas" ("Library Evolution Model") permite al cliente de una biblioteca cargar una nueva versión de la biblioteca sin necesidad de recompilar otra vez el módulo.
Los enums son el caso más delicado. Supongamos que construimos una biblioteca v1, con un enum:
public enum Resultado {
case exito(String)
case error(String)
}
Se puede compilar el siguiente cliente contra la biblioteca v1 y todo funciona.
// ✅ El compilador está satisfecho porque se cubren todos los casos
switch resultado {
case .exito(let result):
// ...
case .error(let error):
// ...
}
Meses después, hago el siguiente cambio en Resultado y lanzo v2:
public enum Resultado {
case exito(String)
case error(String)
case pendiente // ⚠️ Nuevo caso
}
En tiempo de ejecución el cliente carga v2 y de repente el switch encuentra .pendiente, un caso que no existe en el binario compilado. Esto es un comportamiento indefinido.
Marcar el enum como @frozen asegura que el contrato público nunca va a cambiar.
La relación entre @frozen y Sendable es que cuando se tiene un enum cuyos miembros iniciales son Sendable, pero no está marcado como @frozen, el compilador no puede asegurar que el día de mañana aparezca un nuevo caso que no sea Sendable.
Por ejemplo, el día de mañana podríamos tener una nueva propiedad pendiente1 que envuelva una instancia de tipo MiClase que no es Sendable:
class MiClase {
var contador = 0 // ❌ Las clases son no-Sendable por defecto
}
public enum Resultado {
case exito(String)
case error(String)
case pendiente1(MiClase) // ⚠️ Nuevo caso no-Sendable
}
¿Qué significa que todos los miembros de un tipo deben ser Sendable?
Si un tipo de dato es Sendable, todos los atributos deben ser Sendable también. Si pongo uno que no lo sea, el compilador sacará el error:
Stored property ‘x_instance‘ of ‘Sendable’-conforming struct ‘Y’ has non-sendable type ‘X’
¿Cuáles son las alternativas si un miembro no puede ser marcado como Sendable?
Si tenemos una dependencia que no puede ser marcada como Sendable porque posiblemente es un tercero que no podemos modificar, entonces podríamos usar una de las siguientes alternativas:
- Marcar el tipo de dato que estamos definiendo como
@unchecked Sendable. - Revisar cómo se se está usando la dependencia para tener una referencia indirecta a ella.
Por ejemplo, en el siguiente caso, Location no es Sendable. Sin embargo, podemos tener una referencia a un Location por medio de su nombre:
public struct Person: Sendable {
var name: String
var hometown: String
init(name: String, hometown: Location) {
self.name = name
self.hometown = hometown.name
}
}
public struct Location {
var name: String
}
¿Cómo se puede usar un actor para hacer un tipo Sendable?
Un actor es un guardan que serializa el acceso a su estado. Solo permite que una tarea a la vez acceda a sus datos.
public struct Location {
var name: String // ❌ String público sin @frozen, no es Sendable implícito
}
struct LocationDetailViewModel {
let location: Location // ❌ contiene un tipo no-Sendable
}
LocationDetailViewModel no puede ser Sendable porque Location tampoco lo es y Swift no puede garantizar que sea seguro compartirlo entre hilos.
Si todo el acceso a LocationDetailViewModel está garantizado que ocurre en el mismo actor, entonces nunca habrá dos hilos accediendo simultáneamente. El actor es el mecanismo de sincronización.
Por lo tanto, Swift puede inferir Sendable automáticamente - no porque el contenido sea seguro por sí solo, sino porque el acceso está controlado.
¿Qué es @MainActor y cómo simplifica la conformidad a Sendable?
@MainActor asegura que el código se accede únicamente a través del hilo principal y es útil para componentes como Views o ViewModels.
@MainActor se encarga de la sincronización de los hilos, asegurando el acceso a un dato. Como Swift está satisfecho de que no habrá data-races con el tipo de dato marcado con @MainActor, entonces le otorga Sendable implícitamente.
Recitar
Describe el concepto de copy-on-write (COW)
"Copy-on-write" es una optimización de rendimiento que usa Swift para colecciones de tipo por valor.
Teóricamente, los tipos por valor se copian al asignarse:
let a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
let b = a // ⚠️ Teóricamente se copia pero, ¿y en la práctica?
Si cada asignación copiara el contenido real, trabajar con colecciones grandes sería muy costoso. Por esta razón, en lugar de copiar inmediatamente, Swift hace que a y b apunten al mismo buffer en memoria. La copia real ocurre solo cuando uno de los dos intenta modificar el contenido:
var a = [1, 2, 3]
var b = a // 😱 mismo buffer, sin copia aún
b.append(4) // ✅ aquí ocurre la copia real
// ahora b tiene su propio buffer
Antes del append, ambos comparten memoria. En el momento de la mutación, Swift detecta que hay más de un dueño y hace la copia. Internamente, Swift usa un conteo de referencias sobre el buffer interno y, antes de mutar, pregunta: ¿Soy el único dueño de este buffer? Si la respuesta es afirmativa: muto directamente sin copiar. Si la respuesta es negativa: copio primero y luego muto.
CoW da:
-
Semántica de valor:
aybse comportan como copias independientes. - Rendimiento de referencia: no se paga el costo de la copia hasta que realmente se necesita.
En muchos casos no se paga el costo de la copia - Por ejemplo, si se pasa un arreglo a una función que solo lo lee, jamás se copia.
¿Por qué Swift requiere conformidad explícita a Sendable para tipos públicos?
Los clientes de mi API van a asumir que cierto tipo de dato es Sendable. Si el día de mañana agrego un miembro que NO es Sendable, entonces mi API va a cambiar y mis clientes ya no podrán compilar. Por esta razón, yo debo declarar explícitamente que mi tipo de dato es Sendable cuando lo hago public.
Top comments (1)
"This is a really clear and thorough explanation — thank you! 👍
I especially liked how you broke down why public structs don’t get implicit Sendable conformance, while @frozen ones do. The idea of protecting the “contract” with API clients by forcing explicit Sendable on public types is a very smart design decision.
Key takeaways for me:
Value types are safe by default only when the compiler can fully see their implementation (non-public or frozen).
@usableFromInline and @frozen play a big role in allowing implicit Sendable.
Copy-on-Write is such an elegant optimization — you get value semantics with reference-like performance.
Quick question for you:
When building public libraries, do you usually mark your structs/enums as Sendable explicitly from the beginning, or only when you actually need to pass them across actors?
Looking forward to the rest of the Swift Concurrency series. This article cleared up a lot of confusion around Sendable warnings for me. Great work!"