Halaman ini diterjemahkan oleh Cloud Translation API.
Switch to English

Pemrograman berorientasi protokol & amp; obat generik

Lihat di TensorFlow.org Jalankan di Google Colab Lihat sumber di GitHub

Tutorial ini akan membahas pemrograman berorientasi protokol, dan berbagai contoh bagaimana mereka dapat digunakan dengan obat generik dalam contoh sehari-hari.

Protokol

Warisan adalah cara ampuh untuk mengatur kode dalam bahasa pemrograman yang memungkinkan Anda untuk membagikan kode di antara beberapa komponen program.

Di Swift, ada berbagai cara untuk mengekspresikan warisan. Anda mungkin sudah terbiasa dengan salah satu cara itu, dari bahasa lain: warisan kelas. Namun, Swift memiliki cara lain: protokol.

Dalam tutorial ini, kami akan mengeksplorasi protokol - sebuah alternatif untuk subclassing yang memungkinkan Anda untuk mencapai tujuan yang sama melalui pengorbanan yang berbeda. Di Swift, protokol berisi banyak anggota abstrak. Kelas, struct dan enum dapat menyesuaikan dengan beberapa protokol dan hubungan kesesuaian dapat dibangun secara retroaktif. Semua itu memungkinkan beberapa desain yang tidak mudah diungkapkan dalam Swift menggunakan subclassing. Kami akan berjalan melalui idiom yang mendukung penggunaan protokol (ekstensi dan batasan protokol), serta batasan protokol.

Jenis nilai Swift 💖!

Selain kelas yang memiliki semantik referensi, Swift mendukung enum dan struct yang dilewati oleh nilai. Enums dan struct mendukung banyak fitur yang disediakan oleh kelas. Mari lihat!

Pertama, mari kita lihat bagaimana enum mirip dengan kelas:

 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

Sekarang, mari kita lihat struct. Perhatikan bahwa kami tidak dapat mewarisi struct, tetapi sebaliknya dapat menggunakan protokol:

 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

Akhirnya, mari kita lihat bagaimana mereka melewati tipe nilai seperti kelas:

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

Mari kita gunakan protokol

Mari kita mulai dengan membuat protokol untuk mobil yang berbeda:

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

Di dunia berorientasi objek (tanpa pewarisan berganda), Anda mungkin telah membuat kelas abstrak Electric dan Gas kemudian menggunakan pewarisan kelas untuk membuat keduanya mewarisi dari Car , dan kemudian menjadikan model mobil tertentu menjadi kelas dasar. Namun, di sini keduanya merupakan protokol yang sepenuhnya terpisah dengan zero coupling! Ini membuat keseluruhan sistem lebih fleksibel dalam cara Anda mendesainnya.

Mari kita mendefinisikan 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)
 

Ini menentukan struct TeslaModelS yang sesuai dengan protokol Car dan Electric .

Sekarang mari kita mendefinisikan mobil bertenaga gas:

 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)
 

Perpanjang protokol dengan perilaku default

Apa yang dapat Anda perhatikan dari contoh adalah bahwa kami memiliki beberapa redundansi. Setiap kali kita mengisi ulang mobil listrik, kita perlu mengatur tingkat persentase baterai ke 100. Karena semua mobil listrik memiliki kapasitas maksimal 100%, tetapi mobil gas bervariasi di antara kapasitas tangki bensin, kita dapat mengatur tingkat ke 100 untuk mobil listrik. .

Di sinilah ekstensi di Swift berguna:

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

Jadi sekarang, setiap mobil listrik baru yang kita buat akan mengatur baterai ke 100 ketika kita mengisi ulang. Dengan demikian, kami baru saja dapat menghiasi kelas, struct, dan enum dengan perilaku unik dan default.

Komik Protokol

Terima kasih kepada Ray Wenderlich untuk komiknya!

Namun, satu hal yang harus diperhatikan adalah berikut ini. Dalam implementasi pertama kami, kami mendefinisikan foo() sebagai implementasi default pada A , tetapi tidak membuatnya diperlukan dalam protokol. Jadi ketika kita memanggil a.foo() , kita mendapatkan " A default " yang dicetak.

 protocol Default {}

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

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

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

Namun, jika kita membuat foo() diperlukan pada A , kita mendapatkan " 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

Ini terjadi karena perbedaan antara pengiriman statis dalam contoh pertama dan pengiriman statis pada yang kedua pada protokol di Swift. Untuk info lebih lanjut, lihat posting Medium ini.

Mengganti perilaku default

Namun, jika kita mau, kita masih bisa mengesampingkan perilaku default. Satu hal penting yang perlu diperhatikan adalah bahwa ini tidak mendukung pengiriman dinamis .

Katakanlah kita memiliki mobil listrik versi lama, sehingga kesehatan aki telah berkurang hingga 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
    }
}
 

Pustaka standar menggunakan protokol

Sekarang kita memiliki ide bagaimana protokol dalam Swift bekerja, mari kita lihat beberapa contoh khas menggunakan protokol pustaka standar.

Perpanjang perpustakaan standar

Mari kita lihat bagaimana kita dapat menambahkan fungsionalitas tambahan ke tipe yang sudah ada di Swift. Karena tipe dalam Swift tidak terintegrasi, tetapi merupakan bagian dari pustaka standar sebagai struct, ini mudah dilakukan.

Mari kita coba dan lakukan pencarian biner pada array elemen, sembari memastikan juga untuk memeriksa bahwa array diurutkan:

 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

Kami melakukan ini dengan memperluas protokol Collection yang mendefinisikan "urutan yang elemen-elemennya dapat dilalui beberapa kali, tidak rusak, dan diakses oleh subscript yang diindeks." Karena array dapat diindeks menggunakan notasi tanda kurung siku, ini adalah protokol yang ingin kami perpanjang.

Demikian pula, kami hanya ingin menambahkan fungsi utilitas ini ke array yang elemennya dapat dibandingkan. Ini adalah alasan mengapa kita memiliki where Element: Comparable .

Klausa where adalah bagian dari sistem tipe Swift, yang akan kita bahas segera, tetapi singkatnya kita dapat menambahkan persyaratan tambahan untuk ekstensi yang kita tulis, seperti mengharuskan jenis untuk mengimplementasikan protokol, untuk memerlukan dua jenis sebagai sama, atau membutuhkan kelas untuk memiliki superclass tertentu.

Element adalah tipe elemen yang terkait dalam tipe Collection -conforming. Element didefinisikan dalam protokol Sequence , tetapi karena Collection mewarisi dari Sequence , ia mewarisi tipe Element terkait.

Comparable adalah protokol yang mendefinisikan "tipe yang dapat dibandingkan dengan menggunakan operator relasional < , <= , >= , dan > ." . Karena kita sedang melakukan pencarian biner pada Collection diurutkan, ini tentu saja harus benar atau kita tidak tahu apakah akan mengulang / mengulangi kiri atau kanan dalam pencarian biner.

Sebagai catatan tambahan tentang implementasi, untuk info lebih lanjut tentang fungsi index(_:offsetBy:) yang digunakan, lihat dokumentasi berikut.

Generik + protokol = 💥

Generik dan protokol bisa menjadi alat yang ampuh jika digunakan dengan benar untuk menghindari kode duplikat.

Pertama, lihat tutorial lain, A Swift Tour , yang secara singkat membahas obat generik di akhir buku Colab.

Dengan anggapan Anda memiliki gagasan umum tentang obat generik, mari kita lihat beberapa penggunaan tingkat lanjut.

Ketika satu jenis memiliki beberapa persyaratan seperti jenis yang sesuai dengan beberapa protokol, Anda memiliki beberapa opsi yang dapat Anda gunakan:

 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

Perhatikan penggunaan typealias di bagian atas. Ini menambahkan alias bernama dari jenis yang ada ke dalam program Anda. Setelah jenis alias dideklarasikan, nama alias dapat digunakan daripada jenis yang ada di mana-mana di program Anda. Ketik alias tidak membuat tipe baru; mereka hanya mengizinkan nama untuk merujuk ke jenis yang ada.

Sekarang, mari kita lihat bersama bagaimana kita bisa menggunakan protokol dan generik.

Bayangkan kita adalah toko komputer dengan persyaratan berikut pada laptop apa pun yang kita jual untuk menentukan bagaimana kita mengaturnya di belakang toko:

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

Namun, kami memiliki persyaratan baru untuk mengelompokkan Laptop kami secara massal karena rak memiliki batasan berat.

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

Namun, bagaimana jika kita ingin memfilter dengan sesuatu selain Mass ?

Salah satu opsi adalah melakukan hal berikut:

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

Luar biasa! Sekarang kita dapat memfilter berdasarkan batasan laptop apa pun. Namun, kami hanya dapat memfilter Laptop .

Bagaimana dengan bisa menyaring apa pun yang ada di dalam kotak dan memiliki massa? Mungkin gudang laptop ini juga akan digunakan untuk server yang memiliki basis pelanggan berbeda:

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

Kami sekarang dapat memfilter array tidak hanya dengan properti apa pun dari struct tertentu, tetapi juga dapat memfilter setiap struct yang memiliki properti itu!

Kiat untuk desain API yang baik

Bagian ini diambil dari WWDC 2019: Pembicaraan Desain Swift API Modern .

Sekarang setelah Anda memahami bagaimana protokol berperilaku, sebaiknya Anda membahas kapan Anda harus menggunakan protokol. Sekuat protokol bisa, itu tidak selalu ide terbaik untuk menyelam dan segera mulai dengan protokol.

  • Mulai dengan kasus penggunaan konkret:
    • Pertama-tama jelajahi use case dengan tipe konkret dan pahami kode apa yang ingin Anda bagikan dan temukan sedang diulang. Kemudian, faktor yang membagikan kode keluar dengan obat generik. Itu mungkin berarti membuat protokol baru. Temukan kebutuhan kode generik.
  • Pertimbangkan untuk membuat protokol baru dari protokol yang ada yang didefinisikan dalam perpustakaan standar. Lihat dokumentasi Apple berikut untuk contoh yang baik tentang ini.
  • Alih-alih protokol generik, pertimbangkan mendefinisikan tipe generik sebagai gantinya.

Contoh: mendefinisikan jenis vektor khusus

Katakanlah kita ingin mendefinisikan protokol GeometricVector pada angka floating-point untuk digunakan dalam beberapa aplikasi geometri yang kita buat yang mendefinisikan 3 operasi vektor penting:

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

Katakanlah kita ingin menyimpan dimensi vektor, yang dapat membantu protokol SIMD kita, jadi kita akan membuat tipe baru kita memperbaiki protokol SIMD . Vektor SIMD dapat dianggap sebagai vektor ukuran tetap yang sangat cepat ketika Anda menggunakannya untuk melakukan operasi vektor:

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

Sekarang, mari kita tentukan implementasi default dari operasi di atas:

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

Dan kemudian kita perlu menambahkan kesesuaian untuk setiap jenis yang ingin kita tambahkan kemampuan ini:

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

Proses tiga langkah mendefinisikan protokol, memberikannya implementasi default, dan kemudian menambahkan kesesuaian ke beberapa jenis cukup berulang.

Apakah protokol itu diperlukan?

Fakta bahwa tidak ada tipe SIMD memiliki implementasi unik adalah tanda peringatan. Jadi dalam hal ini, protokolnya tidak benar-benar memberi kita apa-apa.

Menentukannya dalam ekstensi SIMD

Jika kami menulis 3 operator dalam ekstensi protokol SIMD , ini dapat memecahkan masalah dengan lebih ringkas:

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

Menggunakan lebih sedikit baris kode, kami menambahkan semua implementasi default ke semua jenis SIMD .

Terkadang Anda mungkin tergoda untuk membuat hierarki jenis ini, tetapi ingat bahwa itu tidak selalu perlu. Ini juga berarti ukuran biner dari program kompilasi Anda akan lebih kecil, dan kode Anda akan lebih cepat dikompilasi.

Namun, pendekatan ekstensi ini sangat bagus ketika Anda memiliki beberapa metode yang ingin Anda tambahkan. Namun, itu mengenai masalah skalabilitas saat Anda merancang API yang lebih besar.

Adalah? Mempunyai sebuah?

Sebelumnya kami mengatakan GeometricVector akan memperbaiki SIMD . Tapi apakah ini hubungan is-a? Masalahnya adalah bahwa SIMD mendefinisikan operasi yang memungkinkan kita menambahkan skalar 1 ke vektor, tetapi tidak masuk akal untuk mendefinisikan operasi seperti itu dalam konteks geometri.

Jadi, mungkin hubungan has-a akan lebih baik dengan membungkus SIMD dalam tipe generik baru yang dapat menangani nomor floating point:

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

Kami kemudian dapat berhati-hati dan hanya mendefinisikan operasi yang masuk akal hanya dalam konteks geometri:

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

Dan kita masih dapat menggunakan ekstensi generik untuk mendapatkan 3 operator sebelumnya yang ingin kita implementasikan yang terlihat hampir sama persis seperti sebelumnya:

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

Secara keseluruhan, kami dapat memperbaiki perilaku dari tiga operasi kami menjadi tipe hanya dengan menggunakan struct. Dengan protokol, kami menghadapi masalah penulisan kesesuaian berulang untuk semua vektor SIMD , dan juga tidak dapat mencegah operator tertentu seperti Scalar + Vector agar tidak tersedia (yang dalam hal ini kami tidak inginkan). Karena itu, ingatlah bahwa protokol bukanlah solusi semua-dan-semua-akhir. Tetapi kadang-kadang solusi yang lebih tradisional terbukti lebih kuat.

Lebih banyak sumber daya pemrograman berorientasi protokol

Berikut adalah sumber daya tambahan tentang topik yang dibahas:

  • WWDC 2015: Pemrograman Berorientasi Protokol di Swift : ini disajikan menggunakan Swift 2, jadi banyak yang telah berubah sejak saat itu (misalnya nama protokol yang mereka gunakan dalam presentasi) tetapi ini masih merupakan sumber yang baik untuk teori dan penggunaan di belakangnya .
  • Memperkenalkan Pemrograman Berorientasi Protokol di Swift 3 : ini ditulis dalam Swift 3, jadi beberapa kode mungkin perlu dimodifikasi untuk mengkompilasinya dengan sukses, tetapi ini merupakan sumber hebat lainnya.
  • WWDC 2019: Desain API Swift Modern : membahas perbedaan antara nilai dan tipe referensi, sebuah kasus penggunaan ketika protokol terbukti menjadi pilihan yang lebih buruk dalam desain API (sama seperti bagian "Tips untuk Desain API yang Baik" di atas), kunci pencarian anggota jalur, dan pembungkus properti.
  • Generik : Dokumentasi Swift sendiri untuk Swift 5 semuanya tentang generik.