Protokol odaklı programlama ve jenerikler

TensorFlow.org'da görüntüle Google Colab'da çalıştırın Kaynağı GitHub'da görüntüle

Bu eğitimde protokol odaklı programlama ve günlük örneklerde bunların jeneriklerle nasıl kullanılabileceğine dair farklı örnekler ele alınacaktır.

Protokoller

Kalıtım, kodu programın birden fazla bileşeni arasında paylaşmanıza olanak tanıyan, programlama dillerinde kodu düzenlemenin güçlü bir yoludur.

Swift'de mirası ifade etmenin farklı yolları vardır. Bu yollardan birine diğer dillerden zaten aşina olabilirsiniz: sınıf kalıtımı. Ancak Swift'in başka bir yolu daha var: protokoller.

Bu eğitimde, farklı ödünleşimler yoluyla benzer hedeflere ulaşmanıza olanak tanıyan alt sınıflandırmaya bir alternatif olan protokolleri inceleyeceğiz. Swift'de protokoller birden fazla soyut üye içerir. Sınıflar, yapılar ve numaralandırmalar birden fazla protokole uygun olabilir ve uyumluluk ilişkisi geriye dönük olarak kurulabilir. Tüm bunlar, Swift'de alt sınıflandırma kullanılarak kolayca ifade edilemeyen bazı tasarımlara olanak tanır. Protokollerin (uzantılar ve protokol kısıtlamaları) kullanımını destekleyen deyimlerin yanı sıra protokollerin sınırlamalarını da inceleyeceğiz.

Swift 💖'in değer türleri!

Referans anlambilimine sahip sınıflara ek olarak Swift, değere göre iletilen numaralandırmaları ve yapıları da destekler. Numaralandırmalar ve yapılar, sınıfların sağladığı birçok özelliği destekler. Hadi bir bakalım!

Öncelikle numaralandırmaların sınıflara ne kadar benzediğine bakalım:

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

Şimdi yapılara bakalım. Yapıları miras alamayacağımıza, bunun yerine protokolleri kullanabileceğimize dikkat edin:

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

Son olarak, bunların sınıflardan farklı olarak değer türlerine göre nasıl aktarıldığını görelim:

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

Protokolleri kullanalım

Farklı arabalar için protokoller oluşturarak başlayalım:

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

Nesne yönelimli bir dünyada (çoklu kalıtımın olmadığı), Electric ve Gas soyut sınıflarını oluşturmuş, ardından her ikisinin de Car miras alınmasını sağlamak için sınıf mirasını kullanmış ve ardından belirli bir araba modelinin temel sınıf olmasını sağlamış olabilirsiniz. Ancak burada her ikisi de sıfır bağlantıya sahip tamamen ayrı protokollerdir! Bu, tüm sistemi nasıl tasarladığınız konusunda daha esnek hale getirir.

Bir Tesla'yı tanımlayalım:

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)

Bu, hem Car hem de Electric protokollerine uyan yeni bir TeslaModelS yapısını belirtir.

Şimdi gazla çalışan bir arabayı tanımlayalım:

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)

Protokolleri varsayılan davranışlarla genişletin

Örneklerden fark edebileceğiniz şey, bazı fazlalıklarımızın olduğudur. Elektrikli bir arabayı her şarj ettiğimizde, pil yüzdesi seviyesini 100'e ayarlamamız gerekir. Tüm elektrikli arabaların maksimum kapasitesi %100 olduğundan, ancak benzinli arabaların benzin deposu kapasitesi arasında değişiklik gösterdiğinden, elektrikli arabalar için seviyeyi varsayılan olarak 100 olarak ayarlayabiliriz. .

Swift'deki uzantıların kullanışlı olabileceği yer burasıdır:

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

Yani artık ürettiğimiz her yeni elektrikli araba, onu yeniden şarj ettiğimizde pili 100'e ayarlayacak. Böylece sınıfları, yapıları ve numaralandırmaları benzersiz ve varsayılan davranışlarla dekore edebildik.

Protokol Çizgi Romanı

Çizgi roman için Ray Wenderlich'e teşekkürler!

Ancak dikkat edilmesi gereken bir husus şudur. İlk uygulamamızda foo() işlevini A üzerinde varsayılan uygulama olarak tanımlıyoruz, ancak bunu protokolde gerekli kılmıyoruz. Yani a.foo() öğesini çağırdığımızda " A default " çıktısını alırız.

protocol Default {}

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

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

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

Ancak, A foo() gerekli kılarsak " Inst " elde ederiz:

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

Bu, Swift'deki protokollerde ilk örnekteki statik gönderim ile ikinci örnekteki statik gönderim arasındaki farktan kaynaklanmaktadır. Daha fazla bilgi için bu Medium gönderisine bakın.

Varsayılan davranışı geçersiz kılma

Ancak istersek yine de varsayılan davranışı geçersiz kılabiliriz. Unutulmaması gereken önemli bir nokta, bunun dinamik gönderimi desteklememesidir .

Diyelim ki elektrikli bir arabanın daha eski bir versiyonuna sahibiz, dolayısıyla pil sağlığı %90'a düşürüldü:

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

Protokollerin standart kütüphane kullanımları

Artık Swift'deki protokollerin nasıl çalıştığına dair bir fikrimiz olduğuna göre, standart kütüphane protokollerini kullanmanın bazı tipik örneklerini inceleyelim.

Standart kitaplığı genişletin

Swift'de zaten mevcut olan türlere nasıl ek işlevsellik ekleyebileceğimizi görelim. Swift'deki türler yerleşik olmadığından ve yapılar olarak standart kitaplığın parçası olduğundan, bunu yapmak kolaydır.

Bir dizi öğe üzerinde ikili arama yapmayı deneyelim ve aynı zamanda dizinin sıralı olup olmadığını da kontrol edelim:

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

Bunu , "öğeleri zarar vermeden birden çok kez geçilebilen ve indekslenmiş bir alt simge tarafından erişilebilen bir diziyi" tanımlayan Collection protokolünü genişleterek yapıyoruz. Diziler köşeli parantez gösterimi kullanılarak indekslenebildiğinden, genişletmek istediğimiz protokol budur.

Benzer şekilde, bu yardımcı program fonksiyonunu yalnızca elemanları karşılaştırılabilen dizilere eklemek istiyoruz. where Element: Comparable sahip olmamızın nedeni budur.

where yan tümcesi Swift'in tür sisteminin bir parçasıdır ve bunu yakında ele alacağız, ancak kısaca yazdığımız uzantıya türün bir protokolü uygulamasını gerektirmek, iki türün bir protokol olmasını gerektirmek gibi ek gereksinimler eklememize olanak tanır. aynı veya bir sınıfın belirli bir üst sınıfa sahip olmasını gerektirmek.

Element , Collection uyumlu bir türdeki öğelerin ilişkili türüdür. Element , Sequence protokolünde tanımlanır, ancak Collection , Sequence öğesinden devralındığından, Element ile ilişkili türü devralır.

Comparable , " < , <= , >= ve > ilişkisel operatörleri kullanılarak karşılaştırılabilecek bir tür" tanımlayan bir protokoldür. . Sıralanmış bir Collection üzerinde ikili arama yaptığımız için, bunun elbette doğru olması gerekir, aksi takdirde ikili aramada sola mı yoksa sağa mı yineleneceğini/yineleneceğini bilmiyoruz.

Uygulamaya ilişkin bir yan not olarak, kullanılan index(_:offsetBy:) işlevi hakkında daha fazla bilgi için aşağıdaki belgelere bakın.

Jenerikler + protokoller = 💥

Generikler ve protokoller, mükerrer kodları önlemek için doğru kullanıldığında güçlü bir araç olabilir.

İlk olarak, Colab kitabının sonunda jenerikleri kısaca kapsayan başka bir eğitim olan A Swift Tour'a bakın.

Jenerikler hakkında genel bir fikriniz olduğunu varsayarak, bazı gelişmiş kullanımlara hızlıca göz atalım.

Tek bir türün, birden fazla protokole uygun bir tür gibi birden fazla gereksinimi olduğunda, emrinizde birkaç seçenek vardır:

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

Üst kısımda typealias kullanımına dikkat edin. Bu, programınıza mevcut türden adlandırılmış bir takma ad ekler. Bir tür takma adı bildirildikten sonra, programınızın her yerinde mevcut tür yerine takma ad kullanılabilir. Tür takma adları yeni türler oluşturmaz; yalnızca bir adın mevcut bir türe gönderme yapmasına izin verirler.

Şimdi protokolleri ve jenerikleri birlikte nasıl kullanabileceğimizi görelim.

Sattığımız herhangi bir dizüstü bilgisayarı mağazanın arkasında nasıl düzenleyeceğimizi belirlemek için aşağıdaki gereksinimleri karşılayan bir bilgisayar mağazası olduğumuzu hayal edelim:

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

Ancak raflarda ağırlık kısıtlamaları olduğundan Laptop kütleye göre gruplandırma yönünde yeni bir zorunluluğumuz var.

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

Peki ya Mass dışında bir şeye göre filtrelemek istersek?

Bir seçenek aşağıdakileri yapmaktır:

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

Mükemmel! Artık herhangi bir dizüstü bilgisayar kısıtlamasına göre filtreleme yapabiliyoruz. Ancak sadece Laptop filtreleyebiliyoruz.

Bir kutunun içindeki ve kütlesi olan her şeyi filtreleyebilmeye ne dersiniz? Belki bu dizüstü bilgisayar deposu farklı müşteri tabanına sahip sunucular için de kullanılacaktır:

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

Artık bir diziyi yalnızca belirli bir struct herhangi bir özelliğine göre değil, aynı zamanda bu özelliğe sahip olan herhangi bir yapıya da göre filtreleyebiliyoruz!

İyi API tasarımı için ipuçları

Bu bölüm WWDC 2019: Modern Swift API Tasarımı konuşmasından alınmıştır.

Artık protokollerin nasıl davrandığını anladığınıza göre, en iyisi protokolleri ne zaman kullanmanız gerektiğini gözden geçirmektir. Protokoller ne kadar güçlü olursa olsun, protokollere dalmak ve hemen protokollere başlamak her zaman en iyi fikir değildir.

  • Somut kullanım durumlarıyla başlayın:
    • Öncelikle kullanım senaryosunu somut türlerle keşfedin ve hangi kodu paylaşmak istediğinizi ve tekrarlandığını anlayın. Ardından jeneriklerle paylaşılan kodu hesaba katın. Yeni protokoller oluşturmak anlamına gelebilir. Genel kod ihtiyacını keşfedin.
  • Standart kitaplıkta tanımlanan mevcut protokollerden yeni protokoller oluşturmayı düşünün. Bunun iyi bir örneği için aşağıdaki Apple belgelerine bakın.
  • Genel bir protokol yerine genel bir tür tanımlamayı düşünün.

Örnek: özel bir vektör tipinin tanımlanması

Diyelim ki, yaptığımız bazı geometri uygulamalarında kullanmak üzere kayan nokta sayıları üzerinde 3 önemli vektör işlemini tanımlayan bir GeometricVector protokolü tanımlamak istiyoruz:

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

Diyelim ki SIMD protokolünün bize yardımcı olabileceği vektörün boyutlarını depolamak istiyoruz, böylece yeni türümüzün SIMD protokolünü hassaslaştırmasını sağlayacağız. SIMD vektörleri, vektör işlemlerini gerçekleştirmek için kullanıldığında çok hızlı olan sabit boyutlu vektörler olarak düşünülebilir:

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

Şimdi yukarıdaki işlemlerin varsayılan uygulamalarını tanımlayalım:

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

Daha sonra bu yetenekleri eklemek istediğimiz türlerin her birine bir uygunluk eklememiz gerekir:

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

Protokolün tanımlanması, ona varsayılan bir uygulama verilmesi ve ardından birden çok türe uygunluk eklenmesinden oluşan bu üç aşamalı süreç oldukça tekrarlayıcıdır.

Protokol gerekli miydi?

Hiçbir SIMD tipinin benzersiz uygulamalarının olmaması bir uyarı işaretidir. Yani bu durumda protokol bize aslında hiçbir şey vermiyor.

SIMD bir uzantısında tanımlama

3 operatörü SIMD protokolünün bir uzantısına yazarsak, bu sorunu daha kısa ve öz bir şekilde çözebilir:

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

Daha az kod satırı kullanarak, tüm varsayılan uygulamaları tüm SIMD türlerine ekledik.

Bazen bu tür hiyerarşisini yaratma isteğine kapılabilirsiniz, ancak bunun her zaman gerekli olmadığını unutmayın. Bu aynı zamanda derlenmiş programınızın ikili boyutunun daha küçük olacağı ve kodunuzun derlenmesinin daha hızlı olacağı anlamına da gelir.

Ancak bu genişletme yaklaşımı, eklemek istediğiniz birkaç yönteminiz olduğunda harikadır. Ancak daha büyük bir API tasarlarken ölçeklenebilirlik sorunuyla karşılaşırsınız.

A mı? Var mı?

Daha önce GeometricVector SIMD geliştireceğini söylemiştik. Ama bu bir olan-bir ilişki midir? Sorun, SIMD bir vektöre skaler 1 eklememize izin veren işlemleri tanımlamasıdır, ancak böyle bir işlemi geometri bağlamında tanımlamanın bir anlamı yoktur.

Dolayısıyla SIMD herhangi bir kayan nokta sayısını işleyebilecek yeni bir genel türe sararak bir sahip-a ilişkisi daha iyi olabilir:

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

O zaman dikkatli olabiliriz ve yalnızca geometri bağlamında anlamlı olan işlemleri tanımlayabiliriz:

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

Uygulamak istediğimiz ve öncekiyle hemen hemen aynı görünen önceki 3 operatörü elde etmek için genel uzantıları kullanmaya devam edebiliriz:

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

Genel olarak, üç işlemimizin davranışını basit bir yapı kullanarak bir türe göre hassaslaştırmayı başardık. Protokollerde, tüm SIMD vektörlerine tekrarlayan uyumluluklar yazma sorunuyla karşılaştık ve ayrıca Scalar + Vector gibi belirli operatörlerin kullanılabilir olmasını engelleyemedik (bu durumda bunu istemedik). Bu nedenle, protokollerin her şeyi kapsayan ve her şeyin sonu olan bir çözüm olmadığını unutmayın. Ancak bazen daha geleneksel çözümlerin daha güçlü olduğu ortaya çıkabilir.

Daha fazla protokol odaklı programlama kaynağı

Tartışılan konulara ilişkin ek kaynaklar şunlardır:

  • WWDC 2015: Swift'de Protokol Odaklı Programlama : Swift 2 kullanılarak sunuldu, dolayısıyla o zamandan beri çok şey değişti (örneğin sunumda kullandıkları protokollerin adı) ancak bu hala teori ve arkasındaki kullanımlar için iyi bir kaynak .
  • Swift 3'te Protokol Odaklı Programlamaya Giriş : Bu Swift 3'te yazılmıştır, dolayısıyla başarılı bir şekilde derlenmesi için kodun bir kısmının değiştirilmesi gerekebilir, ancak bu başka bir harika kaynaktır.
  • WWDC 2019: Modern Swift API Tasarımı : değer ve referans türleri arasındaki farkları ele alır; protokollerin API tasarımında en kötü seçim olabileceği durumlara ilişkin bir kullanım durumu (yukarıdaki "İyi API Tasarımı için İpuçları" bölümüyle aynı), önemli yol üyesi arama ve özellik sarmalayıcılar.
  • Jenerikler : Swift'in Swift 5 için jeneriklerle ilgili kendi belgeleri.