RSVP para seu evento TensorFlow Everywhere hoje mesmo!
Esta página foi traduzida pela API Cloud Translation.
Switch to English

Programação orientada a protocolo e genéricos

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

Este tutorial irá abordar a programação orientada a protocolo 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 o código entre vários componentes do programa.

No Swift, existem diferentes maneiras de expressar herança. Você já deve estar familiarizado com uma dessas formas em outras linguagens: herança de classe. No entanto, o Swift tem outra maneira: protocolos.

Neste tutorial, exploraremos os protocolos - uma alternativa à subclasse que permite atingir objetivos semelhantes por meio de diferentes compensações. Em 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 expressos em Swift usando subclasses. Percorreremos 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 têm semântica de referência, o Swift oferece suporte a enums e estruturas que são passadas por valor. Enums e structs oferecem suporte a muitos recursos fornecidos por classes. Vamos dar uma olhada!

Em primeiro lugar, vamos ver como enums são semelhantes a 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 dar uma olhada nas 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 de 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 carros diferentes:

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 , em seguida, usado a herança de classe para fazer com que ambas herdem de Car , e então 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 gás:

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 nos exemplos é que temos alguma redundância. Sempre 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 gás variam entre a capacidade do tanque de gasolina, podemos padronizar o nível para 100 para carros elétricos .

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

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

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

Protocolo Comic

Obrigado a Ray Wenderlich pelos quadrinhos!

No entanto, uma coisa a ser observada é 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. Portanto, 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 , obteremos " 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 o despacho estático no primeiro exemplo e o despacho estático no segundo em protocolos em Swift. Para mais informações, consulte esta postagem do Medium .

Substituindo o comportamento padrão

No entanto, se quisermos, ainda podemos substituir o comportamento padrão. Uma coisa importante a observar é que isso não oferece suporte a 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
    }
}

Uso de protocolos da biblioteca padrão

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

Amplie 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 integrados, mas fazem parte da biblioteca padrão como estruturas, isso é fácil de fazer.

Vamos tentar fazer uma pesquisa binária em uma matriz de elementos, ao mesmo tempo que 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 protocoloCollection , que define "uma sequência cujos elementos podem ser percorridos várias vezes, de forma não destrutiva, e acessados ​​por um subscrito 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 de utilidade a matrizes cujos elementos podem ser comparados. Esta é a razão pela qual temos where Element: Comparable .

A cláusula where é uma parte do sistema de tipo 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, para exigir que dois tipos sejam o mesmo, ou para 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 de Sequence , mas como a Collection herda da Sequence , ela herda o tipo associado do Element .

Comparable é um protocolo que define "um tipo que pode ser comparado usando os operadores relacionais < , <= , >= e > ." . Uma vez que estamos realizando uma pesquisa binária em uma Collection classificada, isso obviamente tem que ser verdade, ou então não sabemos se devemos repetir / iterar para a esquerda ou 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 seguinte documentação .

Genéricos + protocolos = 💥

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

Primeiramente, dê uma olhada em outro tutorial, A Swift Tour , que cobre brevemente os genéricos no final do livro Colab.

Supondo que você tenha uma ideia geral sobre os genéricos, vamos rapidamente dar uma olhada 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 do 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 computadores com os seguintes requisitos em qualquer laptop vendido para determinar como os organizamos na parte de trás 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, uma vez que 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 da 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)]

Impressionante! Agora podemos filtrar com base em qualquer restrição do laptop. No entanto, só podemos filtrar Laptop .

Que tal ser capaz de filtrar qualquer coisa que esteja em uma caixa e tenha massa? Talvez este armazém de laptops também seja usado para servidores que possuem 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 podemos filtrar um array não apenas por qualquer propriedade de uma struct específica, mas também podemos filtrar qualquer estrutura que tenha essa propriedade!

Dicas para um bom design de API

Esta seção foi retirada do WWDC 2019: palestra sobre o design moderno do Swift API .

Agora que você entende como os protocolos se comportam, é melhor revisar quando você deve usá-los. Por mais poderosos que os protocolos possam ser, nem sempre é a melhor ideia mergulhar de cabeça 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 encontrar está sendo repetido. Em seguida, considere esse código compartilhado com os genéricos. Pode significar criar novos protocolos. Descubra a necessidade de código genérico.
  • Considere a composição de 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 fazendo, que define três 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 desejemos armazenar as dimensões do vetor, nas quais o protocolo SIMD pode nos ajudar, então faremos nosso novo tipo refinar o protocolo SIMD . SIMD 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 para cada um dos tipos aos quais 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 { }

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

O protocolo era necessário?

O fato de nenhum dos tipos SIMD ter implementações exclusivas é um sinal de alerta. Portanto, 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 programa compilado será menor e o código será mais rápido de compilar.

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

É um? Tem um?

Anteriormente, dissemos que o GeometricVector iria refinar o SIMD . Mas isso é um relacionamento é? 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 tem um seria melhor envolvendo o SIMD em um novo tipo genérico que pode 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, ter cuidado 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, pudemos refinar o comportamento de nossas três operações para um tipo simplesmente usando uma estrutura. Com os protocolos, enfrentamos o problema de escrever conformações repetitivas para todos os vetores SIMD , e também não fomos capazes de 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 definitiva. Mas às vezes as soluções mais tradicionais podem provar ser mais poderosas.

Mais recursos de programação orientada a protocolo

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

  • WWDC 2015: Programação Orientada a Protocolo em Swift : isso foi apresentado usando Swift 2, portanto, muita coisa mudou desde então (por exemplo, o nome dos protocolos usados ​​na apresentação), mas ainda é um bom recurso para a teoria e os usos por trás dela .
  • Introdução à Programação Orientada a Protocolo no Swift 3 : isso foi escrito em Swift 3, portanto, parte do código pode precisar ser modificado para que seja compilado com sucesso, mas é outro grande recurso.
  • WWDC 2019: Design moderno de API Swift : examina as diferenças entre os tipos de valor e referência, um caso de uso de quando os protocolos podem provar 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.
  • Genéricos : documentação do próprio Swift para o Swift 5, tudo sobre genéricos.