Programação orientada a protocolo e genéricos

Veja no TensorFlow.org Executar no Google Colab Ver fonte no GitHub

Este tutorial abordará programação orientada a protocolos e diferentes exemplos de como eles podem ser usados ​​com genéricos em exemplos do dia-a-dia.

Protocolos

A herança é uma maneira poderosa de organizar o código em linguagens de programação que permite compartilhar código entre vários componentes do programa.

Em Swift, existem diferentes maneiras de expressar herança. Você já deve estar familiarizado com uma dessas formas, de outras linguagens: herança de classes. No entanto, o Swift tem outro caminho: protocolos.

Neste tutorial, exploraremos protocolos - uma alternativa à subclasse que permite atingir objetivos semelhantes por meio de diferentes compensações. No Swift, os protocolos contêm vários membros abstratos. Classes, structs e enums podem estar em conformidade com vários protocolos e a relação de conformidade pode ser estabelecida retroativamente. Tudo isso permite alguns designs que não são facilmente expressáveis ​​em Swift usando subclasses. Vamos percorrer os idiomas que suportam o uso de protocolos (extensões e restrições de protocolo), bem como as limitações dos protocolos.

Tipos de valor do Swift 💖!

Além das classes que possuem semântica de referência, o Swift suporta enums e structs que são passados ​​por valor. Enums e structs suportam muitos recursos fornecidos por classes. Vamos dar uma olhada!

Em primeiro lugar, vamos ver como os enums são semelhantes às classes:

enum Color: String {
    case red = "red"
    case green = "green"
    case blue = "blue"
    // A computed property. Note that enums cannot contain stored properties.
    var hint: String {
        switch self {
            case .red:
                return "Roses are this color."
            case .green:
                return "Grass is this color."
            case .blue:
                return "The ocean is this color."
        }
    }

    // An initializer like for classes.
    init?(color: String) {
        switch color {
        case "red":
            self = .red
        case "green":
            self = .green
        case "blue":
            self = .blue
        default:
            return nil
        }
    }
}

// Can extend the enum as well!
extension Color {
    // A function.
    func hintFunc() -> String {
        return self.hint
    }
}

let c = Color.red
print("Give me a hint for c: \(c.hintFunc())")

let invalidColor = Color(color: "orange")
print("is invalidColor nil: \(invalidColor == nil)")
Give me a hint for c: Roses are this color.
is invalidColor nil: true

Agora, vamos ver as estruturas. Observe que não podemos herdar structs, mas podemos usar protocolos:

struct FastCar {
    // Can have variables and constants as stored properties.
    var color: Color
    let horsePower: Int
    // Can have computed properties.
    var watts: Float {
       return Float(horsePower) * 745.7
    }
    // Can have lazy variables like in classes!
    lazy var titleCaseColorString: String = {
        let colorString = color.rawValue
        return colorString.prefix(1).uppercased() + 
               colorString.lowercased().dropFirst()
    }()
    // A function.
    func description() -> String {
        return "This is a \(color) car with \(horsePower) horse power!"
    }
    // Can create a variety of initializers.
    init(color: Color, horsePower: Int) {
        self.color = color
        self.horsePower = horsePower
    }
    // Can define extra initializers other than the default one.
    init?(color: String, horsePower: Int) {
        guard let enumColor = Color(color: color) else {
            return nil
        }
        self.color = enumColor
        self.horsePower = horsePower
    }
}

var car = FastCar(color: .red, horsePower: 250)
print(car.description())
print("Horse power in watts: \(car.watts)")
print(car.titleCaseColorString)
This is a red car with 250 horse power!
Horse power in watts: 186425.0
Red

Finalmente, vamos ver como eles são passados ​​por tipos de valor ao contrário das classes:

// Notice we have no problem modifying a constant class with 
// variable properties.
class A {
  var a = "a"
}

func foo(_ a: A) {
  a.a = "foo"
}
let a = A()
print(a.a)
foo(a)
print(a.a)

/* 
Uncomment the following code to see how an error is thrown.
Structs are implicitly passed by value, so we cannot modify it.
> "error: cannot assign to property: 'car' is a 'let' constant"
*/

// func modify(car: FastCar, toColor color: Color) -> Void {
//   car.color = color
// }

// car = FastCar(color: .red, horsePower: 250)
// print(car.description())
// modify(car: &car, toColor: .blue)
// print(car.description())
a
foo

Vamos usar protocolos

Vamos começar criando protocolos para diferentes carros:

protocol Car {
    var color: Color { get set }
    var price: Int { get }
    func turnOn()
    mutating func drive()
}

protocol Electric {
    mutating func recharge()
    // percentage of the battery level, 0-100%.
    var batteryLevel: Int { get set }
}

protocol Gas {
    mutating func refill()
    // # of liters the car is holding, varies b/w models.
    var gasLevelLiters: Int { get set }
}

Em um mundo orientado a objetos (sem herança múltipla), você pode ter feito classes abstratas Electric e Gas e, em seguida, usar herança de classe para fazer com que ambas herdem de Car e, em seguida, ter um modelo de carro específico como uma classe base. No entanto, aqui ambos são protocolos completamente separados com acoplamento zero ! Isso torna todo o sistema mais flexível na forma como você o projeta.

Vamos definir um Tesla:

struct TeslaModelS: Car, Electric {
    var color: Color // Needs to be a var since `Car` has a getter and setter.
    let price: Int
    var batteryLevel: Int

    func turnOn() {
        print("Starting all systems!")
    }

    mutating func drive() {
        print("Self driving engaged!")
        batteryLevel -= 8
    }

    mutating func recharge() {
        print("Recharging the battery...")
        batteryLevel = 100
    }
}

var tesla = TeslaModelS(color: .red, price: 110000, batteryLevel: 100)

Isso especifica uma nova estrutura TeslaModelS que está em conformidade com os protocolos Car e Electric .

Agora vamos definir um carro movido a gasolina:

struct Mustang: Car, Gas{
    var color: Color
    let price: Int
    var gasLevelLiters: Int

    func turnOn() {
        print("Starting all systems!")
    }

    mutating func drive() {
        print("Time to drive!")
        gasLevelLiters -= 1
    }

    mutating func refill() {
        print("Filling the tank...")
        gasLevelLiters = 25
    }
}

var mustang = Mustang(color: .red, price: 30000, gasLevelLiters: 25)

Estenda protocolos com comportamentos padrão

O que você pode notar pelos exemplos é que temos alguma redundância. Toda vez que recarregamos um carro elétrico, precisamos definir o nível de porcentagem da bateria para 100. Como todos os carros elétricos têm uma capacidade máxima de 100%, mas os carros a gasolina variam entre a capacidade do tanque de gasolina, podemos padronizar o nível para 100 para carros elétricos .

É aqui que as extensões no Swift podem ser úteis:

extension Electric {
    mutating func recharge() {
        print("Recharging the battery...")
        batteryLevel = 100
    }
}

Então, agora, qualquer novo carro elétrico que criarmos definirá a bateria para 100 quando a recarregarmos. Assim, acabamos de decorar classes, structs e enums com comportamento único e padrão.

Quadrinho de Protocolo

Obrigado a Ray Wenderlich pelo quadrinho!

No entanto, uma coisa a observar é o seguinte. Em nossa primeira implementação, definimos foo() como uma implementação padrão em A , mas não a tornamos obrigatória no protocolo. Então, quando chamamos a.foo() , obtemos " A default " impresso.

protocol Default {}

extension Default {
    func foo() { print("A default")}
}

struct DefaultStruct: Default {
    func foo() {
        print("Inst")
    }
}

let a: Default = DefaultStruct()
a.foo()
A default

No entanto, se tornarmos foo() obrigatório em A , obtemos " Inst ":

protocol Default {
    func foo()
}

extension Default {
    func foo() { 
        print("A default")
    }
}

struct DefaultStruct: Default {
    func foo() {
        print("Inst")
    }
}

let a: Default = DefaultStruct()
a.foo()
Inst

Isso ocorre devido a uma diferença entre despacho estático no primeiro exemplo e despacho estático no segundo em protocolos em Swift. Para mais informações, consulte esta postagem no Medium .

Substituindo o comportamento padrão

No entanto, se quisermos, ainda podemos substituir o comportamento padrão. Uma coisa importante a ser observada é que isso não suporta despacho dinâmico .

Digamos que temos uma versão mais antiga de um carro elétrico, então a saúde da bateria foi reduzida para 90%:

struct OldElectric: Car, Electric {
    var color: Color // Needs to be a var since `Car` has a getter and setter.
    let price: Int
    var batteryLevel: Int

    func turnOn() {
        print("Starting all systems!")
    }

    mutating func drive() {
        print("Self driving engaged!")
        batteryLevel -= 8
    }

    mutating func reCharge() {
        print("Recharging the battery...")
        batteryLevel = 90
    }
}

Usos de biblioteca padrão de protocolos

Agora que temos uma ideia de como os protocolos no Swift funcionam, vamos ver alguns exemplos típicos de uso dos protocolos de biblioteca padrão.

Estenda a biblioteca padrão

Vamos ver como podemos adicionar funcionalidades adicionais aos tipos que já existem no Swift. Como os tipos no Swift não são incorporados, mas fazem parte da biblioteca padrão como structs, isso é fácil de fazer.

Vamos tentar fazer uma busca binária em uma matriz de elementos, enquanto também verificamos se a matriz está classificada:

extension Collection where Element: Comparable {
    // Verify that a `Collection` is sorted.
    func isSorted(_ order: (Element, Element) -> Bool) -> Bool {
        var i = index(startIndex, offsetBy: 1)

        while i < endIndex {
            // The longer way of calling a binary function like `<(_:_:)`, 
            // `<=(_:_:)`, `==(_:_:)`, etc.
            guard order(self[index(i, offsetBy: -1)], self[i]) else {
                return false
            }
            i = index(after: i)
        }
        return true
    }

    // Perform binary search on a `Collection`, verifying it is sorted.
    func binarySearch(_ element: Element) -> Index? {
        guard self.isSorted(<=) else {
            return nil
        }

        var low = startIndex
        var high = endIndex

        while low <= high {
            let mid = index(low, offsetBy: distance(from: low, to: high)/2)

            if self[mid] == element {
                return mid
            } else if self[mid] < element {
                low = index(after: mid)
            } else {
                high = index(mid, offsetBy: -1)
            }
        }

        return nil
    }
}

print([2, 2, 5, 7, 11, 13, 17].binarySearch(5)!)
print(["a", "b", "c", "d"].binarySearch("b")!)
print([1.1, 2.2, 3.3, 4.4, 5.5].binarySearch(3.3)!)
2
1
2

Fazemos isso estendendo o protocolo Collection que define "uma sequência cujos elementos podem ser percorridos várias vezes, de forma não destrutiva, e acessados ​​por um índice indexado". Como os arrays podem ser indexados usando a notação de colchetes, esse é o protocolo que queremos estender.

Da mesma forma, queremos apenas adicionar essa função utilitária a arrays cujos elementos possam ser comparados. Esta é a razão pela qual temos where Element: Comparable .

A cláusula where faz parte do sistema de tipos do Swift, que abordaremos em breve, mas em resumo nos permite adicionar requisitos adicionais à extensão que estamos escrevendo, como exigir que o tipo implemente um protocolo, exigir que dois tipos sejam os mesmo, ou exigir que uma classe tenha uma superclasse particular.

Element é o tipo associado dos elementos em um tipo em conformidade com a Collection . Element é definido dentro do protocolo Sequence , mas como Collection herda de Sequence , ele herda o tipo associado Element .

Comparable é um protocolo que define "um tipo que pode ser comparado usando os operadores relacionais < , <= , >= e > ." . Como estamos realizando uma pesquisa binária em uma Collection classificada, isso obviamente deve ser verdade, caso contrário, não sabemos se devemos recuar/iterar para a esquerda ou para a direita na pesquisa binária.

Como uma observação lateral sobre a implementação, para obter mais informações sobre a função index(_:offsetBy:) que foi usada, consulte a documentação a seguir.

Genéricos + protocolos = 💥

Genéricos e protocolos podem ser uma ferramenta poderosa se usados ​​corretamente para evitar código duplicado.

Em primeiro lugar, veja outro tutorial, A Swift Tour , que aborda brevemente os genéricos no final do livro do Colab.

Supondo que você tenha uma ideia geral sobre genéricos, vamos dar uma olhada rápida em alguns usos avançados.

Quando um único tipo tem vários requisitos, como um tipo em conformidade com vários protocolos, você tem várias opções à sua disposição:

typealias ComparableReal = Comparable & FloatingPoint

func foo1<T: ComparableReal>(a: T, b: T) -> Bool {
    return a > b
}

func foo2<T: Comparable & FloatingPoint>(a: T, b: T) -> Bool {
    return a > b
}

func foo3<T>(a: T, b: T) -> Bool where T: ComparableReal {
    return a > b
}

func foo4<T>(a: T, b: T) -> Bool where T: Comparable & FloatingPoint {
    return a > b
}

func foo5<T: FloatingPoint>(a: T, b: T) -> Bool where T: Comparable {
    return a > b
}

print(foo1(a: 1, b: 2))
print(foo2(a: 1, b: 2))
print(foo3(a: 1, b: 2))
print(foo4(a: 1, b: 2))
print(foo5(a: 1, b: 2))
false
false
false
false
false

Observe o uso de typealias no topo. Isso adiciona um alias nomeado de um tipo existente em seu programa. Depois que um alias de tipo é declarado, o nome de alias pode ser usado em vez do tipo existente em todo o programa. Os aliases de tipo não criam novos tipos; eles simplesmente permitem que um nome se refira a um tipo existente.

Agora, vamos ver como podemos usar protocolos e genéricos juntos.

Vamos imaginar que somos uma loja de informática com os seguintes requisitos em qualquer laptop que vendemos para determinar como os organizamos no fundo da loja:

enum Box {
    case small
    case medium
    case large
}

enum Mass {
    case light
    case medium
    case heavy
}

// Note: `CustomStringConvertible` protocol lets us pretty-print a `Laptop`.
struct Laptop: CustomStringConvertible {
    var name: String
    var box: Box
    var mass: Mass

    var description: String {
        return "(\(self.name) \(self.box) \(self.mass))"
    }
}

No entanto, temos um novo requisito de agrupar nossos Laptop por massa, pois as prateleiras têm restrições de peso.

func filtering(_ laptops: [Laptop], by mass: Mass) -> [Laptop] {
    return laptops.filter { $0.mass == mass }
}

let laptops: [Laptop] = [
    Laptop(name: "a", box: .small, mass: .light),
    Laptop(name: "b", box: .large, mass: .medium),
    Laptop(name: "c", box: .medium, mass: .heavy),
    Laptop(name: "d", box: .large, mass: .light)
]

let filteredLaptops = filtering(laptops, by: .light)
print(filteredLaptops)
[(a small light), (d large light)]

No entanto, e se quiséssemos filtrar por algo diferente de Mass ?

Uma opção é fazer o seguinte:

// Define a protocol which will act as our comparator.
protocol DeviceFilterPredicate {
    associatedtype Device
    func shouldKeep(_ item: Device) -> Bool
}

// Define the structs we will use for passing into our filtering function.
struct BoxFilter: DeviceFilterPredicate {
    typealias Device = Laptop
    var box: Box 

    func shouldKeep(_ item: Laptop) -> Bool {
        return item.box == box
    }
}

struct MassFilter: DeviceFilterPredicate {
    typealias Device = Laptop  
    var mass: Mass

    func shouldKeep(_ item: Laptop) -> Bool {
        return item.mass == mass
    }
}

// Make sure our filter conforms to `DeviceFilterPredicate` and that we are 
// filtering `Laptop`s.
func filtering<F: DeviceFilterPredicate>(
    _ laptops: [Laptop], 
    by filter: F
) -> [Laptop] where Laptop == F.Device {
    return laptops.filter { filter.shouldKeep($0) }
}

// Let's test the function out!
print(filtering(laptops, by: BoxFilter(box: .large)))
print(filtering(laptops, by: MassFilter(mass: .heavy)))
[(b large medium), (d large light)]
[(c medium heavy)]

Incrível! Agora podemos filtrar com base em qualquer restrição de laptop. No entanto, só podemos filtrar Laptop .

Que tal poder filtrar qualquer coisa que esteja em uma caixa e tenha massa? Talvez este armazém de laptops também seja usado para servidores que tenham uma base de clientes diferente:

// Define 2 new protocols so we can filter anything in a box and which has mass.
protocol Weighable {
    var mass: Mass { get }
}

protocol Boxed {
    var box: Box { get }
}

// Define the new Laptop and Server struct which have mass and a box.
struct Laptop: CustomStringConvertible, Boxed, Weighable {
    var name: String
    var box: Box
    var mass: Mass

    var description: String {
        return "(\(self.name) \(self.box) \(self.mass))"
    }
}

struct Server: CustomStringConvertible, Boxed, Weighable {
    var isWorking: Bool
    var name: String
    let box: Box
    let mass: Mass

    var description: String {
        if isWorking {
            return "(working \(self.name) \(self.box) \(self.mass))"
        } else {
            return "(notWorking \(self.name) \(self.box) \(self.mass))"
        }
    }
}

// Define the structs we will use for passing into our filtering function.
struct BoxFilter<T: Boxed>: DeviceFilterPredicate {
    var box: Box 

    func shouldKeep(_ item: T) -> Bool {
        return item.box == box
    }
}

struct MassFilter<T: Weighable>: DeviceFilterPredicate {
    var mass: Mass

    func shouldKeep(_ item: T) -> Bool {
        return item.mass == mass
    }
}

// Define the new filter function.
func filtering<F: DeviceFilterPredicate, T>(
    _ elements: [T], 
    by filter: F
) -> [T] where T == F.Device {
    return elements.filter { filter.shouldKeep($0) }
}


// Let's test the function out!
let servers = [
    Server(isWorking: true, name: "serverA", box: .small, mass: .heavy),
    Server(isWorking: false, name: "serverB", box: .medium, mass: .medium),
    Server(isWorking: true, name: "serverC", box: .large, mass: .light),
    Server(isWorking: false, name: "serverD", box: .medium, mass: .light),
    Server(isWorking: true, name: "serverE", box: .small, mass: .heavy)
]

let products = [
    Laptop(name: "a", box: .small, mass: .light),
    Laptop(name: "b", box: .large, mass: .medium),
    Laptop(name: "c", box: .medium, mass: .heavy),
    Laptop(name: "d", box: .large, mass: .light)
]

print(filtering(servers, by: BoxFilter(box: .small)))
print(filtering(servers, by: MassFilter(mass: .medium)))

print(filtering(products, by: BoxFilter(box: .small)))
print(filtering(products, by: MassFilter(mass: .medium)))
[(working serverA small heavy), (working serverE small heavy)]
[(notWorking serverB medium medium)]
[(a small light)]
[(b large medium)]

Agora conseguimos filtrar um array não apenas por qualquer propriedade de uma struct específica, mas também por qualquer struct que tenha essa propriedade!

Dicas para um bom design de API

Esta seção foi extraída da palestra WWDC 2019: Modern Swift API Design .

Agora que você entende como os protocolos se comportam, é melhor revisar quando você deve usar protocolos. Por mais poderosos que os protocolos possam ser, nem sempre é a melhor ideia mergulhar e começar imediatamente com os protocolos.

  • Comece com casos de uso concretos:
    • Primeiro, explore o caso de uso com tipos concretos e entenda qual código você deseja compartilhar e descubra que está sendo repetido. Então, fator esse código compartilhado com genéricos. Pode significar criar novos protocolos. Descubra a necessidade de código genérico.
  • Considere compor novos protocolos a partir de protocolos existentes definidos na biblioteca padrão. Consulte a seguinte documentação da Apple para obter um bom exemplo disso.
  • Em vez de um protocolo genérico, considere definir um tipo genérico.

Exemplo: definir um tipo de vetor personalizado

Digamos que queremos definir um protocolo GeometricVector em números de ponto flutuante para usar em algum aplicativo de geometria que estamos criando, que define 3 operações vetoriais importantes:

protocol GeometricVector {
    associatedtype Scalar: FloatingPoint
    static func dot(_ a: Self, _ b: Self) -> Scalar
    var length: Scalar { get }
    func distance(to other: Self) -> Scalar
}

Digamos que queremos armazenar as dimensões do vetor, com as quais o protocolo SIMD pode nos ajudar, então faremos nosso novo tipo refinar o protocolo SIMD . Os vetores SIMD podem ser considerados vetores de tamanho fixo que são muito rápidos quando você os usa para realizar operações vetoriais:

protocol GeometricVector: SIMD {
    associatedtype Scalar: FloatingPoint
    static func dot(_ a: Self, _ b: Self) -> Scalar
    var length: Scalar { get }
    func distance(to other: Self) -> Scalar
}

Agora, vamos definir as implementações padrão das operações acima:

extension GeometricVector {
    static func dot(_ a: Self, _ b: Self) -> Scalar {
        (a * b).sum()
    }

    var length: Scalar {
        Self.dot(self, self).squareRoot()
    }

    func distance(to other: Self) -> Scalar {
        (self - other).length
    }
}

E então precisamos adicionar uma conformidade a cada um dos tipos que queremos adicionar essas habilidades:

extension SIMD2: GeometricVector where Scalar: FloatingPoint { }
extension SIMD3: GeometricVector where Scalar: FloatingPoint { }
extension SIMD4: GeometricVector where Scalar: FloatingPoint { }
extension SIMD8: GeometricVector where Scalar: FloatingPoint { }
extension SIMD16: GeometricVector where Scalar: FloatingPoint { }
extension SIMD32: GeometricVector where Scalar: FloatingPoint { }
extension SIMD64: GeometricVector where Scalar: FloatingPoint { }

Esse processo de três etapas para definir o protocolo, dar a ele uma implementação padrão e adicionar uma conformidade a vários tipos é bastante repetitivo.

O protocolo foi necessário?

O fato de nenhum dos tipos SIMD ter implementações exclusivas é um sinal de alerta. Então, neste caso, o protocolo não está realmente nos dando nada.

Definindo-o em uma extensão do SIMD

Se escrevermos os 3 operadores em uma extensão do protocolo SIMD , isso pode resolver o problema de forma mais sucinta:

extension SIMD where Scalar: FloatingPoint {
    static func dot(_ a: Self, _ b: Self) -> Scalar {
        (a * b).sum()
    }

    var length: Scalar {
        Self.dot(self, self).squareRoot()
    }

    func distance(to other: Self) -> Scalar {
        (self - other).length
    }
}

Usando menos linhas de código, adicionamos todas as implementações padrão a todos os tipos de SIMD .

Às vezes você pode ficar tentado a criar essa hierarquia de tipos, mas lembre-se de que nem sempre é necessário. Isso também significa que o tamanho binário do seu programa compilado será menor e seu código será mais rápido para compilar.

No entanto, essa abordagem de extensão é ótima para quando você tem alguns métodos que deseja adicionar. No entanto, ocorre um problema de escalabilidade quando você está projetando uma API maior.

É um? Tem um?

Anteriormente, dissemos que o GeometricVector refinaria o SIMD . Mas isso é um relacionamento é-um? O problema é que o SIMD define operações que nos permitem adicionar um escalar 1 a um vetor, mas não faz sentido definir tal operação no contexto da geometria.

Então, talvez um relacionamento has-a seja melhor envolvendo SIMD em um novo tipo genérico que possa lidar com qualquer número de ponto flutuante:

// NOTE: `Storage` is the underlying type that is storing the values, 
// just like in a `SIMD` vector.
struct GeometricVector<Storage: SIMD> where Storage.Scalar: FloatingPoint {
    typealias Scalar = Storage.Scalar
    var value: Storage
    init(_ value: Storage) { self.value = value }
}

Podemos então ser cuidadosos e definir apenas as operações que fazem sentido apenas no contexto da geometria:

extension GeometricVector {
    static func + (a: Self, b: Self) -> Self {
        Self(a.value + b.value)
    }

    static func - (a: Self, b: Self) -> Self {
        Self(a.value - b.value)
    }
    static func * (a: Self, b: Scalar) -> Self {
        Self(a.value * b)
    }
}

E ainda podemos usar extensões genéricas para obter os 3 operadores anteriores que queríamos implementar, que parecem quase exatamente os mesmos de antes:

extension GeometricVector {
    static func dot(_ a: Self, _ b: Self) -> Scalar {
        (a.value * b.value).sum()
    }

    var length: Scalar {
        Self.dot(self, self).squareRoot()
    }

    func distance(to other: Self) -> Scalar {
        (self - other).length
    }
}

No geral, conseguimos refinar o comportamento de nossas três operações para um tipo simplesmente usando um struct. Com protocolos, enfrentamos o problema de escrever conformidades repetitivas para todos os vetores SIMD , e também não conseguimos impedir que certos operadores como Scalar + Vector estivessem disponíveis (o que neste caso não queríamos). Como tal, lembre-se de que os protocolos não são uma solução completa. Mas, às vezes, soluções mais tradicionais podem ser mais poderosas.

Mais recursos de programação orientados a protocolo

Aqui estão recursos adicionais sobre os tópicos discutidos:

  • WWDC 2015: Protocol-Oriented Programming em Swift : isso foi apresentado usando Swift 2, então muita coisa mudou desde então (por exemplo, nome dos protocolos que eles usaram na apresentação), mas ainda é um bom recurso para a teoria e usos por trás dela .
  • Apresentando a Programação Orientada a Protocolos em Swift 3 : isso foi escrito em Swift 3, então parte do código pode precisar ser modificado para que ele seja compilado com sucesso, mas é outro ótimo recurso.
  • WWDC 2019: Modern Swift API Design : aborda as diferenças entre os tipos de valor e de referência, um caso de uso de quando os protocolos podem ser a pior escolha no design de API (o mesmo que a seção "Dicas para um bom design de API" acima), chave pesquisa de membro de caminho e wrappers de propriedade.
  • Generics : documentação própria do Swift para o Swift 5 sobre genéricos.