Programación y programación orientada a protocolos. genéricos

Ver en TensorFlow.org Ver código fuente en GitHub

Este tutorial repasará la programación orientada a protocolos y diferentes ejemplos de cómo se pueden utilizar con genéricos en los ejemplos del día a día.

Protocolos

La herencia es una forma poderosa de organizar el código en lenguajes de programación que le permite compartir código entre múltiples componentes del programa.

En Swift, existen diferentes formas de expresar la herencia. Quizás ya estés familiarizado con una de esas formas, en otros lenguajes: la herencia de clases. Sin embargo, Swift tiene otra forma: protocolos.

En este tutorial, exploraremos los protocolos, una alternativa a la subclasificación que le permite lograr objetivos similares a través de diferentes compensaciones. En Swift, los protocolos contienen varios miembros abstractos. Las clases, estructuras y enumeraciones pueden ajustarse a múltiples protocolos y la relación de conformidad se puede establecer de forma retroactiva. Todo eso permite algunos diseños que no se pueden expresar fácilmente en Swift mediante subclases. Analizaremos los modismos que respaldan el uso de protocolos (extensiones y restricciones de protocolo), así como las limitaciones de los protocolos.

¡Tipos de valores de Swift 💖!

Además de las clases que tienen semántica de referencia, Swift admite enumeraciones y estructuras que se pasan por valor. Las enumeraciones y estructuras admiten muchas características proporcionadas por las clases. ¡Vamos a ver!

En primer lugar, veamos en qué se parecen las enumeraciones a las clases:

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

Ahora, veamos las estructuras. Tenga en cuenta que no podemos heredar estructuras, sino que 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, veamos cómo se pasan por tipos de valores a diferencia de las clases:

// 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

Usemos protocolos

Empecemos por crear protocolos para diferentes coches:

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 }
}

En un mundo orientado a objetos (sin herencia múltiple), es posible que haya creado clases abstractas de Electric y Gas y luego haya usado la herencia de clases para que ambas hereden de Car y luego tener un modelo de automóvil específico como clase base. Sin embargo, aquí ambos son protocolos completamente separados con acoplamiento cero . Esto hace que todo el sistema sea más flexible en su diseño.

Definamos un 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)

Esto especifica una nueva estructura TeslaModelS que se ajusta a los protocolos Car y Electric .

Ahora definamos un automóvil propulsado por 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)

Ampliar protocolos con comportamientos predeterminados

Lo que puedes notar en los ejemplos es que tenemos cierta redundancia. Cada vez que recargamos un automóvil eléctrico, debemos establecer el nivel de porcentaje de la batería en 100. Dado que todos los automóviles eléctricos tienen una capacidad máxima del 100%, pero los automóviles de gasolina varían según la capacidad del tanque de gasolina, podemos establecer el nivel predeterminado en 100 para los automóviles eléctricos. .

Aquí es donde las extensiones en Swift pueden resultar útiles:

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

Así que ahora, cualquier coche eléctrico nuevo que creemos pondrá la batería a 100 cuando lo recarguemos. Por lo tanto, acabamos de poder decorar clases, estructuras y enumeraciones con un comportamiento único y predeterminado.

Cómic de protocolo

¡Gracias a Ray Wenderlich por el cómic!

Sin embargo, una cosa a tener en cuenta es la siguiente. En nuestra primera implementación, definimos foo() como una implementación predeterminada en A , pero no la hacemos obligatoria en el protocolo. Entonces, cuando llamamos a.foo() , obtenemos " A default " impreso.

protocol Default {}

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

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

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

Sin embargo, si hacemos foo() sea obligatorio en A , obtenemos " 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

Esto ocurre debido a una diferencia entre el envío estático en el primer ejemplo y el envío estático en el segundo en protocolos en Swift. Para obtener más información, consulte esta publicación mediana .

Anular el comportamiento predeterminado

Sin embargo, si queremos, aún podemos anular el comportamiento predeterminado. Una cosa importante a tener en cuenta es que esto no admite el envío dinámico .

Digamos que tenemos una versión anterior de un coche eléctrico, por lo que el estado de la batería se ha reducido al 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 protocolos en la biblioteca estándar

Ahora que tenemos una idea de cómo funcionan los protocolos en Swift, veamos algunos ejemplos típicos del uso de los protocolos de la biblioteca estándar.

Ampliar la biblioteca estándar

Veamos cómo podemos agregar funcionalidad adicional a los tipos que ya existen en Swift. Dado que los tipos en Swift no están integrados, sino que son parte de la biblioteca estándar como estructuras, esto es fácil de hacer.

Intentemos realizar una búsqueda binaria en una matriz de elementos, asegurándonos al mismo tiempo de verificar que la matriz esté ordenada:

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

Hacemos esto ampliando el protocolo Collection que define "una secuencia cuyos elementos se pueden atravesar varias veces, de forma no destructiva, y se puede acceder a ellos mediante un subíndice indexado". Dado que las matrices se pueden indexar utilizando la notación de corchetes, este es el protocolo que queremos ampliar.

De manera similar, solo queremos agregar esta función de utilidad a matrices cuyos elementos se puedan comparar. Esta es la razón por la que tenemos where Element: Comparable .

La cláusula where es parte del sistema de tipos de Swift, que cubriremos pronto, pero en resumen nos permite agregar requisitos adicionales a la extensión que estamos escribiendo, como requerir que el tipo implemente un protocolo, requerir que dos tipos sean el lo mismo, o requerir que una clase tenga una superclase particular.

Element es el tipo asociado de los elementos en un tipo conforme a Collection . Element se define dentro del protocolo Sequence , pero como Collection hereda de Sequence , hereda el tipo asociado Element .

Comparable es un protocolo que define "un tipo que se puede comparar utilizando los operadores relacionales < , <= , >= y > ". . Dado que estamos realizando una búsqueda binaria en una Collection ordenada, esto, por supuesto, tiene que ser cierto o, de lo contrario, no sabemos si recurrir/iterar hacia la izquierda o hacia la derecha en la búsqueda binaria.

Como nota al margen sobre la implementación, para obtener más información sobre la función index(_:offsetBy:) que se utilizó, consulte la siguiente documentación .

Genéricos + protocolos = 💥

Los genéricos y los protocolos pueden ser una herramienta poderosa si se usan correctamente para evitar código duplicado.

En primer lugar, consulte otro tutorial, A Swift Tour , que cubre brevemente los genéricos al final del libro de Colab.

Suponiendo que tenga una idea general sobre los genéricos, echemos un vistazo rápido a algunos usos avanzados.

Cuando un solo tipo tiene múltiples requisitos, como por ejemplo un tipo que se ajusta a varios protocolos, tiene varias opciones a su disposición:

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 el uso de typealias en la parte superior. Esto agrega un alias con nombre de un tipo existente a su programa. Después de declarar un alias de tipo, el nombre del alias se puede utilizar en lugar del tipo existente en todas partes de su programa. Los alias de tipo no crean nuevos tipos; simplemente permiten que un nombre se refiera a un tipo existente.

Ahora veamos cómo podemos usar protocolos y genéricos juntos.

Imaginemos que somos una tienda de informática con los siguientes requisitos en cualquier portátil que vendamos para determinar cómo los organizamos en la parte trasera de la tienda:

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))"
    }
}

Sin embargo, tenemos el nuevo requisito de agrupar nuestras Laptop por masa ya que los estantes tienen restricciones 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)]

Sin embargo, ¿qué pasaría si quisiéramos filtrar por algo que no sea Mass ?

Una opción es hacer lo siguiente:

// 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)]

¡Impresionante! Ahora podemos filtrar según cualquier restricción de la computadora portátil. Sin embargo, sólo podemos filtrar Laptop .

¿Qué tal poder filtrar cualquier cosa que esté en una caja y tenga masa? Quizás este almacén de portátiles también se utilice para servidores que tienen una 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)]

¡Ahora hemos podido filtrar una matriz no solo por cualquier propiedad de una struct específica, sino también por cualquier estructura que tenga esa propiedad!

Consejos para un buen diseño de API

Esta sección fue tomada de la charla WWDC 2019: Modern Swift API Design .

Ahora que comprende cómo se comportan los protocolos, es mejor repasar cuándo debe utilizarlos. Por más poderosos que puedan ser los protocolos, no siempre es la mejor idea sumergirse y comenzar inmediatamente con ellos.

  • Comience con casos de uso concretos:
    • Primero explore el caso de uso con tipos concretos y comprenda qué código desea compartir y descubra que se repite. Luego, factorice ese código compartido con los genéricos. Podría significar crear nuevos protocolos. Descubra la necesidad de código genérico.
  • Considere la posibilidad de crear nuevos protocolos a partir de protocolos existentes definidos en la biblioteca estándar. Consulte la siguiente documentación de Apple para ver un buen ejemplo de esto.
  • En lugar de un protocolo genérico, considere definir un tipo genérico.

Ejemplo: definir un tipo de vector personalizado

Digamos que queremos definir un protocolo GeometricVector en números de punto flotante para usarlo en alguna aplicación de geometría que estamos creando y que define 3 operaciones vectoriales 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 almacenar las dimensiones del vector, con lo que el protocolo SIMD puede ayudarnos, por lo que haremos que nuestro nuevo tipo refine el protocolo SIMD . Los vectores SIMD se pueden considerar como vectores de tamaño fijo que son muy rápidos cuando se usan para realizar operaciones vectoriales:

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

Ahora, definamos las implementaciones predeterminadas de las operaciones anteriores:

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
    }
}

Y luego necesitamos agregar una conformidad a cada uno de los tipos a los que queremos agregar estas 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 proceso de tres pasos para definir el protocolo, darle una implementación predeterminada y luego agregar una conformidad a múltiples tipos es bastante repetitivo.

¿Era necesario el protocolo?

El hecho de que ninguno de los tipos SIMD tenga implementaciones únicas es una señal de advertencia. Entonces, en este caso, el protocolo realmente no nos está dando nada.

Definirlo en una extensión de SIMD

Si escribimos los 3 operadores en una extensión del protocolo SIMD , esto puede solucionar el problema de forma más 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 líneas de código, agregamos todas las implementaciones predeterminadas a todos los tipos de SIMD .

A veces puedes sentirte tentado a crear esta jerarquía de tipos, pero recuerda que no siempre es necesario. Esto también significa que el tamaño binario de su programa compilado será más pequeño y su código será más rápido de compilar.

Sin embargo, este enfoque de extensión es excelente cuando tiene varios métodos que desea agregar. Sin embargo, se enfrenta a un problema de escalabilidad cuando se diseña una API más grande.

¿Es un? ¿Tiene un?

Anteriormente dijimos que GeometricVector refinaría SIMD . ¿Pero es esta una relación es-un? El problema es que SIMD define operaciones que nos permiten agregar un escalar 1 a un vector, pero no tiene sentido definir tal operación en el contexto de la geometría.

Entonces, tal vez una relación tiene-a sería mejor envolviendo SIMD en un nuevo tipo genérico que pueda manejar cualquier número de punto flotante:

// 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 }
}

Entonces podemos tener cuidado y definir sólo las operaciones que tengan sentido únicamente en el contexto de la geometría:

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)
    }
}

Y aún podemos usar extensiones genéricas para obtener los 3 operadores anteriores que queríamos implementar y que se ven casi exactamente iguales que 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
    }
}

En general, hemos podido refinar el comportamiento de nuestras tres operaciones a un tipo simplemente usando una estructura. Con los protocolos, enfrentamos el problema de escribir conformidades repetitivas para todos los vectores SIMD y tampoco pudimos evitar que ciertos operadores como Scalar + Vector estuvieran disponibles (que en este caso no queríamos). Como tal, recuerde que los protocolos no son la solución definitiva. Pero a veces las soluciones más tradicionales pueden resultar más poderosas.

Más recursos de programación orientados a protocolos

Aquí hay recursos adicionales sobre los temas discutidos:

  • WWDC 2015: Programación orientada a protocolos en Swift : esto se presentó usando Swift 2, por lo que muchas cosas han cambiado desde entonces (por ejemplo, el nombre de los protocolos que usaron en la presentación), pero sigue siendo un buen recurso para la teoría y los usos detrás de ella. .
  • Presentación de la programación orientada a protocolos en Swift 3 : esto se escribió en Swift 3, por lo que es posible que sea necesario modificar parte del código para que se compile correctamente, pero es otro gran recurso.
  • WWDC 2019: Diseño moderno de API Swift : repasa las diferencias entre los tipos de valor y de referencia, un caso de uso en el que los protocolos pueden resultar la peor opción en el diseño de API (igual que la sección "Consejos para un buen diseño de API" anterior), clave búsqueda de miembros de ruta y envoltorios de propiedades.
  • Genéricos : documentación propia de Swift para Swift 5, todo sobre genéricos.