As enumerações no Swift são, de longe, mais poderosas do que aquelas em muitas outras linguagens de programação. Embora, na maioria das linguagens, as enumerações sejam limitadas a um simples conjunto de valores, o Swift oferece recursos que ampliam as possibilidades e a flexibilidade dessas estruturas. Nesse sentido, as enumerações no Swift podem se comportar de maneira muito similar a classes e estruturas, adicionando funcionalidades e estados aos seus casos. Ao contrário do que ocorre em outras linguagens, onde as enumerações são estáticas e restritas, o Swift permite que elas sejam altamente dinâmicas e adaptáveis, o que se reflete em diversas características, como a adição de propriedades computadas, métodos e a adoção de protocolos.

As enumerações no Swift são muito mais do que simples listas de valores. Elas podem incorporar propriedades computadas e métodos, aproximando-se da flexibilidade de uma classe ou estrutura. No exemplo a seguir, vemos uma enumeração Priority, que define quatro níveis de prioridade e implementa uma propriedade computada isHigh e um método description():

swift
enum Priority { case low, medium, high, critical var isHigh: Bool {
self == .high || self == .critical
}
func description() -> String { switch self { case .low: return "Low Priority" case .medium: return "Medium Priority" case .high: return "High Priority" case .critical: return "Critical Priority" } } }

Com esse código, a enumeração Priority agora não é apenas um conjunto de valores, mas também possui lógica embutida, permitindo que seja utilizada em diversas situações mais complexas. Por exemplo, ao criar uma variável do tipo Priority e acessar seus métodos e propriedades, como neste trecho:

swift
let priority = Priority.high
print("This is a \(priority.description()) and needs to be done now \(priority.isHigh)")

Além disso, uma das características notáveis das enumerações no Swift é a possibilidade de adotar protocolos, o que aumenta sua capacidade de integrar-se com outros tipos e componentes do sistema. A seguir, vemos um exemplo de como isso funciona com o protocolo Describable:

swift
protocol Describable { func description() -> String } enum TrafficLight: Describable { case red case yellow case green func description() -> String { switch self { case .red: return "Stop" case .yellow: return "Proceed with caution" case .green: return "Go" } } }

O uso do protocolo Describable torna a enumeração TrafficLight mais flexível, permitindo que ela seja usada de maneira consistente com outros tipos que também adotem esse protocolo. Essa flexibilidade pode ser vista no exemplo a seguir, onde várias instâncias de tipos diferentes, como TrafficLight e Priority, são agrupadas e manipuladas da mesma maneira:

swift
let describe: [any Describable] = [TrafficLight.green, Priority.high] for item in describe { print("Description: \(item.description())") }

Neste caso, mesmo sendo tipos diferentes (uma enumeração e outra estrutura), ambos podem ser tratados da mesma forma devido ao protocolo comum. Isso demonstra o poder do Swift em permitir que tipos distintos se comportem de forma consistente, facilitando a manipulação e a extensão de código.

Outro aspecto interessante das enumerações no Swift é sua natureza como tipos de valor, semelhante às estruturas. Isso significa que, quando uma instância de enumeração é passada para outra parte do código, ela é passada por valor, e não por referência, o que contribui para a segurança e a previsibilidade do código. Essa característica é especialmente importante em contextos de concorrência, onde a imutabilidade e a separação de dados podem evitar condições de corrida e outros problemas típicos em sistemas paralelos.

Ao contrário das enumerações em muitas outras linguagens, que possuem apenas valores brutos (raw values) de tipos simples, o Swift vai além, permitindo valores associados que tornam as enumerações ainda mais poderosas e capazes de representar estruturas de dados complexas. Isso é um reflexo da filosofia do Swift, que busca maximizar a segurança, flexibilidade e clareza no código.

Por fim, ao escrever código utilizando enumerações no Swift, é importante ter em mente o impacto de sua utilização na performance, principalmente quando trabalhamos com estruturas complexas ou sistemas de grande escala. Embora o Swift seja projetado para ser altamente eficiente, o uso excessivo de propriedades computadas ou métodos dentro das enumerações pode resultar em um custo de desempenho, especialmente quando essas operações são realizadas repetidamente em grandes volumes de dados. Assim, o uso de enumerações deve ser cuidadosamente planejado para equilibrar funcionalidade e performance.

Como o Grand Central Dispatch (GCD) Gerencia Tarefas Assíncronas e Síncronas em Fila

O uso do Grand Central Dispatch (GCD) permite que tarefas sejam executadas de forma eficiente em múltiplos núcleos de processamento, organizadas em diferentes tipos de filas: concorrentes e seriais. Esse mecanismo oferece uma maneira poderosa de controlar a execução de código em segundo plano, otimizando o uso dos recursos do sistema e evitando bloqueios na interface do usuário. A seguir, exploraremos o funcionamento das filas concorrentes e seriais, além da distinção entre os métodos async e sync.

Quando utilizamos o método async(execute:) em uma fila do GCD, a tarefa é executada de forma assíncrona, ou seja, não bloqueia a execução do código subsequente. Esse método é particularmente útil quando temos uma operação que pode ser realizada em paralelo com outras, sem a necessidade de aguardar sua conclusão. O código a seguir mostra como uma tarefa pode ser executada em uma fila concorrente:

swift
queue.async { performCalculation(1000, tag: "async2") }

Esse código reduz a quantidade de código necessário ao omitir a criação de um fechamento separado, o que é mais comum em tarefas pequenas. No entanto, para tarefas maiores ou que precisam ser executadas múltiplas vezes, a criação de um fechamento torna-se uma prática recomendada, conforme demonstrado inicialmente. Ao adicionar múltiplas tarefas a uma fila concorrente, podemos observar como o GCD gerencia sua execução em paralelo, respeitando a disponibilidade de recursos do sistema. O exemplo a seguir ilustra essa operação:

swift
let cqueue = DispatchQueue(label: "cqueue.hoffman.jon", attributes: .concurrent)
cqueue.async { performCalculation(10_000_000, tag: "async1") } cqueue.async { performCalculation(1000, tag: "async2") } cqueue.async { performCalculation(100_000, tag: "async3") }

Cada tarefa possui um número diferente de iterações a serem realizadas, e a função performCalculation() imprime o tempo gasto para completar cada uma. Embora o GCD execute essas tarefas de forma concorrente, o tempo de execução pode variar significativamente, dependendo do número de iterações de cada tarefa. Por exemplo, mesmo que a tarefa async1 seja iniciada primeiro, ela pode demorar muito mais do que async2 e async3, que possuem uma carga computacional muito menor.

Em contraste com a fila concorrente, a fila serial executa uma tarefa de cada vez, aguardando a conclusão de uma antes de iniciar a próxima. O comportamento FIFO (First In, First Out) ainda é mantido, mas a diferença crucial é que, em uma fila serial, as tarefas não são executadas simultaneamente. O exemplo a seguir demonstra a criação de uma fila serial e a execução de múltiplas tarefas nela:

swift
let squeue = DispatchQueue(label: "squeue.hoffman.jon")
squeue.async { performCalculation(10_000_000, tag: "async1") } squeue.async { performCalculation(1000, tag: "async2") } squeue.async { performCalculation(100_000, tag: "async3") }

Ao executar o código, podemos observar que as tarefas são concluídas na ordem em que foram enviadas para a fila. Mesmo que async2 e async3 tenham um tempo de execução consideravelmente menor, elas ainda são executadas uma após a outra, sem qualquer paralelismo, o que caracteriza a natureza da fila serial. Esse comportamento pode ser útil em cenários onde as tarefas precisam ser executadas em uma ordem específica ou onde o compartilhamento de recursos entre tarefas não é desejado.

Além de ser útil para a execução de tarefas de fundo, o GCD também facilita a manipulação da interface do usuário. No contexto de aplicativos móveis, por exemplo, atualizações na interface precisam ser feitas no thread principal. O método DispatchQueue.main.async(execute:) é usado justamente para enviar tarefas de fundo que afetam a UI para o thread principal. Este exemplo ilustra a prática correta de redirecionar a execução de tarefas que alteram a UI para o thread principal:

swift
let squeue = DispatchQueue(label: "squeue.hoffman.jon") squeue.async { let resizedImage = image.resize(to: rect) DispatchQueue.main.async { picView.image = resizedImage } }

Aqui, a imagem é redimensionada em uma fila de fundo, para que a interface do usuário não seja bloqueada, e apenas quando o processamento estiver completo é que a imagem é atualizada no UIImageView no thread principal.

O GCD também oferece a opção de usar o método sync para executar tarefas de forma síncrona. Quando usamos sync, o thread atual será bloqueado até que a tarefa seja concluída. Essa abordagem pode ser útil em situações específicas, como quando precisamos garantir que uma operação seja concluída antes de continuar com a execução do código, mas deve ser utilizada com cautela, pois pode levar a bloqueios e travamentos se não for bem gerida.

É importante notar que o uso de filas concorrentes e seriais tem implicações no desempenho de um aplicativo. Em filas concorrentes, a execução simultânea das tarefas pode melhorar a performance geral, mas a ordem de execução pode ser imprevisível. Já nas filas seriais, a previsibilidade e o controle sobre a ordem das tarefas são garantidos, mas isso pode resultar em tempos de espera maiores, especialmente quando há tarefas pesadas na fila.

Além disso, ao trabalhar com o GCD, é essencial compreender a gestão de recursos do sistema e como o número de threads disponíveis pode impactar a performance do aplicativo. Filas concorrentes não garantem uma ordem de execução, e o tempo de resposta pode variar com base na carga do sistema e na quantidade de recursos disponíveis. Portanto, é fundamental que o desenvolvedor tenha uma visão clara de como as tarefas serão distribuídas e de que tipo de fila utilizar, levando em conta a natureza e os requisitos do código.