Pemrograman & 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 berbagi kode di antara beberapa komponen program.

Di Swift, ada berbagai cara untuk mengekspresikan warisan. Anda mungkin sudah familiar dengan salah satu cara tersebut, dari bahasa lain: pewarisan kelas. Namun, Swift punya cara lain: protokol.

Dalam tutorial ini, kita akan mengeksplorasi protokol - sebuah alternatif untuk subkelas yang memungkinkan Anda mencapai tujuan serupa melalui pengorbanan yang berbeda. Di Swift, protokol berisi banyak anggota abstrak. Kelas, struct, dan enum dapat menyesuaikan diri dengan banyak protokol dan hubungan kesesuaian dapat dibuat secara surut. Semua itu memungkinkan beberapa desain yang tidak mudah diekspresikan di Swift menggunakan subkelas. Kita akan membahas idiom yang mendukung penggunaan protokol (ekstensi dan batasan protokol), serta batasan protokol.

Tipe nilai Swift 💖!

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

Pertama, mari kita lihat kemiripan enum 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 kita tidak dapat mewarisi struct, namun 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

Terakhir, mari kita lihat bagaimana mereka melewati tipe nilai, tidak 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 gunakan protokol

Mari kita mulai dengan membuat protokol untuk berbagai mobil:

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

Dalam dunia berorientasi objek (tanpa pewarisan ganda), Anda mungkin telah membuat kelas abstrak Electric dan Gas lalu menggunakan pewarisan kelas untuk membuat keduanya mewarisi Car , dan kemudian menjadikan model mobil tertentu menjadi kelas dasar. Namun, di sini keduanya merupakan protokol yang benar-benar terpisah tanpa kopling! Hal ini membuat keseluruhan sistem lebih fleksibel dalam cara Anda mendesainnya.

Mari kita definisikan 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 baru yang sesuai dengan protokol Car dan Electric .

Sekarang mari kita definisikan 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)

Perluas protokol dengan perilaku default

Apa yang dapat Anda perhatikan dari contoh-contoh ini adalah bahwa kita mempunyai beberapa redundansi. Setiap kali kita mengisi ulang mobil listrik, kita perlu menyetel tingkat persentase baterai ke 100. Karena semua mobil listrik memiliki kapasitas maksimal 100%, namun mobil berbahan bakar bensin bervariasi antar kapasitas tangki bensin, kita dapat menetapkan level default ke 100 untuk mobil listrik .

Di sinilah ekstensi di Swift bisa berguna:

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

Jadi sekarang, setiap mobil listrik baru yang kami buat akan menghabiskan baterai hingga 100 saat kami mengisi ulangnya. Jadi, kita baru saja dapat mendekorasi 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 sebagai berikut. Dalam implementasi pertama kami, kami mendefinisikan foo() sebagai implementasi default pada A , tetapi tidak menjadikannya wajib dalam protokol. Jadi saat kita memanggil a.foo() , kita mendapatkan cetakan " A default ".

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

Hal ini terjadi karena perbedaan antara pengiriman statis pada contoh pertama dan pengiriman statis pada protokol kedua di Swift. Untuk info lebih lanjut, lihat postingan Medium ini.

Mengganti perilaku default

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

Katakanlah kita memiliki mobil listrik versi lama, sehingga kesehatan baterainya 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
    }
}

Perpustakaan standar menggunakan protokol

Sekarang kita sudah mempunyai gambaran bagaimana protokol di Swift bekerja, mari kita lihat beberapa contoh umum penggunaan protokol perpustakaan standar.

Perluas perpustakaan standar

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

Mari kita coba melakukan pencarian biner pada array elemen, sambil memastikan bahwa array sudah 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 elemennya dapat dilintasi beberapa kali, tanpa merusak, dan diakses oleh subskrip yang diindeks." Karena array dapat diindeks menggunakan notasi tanda kurung siku, inilah protokol yang ingin kami perluas.

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

Klausa where adalah bagian dari sistem tipe Swift, yang akan segera kita bahas, namun secara singkat memungkinkan kita menambahkan persyaratan tambahan pada ekstensi yang sedang kita tulis, seperti mengharuskan tipe untuk mengimplementasikan protokol, memerlukan dua tipe untuk menjadi sama, atau mengharuskan suatu kelas memiliki superkelas tertentu.

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

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

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

Generik + protokol = 💥

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

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

Dengan asumsi Anda memiliki gambaran umum tentang obat generik, mari kita lihat beberapa kegunaan lanjutannya.

Ketika satu tipe memiliki beberapa persyaratan seperti tipe 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 tipe yang sudah ada ke dalam program Anda. Setelah alias tipe dideklarasikan, nama alias tersebut dapat digunakan sebagai pengganti tipe yang sudah ada di mana pun dalam program Anda. Alias ​​tipe tidak membuat tipe baru; mereka hanya mengizinkan nama untuk merujuk ke tipe yang sudah ada.

Sekarang, mari kita lihat bagaimana kita dapat menggunakan protokol dan obat generik secara bersamaan.

Bayangkan kita adalah sebuah toko komputer dengan persyaratan berikut pada setiap laptop yang kita jual untuk menentukan bagaimana kita mengaturnya di bagian 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 berdasarkan massa 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 berdasarkan sesuatu selain Mass ?

Salah satu opsinya 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 kami dapat memfilter berdasarkan batasan laptop apa pun. Namun, kami hanya dapat memfilter Laptop .

Bagaimana dengan kemampuan memfilter apa pun yang ada di dalam kotak dan bermassa? 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)]

Kita sekarang dapat memfilter array tidak hanya berdasarkan properti apa pun dari struct tertentu, tetapi juga dapat memfilter struct mana pun yang memiliki properti tersebut!

Tips untuk desain API yang baik

Bagian ini diambil dari pembicaraan WWDC 2019: Modern Swift API Design .

Sekarang setelah Anda memahami bagaimana protokol berperilaku, yang terbaik adalah membahas kapan Anda harus menggunakan protokol. Meskipun protokol sangat kuat, tidak selalu merupakan ide terbaik untuk mendalami dan langsung memulai dengan protokol.

  • Mulailah dengan kasus penggunaan nyata:
    • Pertama-tama jelajahi kasus penggunaan dengan tipe konkret dan pahami kode apa yang ingin Anda bagikan dan temukan sedang diulang. Kemudian, faktorkan kode yang dibagikan dengan obat generik. Ini mungkin berarti membuat protokol baru. Temukan kebutuhan akan kode generik.
  • Pertimbangkan untuk membuat protokol baru dari protokol yang sudah ada yang ditentukan di perpustakaan standar. Lihat dokumentasi Apple berikut untuk contoh bagusnya.
  • Daripada menggunakan protokol generik, pertimbangkan untuk mendefinisikan tipe generik.

Contoh: mendefinisikan tipe vektor khusus

Katakanlah kita ingin mendefinisikan protokol GeometricVector pada bilangan 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 dibantu oleh protokol SIMD , jadi kita akan membuat tipe baru yang menyempurnakan protokol SIMD . Vektor SIMD dapat dianggap sebagai vektor berukuran 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
    }
}

Lalu kita perlu menambahkan kesesuaian pada masing-masing tipe yang ingin kita tambahkan kemampuan berikut:

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 dalam mendefinisikan protokol, memberinya implementasi default, dan kemudian menambahkan kesesuaian ke beberapa tipe cukup berulang.

Apakah protokol itu diperlukan?

Fakta bahwa tidak ada tipe SIMD yang memiliki implementasi unik merupakan tanda peringatan. Jadi dalam kasus ini, protokol tidak memberikan apa pun kepada kita.

Mendefinisikannya dalam perpanjangan SIMD

Jika kita menulis 3 operator dalam perpanjangan protokol SIMD , ini dapat menyelesaikan 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
    }
}

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

Terkadang Anda mungkin tergoda untuk membuat hierarki tipe ini, namun ingatlah bahwa ini tidak selalu diperlukan. Ini juga berarti ukuran biner dari program yang Anda kompilasi akan lebih kecil, dan kode Anda akan lebih cepat untuk dikompilasi.

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

Adalah? Mempunyai sebuah?

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

Jadi, mungkin hubungan yang memiliki hubungan akan lebih baik dengan membungkus SIMD dalam tipe generik baru yang dapat menangani nomor floating point apa pun:

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

Kita 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 terapkan yang tampilannya 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 telah mampu menyempurnakan perilaku ketiga operasi kami menjadi suatu tipe hanya dengan menggunakan sebuah struct. Dengan protokol, kami menghadapi masalah penulisan kesesuaian berulang ke semua vektor SIMD , dan juga tidak dapat mencegah tersedianya operator tertentu seperti Scalar + Vector (yang dalam hal ini tidak kami inginkan). Oleh karena itu, ingatlah bahwa protokol bukanlah solusi segalanya. Namun terkadang solusi yang lebih tradisional terbukti lebih ampuh.

Lebih banyak sumber daya pemrograman berorientasi protokol

Berikut adalah sumber tambahan mengenai topik yang dibahas:

  • WWDC 2015: Pemrograman Berorientasi Protokol di Swift : ini disajikan menggunakan Swift 2, jadi banyak yang berubah sejak saat itu (misalnya nama protokol yang mereka gunakan dalam presentasi) tetapi ini masih merupakan sumber yang bagus untuk teori dan kegunaan di baliknya .
  • Memperkenalkan Pemrograman Berorientasi Protokol di Swift 3 : ini ditulis di Swift 3, jadi beberapa kode mungkin perlu dimodifikasi agar kompilasi berhasil, tetapi ini adalah sumber daya hebat lainnya.
  • WWDC 2019: Desain API Swift Modern : membahas perbedaan antara nilai dan tipe referensi, 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 obat generik.