Esta página foi traduzida pela API Cloud Translation.
Switch to English

Programação orientada a protocolo & amp; genéricos

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

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

Protocolos

A herança é uma maneira poderosa de organizar código em linguagens de programação que permite compartilhar 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 maneiras, de outros idiomas: herança de classe. No entanto, Swift tem outra maneira: protocolos.

Neste tutorial, exploraremos os 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, estruturas e enums podem estar em conformidade com vários protocolos e o relacionamento de conformidade pode ser estabelecido retroativamente. Tudo isso permite alguns projetos que não são facilmente expressáveis ​​no Swift usando subclassificação. 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 suporta enumerações e estruturas que são passadas por valor. Enums e estruturas suportam muitos recursos fornecidos pelas classes. Vamos dar uma olhada!

Primeiro, vamos ver como as 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 olhar para estruturas. Observe que não podemos herdar estruturas, 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

Por fim, vamos ver como eles são transmitidos por tipos de valor, diferentemente 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 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 criado as classes abstratas Electric e Gas e, em seguida, usado a herança de classe para herdar ambas da 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 maneira 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 esteja 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 no Swift podem ser úteis:

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

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

Protocol Comic

Obrigado a Ray Wenderlich pela história em quadrinhos!

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 exigida 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 fizermos foo() necessá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 envio estático no primeiro exemplo e o envio estático no segundo nos protocolos no Swift. Para mais informações, consulte esta publicação Média .

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 o envio dinâmico .

Digamos que temos uma versão mais antiga de um carro elétrico, portanto a vida útil 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 da biblioteca padrão de protocolos

Agora que temos uma idéia de como os protocolos do 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 estruturas, isso é fácil de fazer.

Vamos tentar fazer uma pesquisa binária em uma matriz de elementos, além de verificar 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 maneira não destrutiva e acessados ​​por um índice indexado". Como matrizes podem ser indexadas usando a notação entre colchetes, esse é o protocolo que queremos estender.

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

A cláusula where faz parte do sistema de tipos da Swift, que abordaremos em breve, mas, resumidamente, vamos adicionar requisitos adicionais à extensão que estamos escrevendo, como exigir que o tipo implemente um protocolo, exigir que dois tipos sejam os mesmo ou para exigir que uma classe tenha uma superclasse específica.

Element é o tipo associado dos elementos em um tipo de Collection compatível. Element é definido no protocolo Sequence , mas como a Collection herda da Sequence , ele herda o tipo associado ao 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, é claro que isso deve ser verdadeiro ou não sabemos se é necessário recursar / iterar à 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:) 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.

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

Supondo que você tenha uma idéia geral sobre genéricos, vamos examinar rapidamente alguns usos avançados.

Quando um único tipo possui 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 na parte superior. Isso adiciona um alias nomeado de um tipo existente ao seu programa. Depois que um alias de tipo é declarado, o nome alternativo pode ser usado em vez do tipo existente em qualquer lugar do seu 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 que vendemos 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 para agrupar nossos Laptop em 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 que não fosse a 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 somos capazes de filtrar com base em qualquer restrição de laptop. No entanto, só podemos filtrar Laptop .

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

Dicas para um bom design de API

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

Agora que você entende como os protocolos se comportam, é melhor revisar quando você deve usar os protocolos. Por mais poderosos que os protocolos possam ser, nem sempre é a melhor ideia para 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. Em seguida, fatore esse código compartilhado com os 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: definindo 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 queremos armazenar as dimensões do vetor, com as quais o protocolo SIMD pode nos ajudar, portanto 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 utiliza para executar operações de vetor:

 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, fornecer 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 aviso. 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 poderá resolver o problema de maneira 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 ser 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ê cria uma API maior.

É um? Tem um?

Anteriormente, dissemos que o GeometricVector refinaria 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 essa operação no contexto da geometria.

Portanto, talvez um relacionamento tenha um seria melhor envolvendo o 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 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 três 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 uma estrutura. Com os protocolos, enfrentamos a questão de escrever conformidades repetitivas para todos os vetores SIMD e também não conseguimos impedir que certos operadores como o 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. Às vezes, porém, 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: Programação Orientada a Protocolo no Swift : isso foi apresentado usando o Swift 2, então muita coisa mudou desde então (por exemplo, nome dos protocolos que eles usaram na apresentação), mas esse ainda é um bom recurso para a teoria e seus usos. .
  • Introduzindo a programação orientada a protocolo no Swift 3 : isso foi escrito no Swift 3; portanto, parte do código pode precisar ser modificado para que ele seja compilado com êxito, mas é outro ótimo recurso.
  • WWDC 2019: Design moderno da API Swift : aborda as diferenças entre os tipos de valor e referência, um caso de uso em que os protocolos podem ser a pior escolha no design da API (o mesmo que a seção "Dicas para um bom design da API" acima), chave pesquisa de membro do caminho e wrappers de propriedades.
  • Genéricos : a própria documentação do Swift para o Swift 5, sobre genéricos.