
[ad_1]
A estrutura Combine no Swift é uma API declarativa poderosa para o processamento assíncrono de valores ao longo do tempo. Ele aproveita ao máximo os recursos do Swift, como o Generics, para fornecer algoritmos de segurança de tipo que podem ser compostos em pipelines de processamento. Esses pipelines podem ser manipulados e transformados por componentes chamados operadores. Combine navios com uma grande variedade de operadores integrados que podem ser encadeados para formar canais impressionantes através dos quais os valores podem ser transformados, filtrados, armazenados em buffer, programados e muito mais.
Apesar da utilidade dos operadores integrados do Combine, há momentos em que eles ficam aquém. Isso ocorre quando a construção de seus próprios operadores personalizados adiciona a flexibilidade necessária para executar tarefas frequentemente complexas de maneira concisa e com desempenho de sua escolha.
Combinar ciclo de vida
Para criar nossos próprios operadores, é necessário entender o ciclo de vida básico e a estrutura de um pipeline Combine. No Combine, existem três abstrações principais: Publicadores, Assinantes e Operadores.
Os editores são tipos de valor, ou Structs, que descrevem como os valores e os erros são produzidos. Eles permitem o cadastro de assinantes que receberão valores ao longo do tempo. Além de receber valores, um Assinante pode receber uma conclusão, como sucesso ou erro, de um Publicador. Os assinantes podem mudar de estado e, como tal, são normalmente implementados como um tipo de referência ou classe.
Os assinantes são criados e, em seguida, anexados a um Publicador, assinando-o. O Publicador enviará uma assinatura de volta ao Assinante. Essa assinatura é usada pelo Assinante para solicitar valores do Publicador. Finalmente, o Publicador pode começar a enviar os valores solicitados de volta ao Assinante conforme solicitado. Dependendo do tipo de Publicador, ele pode enviar valores que possui indefinidamente ou pode ser concluído com sucesso ou falha. Esta é a estrutura básica e o ciclo de vida usados no Combine.
Operadores ficam entre Publicadores e Assinantes, onde transformam valores recebidos de um Publicador, chamado de upstream, e os enviam para Assinantes, o downstream. Na verdade, os operadores atuam tanto como Publicadores quanto como Assinantes.
Criando um operador personalizado
Vamos abordar duas estratégias diferentes para criar um operador Combine personalizado. Na primeira abordagem, usaremos a composição de uma cadeia de operadores existente para criar um componente reutilizável. A segunda estratégia é mais complexa, mas oferece o máximo em flexibilidade.
Compondo um Operador de Combinação
Em nosso primeiro exemplo, criaremos um histograma a partir de um array aleatório de valores inteiros. Um histograma nos informa a frequência com que cada valor no conjunto de dados de amostra aparece. Por exemplo, se nosso conjunto de dados de amostra tiver duas ocorrências do número um, nosso histograma mostrará uma contagem de dois como o número de ocorrências do número um.
// random sample of Int
let sample = [1, 3, 2, 1, 4, 2, 3, 2]
// Histogram
// key: a unique Int from the sample
// value: the count of this unique Int in the sample
let histogram = [1: 2, 2: 3, 3: 2, 4: 1]
Podemos usar Combine para calcular o histograma de uma amostra de Int aleatório.
// random sample of Int
// 1
let sample = [1, 3, 2, 1, 4, 2, 3, 2]
// 2
sample.publisher
// 3
.reduce([Int:Int](), { accum, value in
var next = accum
if let current = next[value] {
next[value] = current + 1
} else {
next[value] = 1
}
return next
})
// 4
.map({ dictionary in
dictionary.map { $0 }
})
// 5
.map({ item in
item.sorted { element1, element2 in
element1.key < element2.key
}
})
.sink { printHistogram(histogram: $0) }
.store(in: &cancellables)
O que nos dá a seguinte saída.
histogram standard operators:
1: 2
2: 3
3: 2
4: 1
Aqui está um detalhamento do que está acontecendo com o código:
- Definir nosso conjunto de dados de amostra
- Arranje um
Publisher
dos nossos dados de amostra - Coloque cada valor exclusivo no conjunto de dados e aumente um contador para cada ocorrência.
- Converta nosso
Dictionary
de valores categorizados em umArray
de tuplas de chave/valor. por exemplo[(key: Int, value: Int)]
- Classifique a matriz em ordem crescente por
key
Como você pode ver, criamos uma série de operadores Combine encadeados que calculam um histograma para um conjunto de dados publicado de Int
. Mas e se usarmos essa sequência de código em mais de um local? Seria muito bom se pudéssemos usar um único operador para executar toda essa cadeia de operadores. Essa reutilização não apenas torna nosso código mais conciso e fácil de entender, mas também mais fácil de depurar e manter. Então vamos fazer isso compondo um novo operador baseado no que já fizemos.
// 1
extension Publisher where Output == Int, Failure == Never {
// 2
func histogramComposed() -> AnyPublisher<[(key:Int, value:Int)], Never>{
// 3
self.reduce([Int:Int](), { accum, value in
var next = accum
if let current = next[value] {
next[value] = current + 1
} else {
next[value] = 1
}
return next
})
.map({ dictionary in
dictionary.map { $0 }
})
.map({ item in
item.sorted { element1, element2 in
element1.key < element2.key
}
})
// 4
.eraseToAnyPublisher()
}
}
O que esse código está fazendo:
- Criar uma extensão em
Publisher
e restringir sua saída ao tipoInt
- Defina uma nova função em
Publisher
que retorna umAnyPublisher
da nossa saída de histograma - Execute a cadeia de operadores do histograma como no exemplo anterior, mas desta vez em
self
. Nós usamosself
aqui já que estamos executando no atualPublisher
instância - Digite apague nosso editor para ser um
AnyPublisher
Agora vamos usar nosso novo operador Combine.
// 1
let sample = [1, 3, 2, 1, 4, 2, 3, 2]
// 2
sample.publisher
.histogramComposed()
.sink { printHistogram(histogram: $0) }
.store(in: &cancellables)
O que nos dá a seguinte saída.
histogram composed: 1: 2 2: 3 3: 2 4: 1
Usando o novo operador de histograma composto:
- Definir nosso conjunto de dados de amostra
- Use diretamente nosso novo operador de histograma Combine composto
A partir do exemplo de uso do nosso novo operador de histograma, você pode ver que o código no ponto de uso é bastante simples e reutilizável. Esta é uma técnica fantástica para criar uma caixa de ferramentas de operadores Combine reutilizáveis.
O operador de colheitadeira completo
Criar um operador Combine por meio da composição, como vimos, é uma ótima maneira de refatorar o código existente para reutilização. No entanto, a composição tem suas limitações, e é aí que a criação de um operador Combine nativo se torna importante.
Um operador Combine implementado nativamente utiliza o Combine Publisher
, Subscriber
e Subscription
interfaces e relacionamentos para fornecer sua funcionalidade. Um operador Combine nativo atua como um Subscriber
de dados upstream e um Publisher
para assinantes a jusante.
Para este exemplo, criaremos um operador de módulo implementado nativamente em Combine. O módulo é um operador matemático que dá o resto de uma divisão como um valor absoluto e é representado pelo sinal de porcentagem, %. Assim, por exemplo, 10 % 3 = 1, ou 10 módulo 3 é 1 (10 ➗ 3 = 3 Restante 1).
Vejamos o código completo desse operador Combine nativo, como usá-lo e, em seguida, discutiremos como ele funciona.
// 1
struct ModulusOperator<Upstream: Publisher>: Publisher where Upstream.Output: SignedInteger {
typealias Output = Upstream.Output // 2
typealias Failure = Upstream.Failure
let modulo: Upstream.Output
let upstream: Upstream
// 3
func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input {
let bridge = ModulusOperatorBridge(modulo: modulo, downstream: subscriber)
upstream.subscribe(bridge)
}
}
extension ModulusOperator {
// 4
struct ModulusOperatorBridge<S>: Subscriber where S: Subscriber, S.Input == Output, S.Failure == Failure {
typealias Input = S.Input
typealias Failure = S.Failure
// 5
let modulo: S.Input
// 6
let downstream: S
//7
let combineIdentifier = CombineIdentifier()
// 8
func receive(subscription: Subscription) {
downstream.receive(subscription: subscription)
}
// 9
func receive(_ input: S.Input) -> Subscribers.Demand {
downstream.receive(abs(input % modulo))
}
func receive(completion: Subscribers.Completion<S.Failure>) {
downstream.receive(completion: completion)
}
}
// Note: `where Output == Int` here limits the `modulus` operator to
// only being available on publishers of Ints.
extension Publisher where Output == Int {
// 10
func modulus(_ modulo: Int) -> ModulusOperator<Self> {
return ModulusOperator(modulo: modulo, upstream: self)
}
}
Como você pode ver, o módulo é sempre positivo e, quando divisível, é igual a 0.
Como funciona o código?
Agora podemos discutir como funciona o código do operador Combine nativo.
- Definimos nosso novo operador Combine como um
Publisher
com uma restrição em alguns upstreamPublisher
s saída do tipoSignedInteger
. Lembre-se, nosso operador atuará como umPublisher
e umaSubscriber
. Assim, nossa entrada, a montante, deve serSignedInteger
s. - Nosso
ModulusOperator
saída, atuando comoPublisher
será o mesmo que nossa entrada (ou seja,SignedInteger
s). - Implementação de função necessária para
Publisher
. Cria umSubscription
que atua como uma ponte entre os operadores a montantePublisher
e a jusanteSubscriber
. - o
ModulusOperatorBridge
pode atuar tanto comoSubscription
e umSubscriber
. No entanto, operadores simples como este podem ser umSubscriber
sem a necessidade de serSubscription
. Isso se deve às necessidades do ciclo de vida de manuseio upstream, comoDemand
. O comportamento upstream é aceitável para nosso operador, então não há necessidade de implementarSubscription
. oModulusOperatorBridge
também executa as tarefas primárias do operador de módulo. - Parâmetro de entrada para o operador para o módulo que será calculado.
- Referências a jusante
Subscriber
e a montantePublisher
. CombineIdentifier
porCustomCombineIdentifierConvertible
conformidade quando umSubscription
ouSubject
é implementado como uma estrutura.- Implementações de funções necessárias para
Subscriber
. Liga o upstreamSubscription
para a ponte como a jusanteSubscription
além do ciclo de vida. - Recebe entrada como
Subscriber
executa a operação de módulo nesta entrada e, em seguida, passa para o downstreamSubscriber
. A nova demanda de dados, se houver, do downstream é retransmitida para o upstream. - Por fim, uma extensão em
Publisher
torna nosso operador Combine personalizado disponível para uso. A extensão é limitada àqueles upstreamPublishers
cuja saída é do tipoInt
.
Colocando este novo operador de módulo em ação em um Publisher
do Int
pareceria:
[-10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].publisher
.modulus(3)
.sink { modulus in
print("modulus: \(modulus)")
}
.store(in: &cancellables)
modulus: 1
modulus: 0
modulus: 2
modulus: 1
modulus: 0
modulus: 2
modulus: 1
modulus: 0
modulus: 2
modulus: 1
modulus: 0
modulus: 1
modulus: 2
modulus: 0
modulus: 1
modulus: 2
modulus: 0
modulus: 1
modulus: 2
modulus: 0
modulus: 1
Como você pode ver, o operador de módulo atuará sobre um Publisher
do Int
. Neste exemplo, estamos tomando o módulo de 3 para cada Int
valor por sua vez.
Conclusão
Combine é uma estrutura declarativa poderosa para o processamento assíncrono de valores ao longo do tempo. Sua utilidade pode ser estendida e personalizada ainda mais através da criação de operadores personalizados que atuam como processadores em um pipeline de dados. Esses operadores podem ser criados por composição, permitindo um excelente reaproveitamento de dutos comuns. Eles também podem ser criados através da implementação direta do Combine Publisher
, Subscriber
e Subscription
protocolos, o que permite o máximo em flexibilidade e controle sobre o fluxo de dados.
Sempre que você estiver trabalhando com o Combine, lembre-se dessas técnicas e procure oportunidades para criar operadores personalizados quando for relevante. Um pouco de tempo e esforço para criar um operador Combine personalizado pode economizar horas de trabalho no futuro.
[ad_2]
Source link