Os erros que a maioria dos desenvolvedores Swift não sabe que está cometendo

Publicados: 2022-03-11

Vindo de um background em Objective-C, no começo, eu senti que Swift estava me segurando. Swift não estava me permitindo progredir por causa de sua natureza fortemente tipada, que costumava ser irritante às vezes.

Ao contrário do Objective-C, o Swift impõe muitos requisitos no tempo de compilação. Coisas que são relaxadas em Objective-C, como o tipo de id e conversões implícitas, não são uma coisa em Swift. Mesmo se você tiver um Int e um Double e quiser adicioná-los, você terá que convertê-los em um único tipo explicitamente.

Além disso, opcionais são uma parte fundamental da linguagem, e mesmo sendo um conceito simples, leva algum tempo para se acostumar com eles.

No início, você pode querer forçar o desempacotamento de tudo, mas isso acabará levando a falhas. À medida que você se familiariza com a linguagem, você começa a amar como dificilmente tem erros de tempo de execução, já que muitos erros são detectados em tempo de compilação.

A maioria dos programadores Swift tem experiência anterior significativa com Objective-C, o que, entre outras coisas, pode levá-los a escrever código Swift usando as mesmas práticas com as quais estão familiarizados em outras linguagens. E isso pode causar alguns erros graves.

Neste artigo, descrevemos os erros mais comuns que os desenvolvedores Swift cometem e as formas de evitá-los.

Não se engane - as melhores práticas do Objective-C não são as melhores práticas do Swift.
Tweet

1. Opcionais de desempacotamento forçado

Uma variável de um tipo opcional (por exemplo, String? ) pode ou não conter um valor. Quando eles não possuem um valor, eles são iguais a nil . Para obter o valor de um opcional, primeiro você precisa desembrulhar eles, e isso pode ser feito de duas maneiras diferentes.

Uma maneira é a vinculação opcional usando um if let ou um guard let , ou seja:

 var optionalString: String? //... if let s = optionalString { // if optionalString is not nil, the test evaluates to // true and s now contains the value of optionalString } else { // otherwise optionalString is nil and the if condition evaluates to false }

O segundo é forçar o desempacotamento usando o ! operador, ou usando um tipo opcional desempacotado implicitamente (por exemplo, String! ). Se o opcional for nil , forçar um unwrap causará um erro de tempo de execução e encerrará o aplicativo. Além disso, tentar acessar o valor de um opcional desempacotado implicitamente causará o mesmo.

Às vezes, temos variáveis ​​que não podemos (ou não queremos) inicializar no inicializador de classe/estrutura. Assim, temos que declará-los como opcionais. Em alguns casos, assumimos que eles não serão nil em certas partes do nosso código, então forçamos o desempacotamento deles ou os declaramos como opcionais desempacotados implicitamente porque isso é mais fácil do que ter que fazer vinculação opcional o tempo todo. Isso deve ser feito com cuidado.

Isso é semelhante a trabalhar com IBOutlet s, que são variáveis ​​que fazem referência a um objeto em um nib ou storyboard. Eles não serão inicializados na inicialização do objeto pai (geralmente um controlador de exibição ou UIView personalizado), mas podemos ter certeza de que não serão nil quando viewDidLoad (em um controlador de exibição) ou awakeFromNib (em uma exibição) for chamado, e assim podemos acessá-los com segurança.

Em geral, a melhor prática é evitar forçar o desempacotamento e usar opcionais desempacotados implicitamente. Sempre considere que o opcional pode ser nil e trate-o adequadamente usando a ligação opcional ou verificando se não é nil antes de forçar um desempacotamento ou acessando a variável no caso de um opcional desempacotado implicitamente.

2. Não conhecer as armadilhas dos ciclos de referência fortes

Um ciclo de referência forte existe quando um par de objetos mantém uma referência forte entre si. Isso não é algo novo para o Swift, já que o Objective-C tem o mesmo problema, e espera-se que os desenvolvedores experientes do Objective-C gerenciem isso adequadamente. É importante prestar atenção nas referências fortes e no que referencia o quê. A documentação do Swift tem uma seção dedicada a este tópico.

É particularmente importante gerenciar suas referências ao usar closures. Por padrão, closures (ou blocos), mantêm uma referência forte para cada objeto referenciado dentro deles. Se algum desses objetos tiver uma referência forte ao próprio fechamento, teremos um ciclo de referência forte. É necessário fazer uso de listas de captura para gerenciar adequadamente como suas referências são capturadas.

Se houver a possibilidade de que a instância capturada pelo bloco seja desalocada antes que o bloco seja chamado, você deve capturá-la como uma referência fraca , o que será opcional, pois pode ser nil . Agora, se tiver certeza de que a instância capturada não será desalocada durante o tempo de vida do bloco, você poderá capturá-la como uma referência sem proprietário . A vantagem de usar unowned em vez de weak é que a referência não será opcional e você pode usar o valor diretamente sem a necessidade de desembrulhar.

No exemplo a seguir, que você pode executar no Xcode Playground, a classe Container tem uma matriz e um encerramento opcional que é invocado sempre que sua matriz é alterada (ela usa observadores de propriedade para fazer isso). A classe Whatever tem uma instância Container e, em seu inicializador, atribui um closure para arrayDidChange e esse closure referencia self , criando assim um forte relacionamento entre a instância Whatever e o closure.

 struct Container<T> { var array: [T] = [] { didSet { arrayDidChange?(array: array) } } var arrayDidChange: ((array: [T]) -> Void)? } class Whatever { var container: Container<String> init() { container = Container<String>() container.arrayDidChange = { array in self.f(array) } } deinit { print("deinit whatever") } func f(s: [String]) { print(s) } } var w: Whatever! = Whatever() // ... w = nil

Se você executar este exemplo, notará que deinit whatever nunca seja impressa, o que significa que nossa instância w não é desalocada da memória. Para corrigir isso, temos que usar uma lista de captura para não self capturar fortemente:

 struct Container<T> { var array: [T] = [] { didSet { arrayDidChange?(array: array) } } var arrayDidChange: ((array: [T]) -> Void)? } class Whatever { var container: Container<String> init() { container = Container<String>() container.arrayDidChange = { [unowned self] array in self.f(array) } } deinit { print("deinit whatever") } func f(s: [String]) { print(s) } } var w: Whatever! = Whatever() // ... w = nil

Nesse caso, podemos usar unowned , pois self nunca será nil durante a vida útil do fechamento.

É uma boa prática quase sempre usar listas de captura para evitar ciclos de referência, o que reduzirá vazamentos de memória e um código mais seguro no final.

3. Usando self em todos os lugares

Ao contrário do Objective-C, com o Swift, não somos obrigados a usar self para acessar as propriedades de uma classe ou struct dentro de um método. Só somos obrigados a fazê-lo em um encerramento porque ele precisa capturar a self . Usar self onde não é necessário não é exatamente um erro, funciona muito bem e não haverá erros nem avisos. No entanto, por que escrever mais código do que você precisa? Além disso, é importante manter seu código consistente.

4. Não saber o tipo de seus tipos

Swift usa tipos de valor e tipos de referência . Além disso, instâncias de um tipo de valor exibem um comportamento ligeiramente diferente das instâncias de tipos de referência. Não saber em qual categoria cada uma de suas instâncias se encaixa causará falsas expectativas sobre o comportamento do código.

Nas linguagens mais orientadas a objetos, quando criamos uma instância de uma classe e a passamos para outras instâncias e como argumento para métodos, esperamos que essa instância seja a mesma em todos os lugares. Isso significa que qualquer alteração será refletida em todos os lugares, porque, na verdade, o que temos são apenas um monte de referências aos mesmos dados. Objetos que exibem esse comportamento são tipos de referência e, em Swift, todos os tipos declarados como class são tipos de referência.

Em seguida, temos tipos de valor que são declarados usando struct ou enum . Os tipos de valor são copiados quando são atribuídos a uma variável ou passados ​​como um argumento para uma função ou método. Se você alterar algo na instância copiada, a original não será modificada. Os tipos de valor são imutáveis . Se você atribuir um novo valor a uma propriedade de uma instância de um tipo de valor, como CGPoint ou CGSize , uma nova instância será criada com as alterações. É por isso que podemos usar observadores de propriedade em um array (como no exemplo acima na classe Container ) para nos notificar sobre alterações. O que está realmente acontecendo é que um novo array é criado com as mudanças; ele é atribuído à propriedade e, em seguida, didSet é invocado.

Assim, se você não sabe que o objeto com o qual está lidando é de um tipo de referência ou valor, suas expectativas sobre o que seu código fará podem estar totalmente erradas.

5. Não usar todo o potencial dos enums

Quando falamos sobre enums, geralmente pensamos no C enum básico, que é apenas uma lista de constantes relacionadas que são números inteiros abaixo. Em Swift, enums são muito mais poderosos. Por exemplo, você pode anexar um valor a cada caso de enumeração. Enums também possuem métodos e propriedades somente leitura/computadas que podem ser usadas para enriquecer cada caso com mais informações e detalhes.

A documentação oficial sobre enums é muito intuitiva, e a documentação de tratamento de erros apresenta alguns casos de uso para poder extra de enums em Swift. Além disso, confira a extensa exploração de enums no Swift para aprender praticamente tudo o que você pode fazer com eles.

6. Não usar recursos funcionais

A Swift Standard Library fornece muitos métodos que são fundamentais na programação funcional e nos permitem fazer muito com apenas uma linha de código, como map, reduce, filter, entre outros.

Vamos examinar alguns exemplos.

Digamos que você precise calcular a altura de uma visualização de tabela. Dado que você tem uma subclasse UITableViewCell como a seguinte:

 class CustomCell: UITableViewCell { // Sets up the cell with the given model object (to be used in tableView:cellForRowAtIndexPath:) func configureWithModel(model: Model) // Returns the height of a cell for the given model object (to be used in tableView:heightForRowAtIndexPath:) class func heightForModel(model: Model) -> CGFloat }

Considere, temos uma matriz de instâncias de modelo modelArray ; podemos calcular a altura da visualização da tabela com uma linha de código:

 let tableHeight = modelArray.map { CustomCell.heightForModel($0) }.reduce(0, combine: +)

O map produzirá um array de CGFloat , contendo a altura de cada célula, e o reduce irá adicioná-los.

Se você deseja remover elementos de uma matriz, pode acabar fazendo o seguinte:

 var supercars = ["Lamborghini", "Bugatti", "AMG", "Alfa Romeo", "Koenigsegg", "Porsche", "Ferrari", "McLaren", "Abarth", "Morgan", "Caterham", "Rolls Royce", "Audi"] func isSupercar(s: String) -> Bool { return s.characters.count > 7 } for s in supercars { if !isSupercar(s), let i = supercars.indexOf(s) { supercars.removeAtIndex(i) } }

Este exemplo não parece elegante, nem muito eficiente, pois estamos chamando indexOf para cada item. Considere o seguinte exemplo:

 var supercars = ["Lamborghini", "Bugatti", "AMG", "Alfa Romeo", "Koenigsegg", "Porsche", "Ferrari", "McLaren", "Abarth", "Morgan", "Caterham", "Rolls Royce", "Audi"] func isSupercar(s: String) -> Bool { return s.characters.count > 7 } for (i, s) in supercars.enumerate().reverse() { // reverse to remove from end to beginning if !isSupercar(s) { supercars.removeAtIndex(i) } }

Agora, o código é mais eficiente, mas pode ser melhorado ainda mais usando o filter :

 var supercars = ["Lamborghini", "Bugatti", "AMG", "Alfa Romeo", "Koenigsegg", "Porsche", "Ferrari", "McLaren", "Abarth", "Morgan", "Caterham", "Rolls Royce", "Audi"] func isSupercar(s: String) -> Bool { return s.characters.count > 7 } supercars = supercars.filter(isSupercar)

O próximo exemplo ilustra como você pode remover todas as subvisualizações de uma UIView que atendem a determinados critérios, como o quadro que cruza um retângulo específico. Você pode usar algo como:

 for v in view.subviews { if CGRectIntersectsRect(v.frame, rect) { v.removeFromSuperview() } } ``` We can do that in one line using `filter` ``` view.subviews.filter { CGRectIntersectsRect($0.frame, rect) }.forEach { $0.removeFromSuperview() }

Temos que ter cuidado, porém, porque você pode ficar tentado a encadear algumas chamadas para esses métodos para criar filtragem e transformação sofisticadas, que podem acabar com uma linha de código espaguete ilegível.

7. Permanecer na zona de conforto e não tentar a programação orientada por protocolo

Swift é reivindicada como a primeira linguagem de programação orientada a protocolo , conforme mencionado na sessão WWDC Protocol-Oriented Programming in Swift. Basicamente, isso significa que podemos modelar nossos programas em torno de protocolos e adicionar comportamento aos tipos simplesmente em conformidade com os protocolos e estendendo-os. Por exemplo, dado que temos um protocolo Shape , podemos estender CollectionType (que é conformado por tipos como Array , Set , Dictionary ) e adicionar um método a ele que calcula a área total contabilizando as interseções

 protocol Shape { var area: Float { get } func intersect(shape: Shape) -> Shape? } extension CollectionType where Generator.Element: Shape { func totalArea() -> Float { let area = self.reduce(0) { (a: Float, e: Shape) -> Float in return a + e.area } return area - intersectionArea() } func intersectionArea() -> Float { /*___*/ } }

A instrução where Generator.Element: Shape é uma restrição que indica que os métodos na extensão estarão disponíveis apenas em instâncias de tipos que estejam em conformidade com CollectionType , que contém elementos de tipos que estão em conformidade com Shape . Por exemplo, esses métodos podem ser invocados em uma instância de Array<Shape> , mas não em uma instância de Array<String> . Se tivermos uma classe Polygon que esteja em conformidade com o protocolo Shape , esses métodos também estarão disponíveis para uma instância de Array<Polygon> .

Com extensões de protocolo, você pode dar uma implementação padrão aos métodos declarados no protocolo, que estarão disponíveis em todos os tipos que estejam em conformidade com esse protocolo sem precisar fazer alterações nesses tipos (classes, structs ou enums). Isso é feito extensivamente em toda a Swift Standard Library, por exemplo, map e reduce são definidos em uma extensão de CollectionType , e essa mesma implementação é compartilhada por tipos como Array e Dictionary sem nenhum código extra.

Esse comportamento é semelhante aos mixins de outras linguagens, como Ruby ou Python. Simplesmente conformando-se a um protocolo com implementações de método padrão, você adiciona funcionalidade ao seu tipo.

A programação orientada a protocolos pode parecer bastante estranha e não muito útil à primeira vista, o que pode fazer você ignorá-la e nem mesmo tentar. Este post dá uma boa compreensão do uso de programação orientada a protocolo em aplicativos reais.

Como aprendemos, Swift não é uma linguagem de brinquedo

Swift foi inicialmente recebido com muito ceticismo; as pessoas pareciam pensar que a Apple iria substituir o Objective-C por uma linguagem de brinquedo para crianças ou por algo para não programadores. No entanto, Swift provou ser uma linguagem séria e poderosa que torna a programação muito agradável. Como é fortemente tipado, é difícil cometer erros e, como tal, é difícil listar os erros que você pode cometer com o idioma.

Quando você se acostumar com o Swift e voltar ao Objective-C, você notará a diferença. Você sentirá falta dos bons recursos que o Swift oferece e terá que escrever código tedioso em Objective-C para obter o mesmo efeito. Outras vezes, você enfrentará erros de tempo de execução que o Swift teria detectado durante a compilação. É uma ótima atualização para os programadores da Apple, e ainda há muito mais por vir à medida que a linguagem amadurece.

Relacionado: Guia do desenvolvedor iOS: do Objective-C ao aprendizado Swift