Sendable é um protocolo em Swift que indica que um tipo é seguro para compartilhar entre domínios de concorrência como atores, tarefas e threads. Quando um tipo adere ao Sendable, o compilador verifica no tempo de compilação que passá-lo não introduzirá corridas de dados. Juntamente com a atribuição @Sendable para fechamentos, é uma das principais estruturas da segurança contra corridas de dados do Swift.

Antes de mergulhar neste tópico sobre sendables, encorajo você a ler meus artigos sobre async/await, atores e isolação de atores. Esses artigos abordam os fundamentos das mudanças de concorrência, que se conectam diretamente com as técnicas explicadas neste artigo.

Quando devo usar Sendable?

O próprio protocolo Sendable é surpreendentemente simples:

public protocol Sendable {}

É um protocolo vazio sem requisitos. Sua única função é atuar como marcador: aderir informa ao compilador que seu tipo é seguro para ser passado entre domínios de isolamento.

O protocolo Sendable indica se o valor passado tem uma API pública segura em termos de concorrência. Uma API pública pode ser usada entre domínios de concorrência quando não há mutadores públicos, um sistema interno de bloqueio está em vigor ou os mutadores implementam cópia-on-write, como com tipos valor.

Muitos tipos da biblioteca padrão já suportam o protocolo Sendable, removendo a necessidade de adicionar conformidade para muitos tipos. Como resultado do suporte da biblioteca padrão, o compilador pode implicitamente criar suporte para seus próprios tipos personalizados.

Por exemplo, inteiros suportam o protocolo:

extension Int: Sendable {}

Ao criarmos uma estrutura de valor com um único atributo do tipo inteiro, implicitamente obtemos suporte para o protocolo Sendable:

// Implicitly conforms to Sendable
struct Article {
    var views: Int
}

Ao mesmo tempo, a seguinte classe do exemplo do mesmo artigo não teria conformidade implícita:

// Does not implicitly conform to Sendable
class Article {
    var views: Int
}

A classe não adere porque é um tipo de referência e, portanto, mutável de outros domínios concorrentes. Em outras palavras, a classe artigo não é segura para ser passada em torno de threads e o compilador não pode marcar implicitamente como Sendable.

Conformidade implícita ao usar genéricos e enums

É bom entender que o compilador não adiciona conformidade implícita a tipos genéricos se o tipo genérico não aderir ao Sendable.

// Sem conformidade implícita com Sendable porque Value não é conformante a Sendable
struct Container<Value> {
    var child: Value
}

No entanto, se adicionarmos uma exigência de protocolo ao nosso valor genérico, teremos suporte implícito:

// Container tem conformidade implícita com Sendable pois todos os seus propriedades públicas também o fazem.
struct Container<Value: Sendable> {
    var child: Value
}

O mesmo vale para enums com valores associados:

Conformidade implícita do protocolo Sendable não funcionará se as crianças não conformarem a Sendable.
Conformidade implícita do protocolo Sendable não funcionará se as crianças não conformarem a Sendable.

Pode-se ver que obtemos um erro do compilador:

O valor associado ‘loggedIn(name:)’ de enum ‘State’ com conformidade a Sendable tem tipo não sendável ‘(name: NSAttributedString)’

Pode-se resolver o erro usando um tipo de valor String, já que ele já é conformante a Sendable:

enum State: Sendable {
    case loggedOut
    case loggedIn(name: String)
}

Lançando erros de instâncias seguras em threads

As mesmas regras se aplicam a erros que desejam conformar-se a Sendable:

struct ArticleSavingError: Error {
    var author: NonFinalAuthor
}

extension ArticleSavingError: Sendable { }

Como o autor é não final e não thread-safe (mais sobre isso depois), teremos o seguinte erro:

A propriedade armazenada ‘author’ de struct ‘ArticleSavingError’ com conformidade a Sendable tem tipo não sendável ‘NonFinalAuthor’

Pode-se resolver o erro garantindo que todos os membros de ArticleSavingError sejam conformes a Sendable.