RSVP untuk acara TensorFlow Everywhere lokal Anda hari ini!
Halaman ini diterjemahkan oleh Cloud Translation API.
Switch to English

Pemrograman berorientasi protokol & 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 yang ampuh untuk mengatur kode dalam bahasa pemrograman yang memungkinkan Anda untuk berbagi kode di antara banyak komponen program.

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

Dalam tutorial ini, kita akan menjelajahi protokol - alternatif untuk subclass yang memungkinkan Anda mencapai tujuan serupa 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 diekspresikan di Swift menggunakan subclassing. Kami akan membahas 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 diteruskan 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 kita tidak dapat mewarisi struct, melainkan 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 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 }
}
.dll

Dalam dunia berorientasi objek (tanpa multiple inheritance), Anda mungkin telah membuat class abstrak Electric dan Gas kemudian menggunakan class inheritance untuk membuat keduanya mewarisi dari Car , dan kemudian menjadikan model mobil tertentu sebagai class dasar. Namun, di sini keduanya adalah protokol yang benar-benar terpisah dengan zero coupling! 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 yang sesuai dengan protokol Car dan Electric .

Sekarang mari kita tentukan 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 adalah bahwa kami memiliki 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%, tetapi mobil bensin bervariasi antara 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 menyetel baterainya menjadi 100 saat kami mengisi ulang. Jadi, kami baru saja dapat menghias kelas, struct, dan enum dengan perilaku unik dan default.

Protokol Komik

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 di A , tetapi tidak membuatnya diperlukan dalam protokol. Jadi ketika kita memanggil a.foo() , kita mendapatkan " A default " tercetak.

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 di 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 contoh kedua pada protokol di Swift. Untuk info lebih lanjut, lihat posting Medium ini.

Mengganti perilaku default

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

Katakanlah kita memiliki mobil listrik versi lama, jadi kesehatan baterai 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 setelah kita mengetahui cara kerja protokol di Swift, mari kita lihat beberapa contoh umum penggunaan protokol pustaka 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 dibangun di dalamnya, tetapi merupakan bagian dari pustaka standar sebagai struct, ini mudah dilakukan.

Mari kita coba dan lakukan pencarian biner pada larik elemen, sambil juga memastikan untuk memeriksa bahwa larik 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 protokolCollection yang mendefinisikan "urutan yang elemennya dapat dilalui beberapa kali, tidak merusak, dan diakses oleh subskrip 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. Inilah alasan mengapa kami memiliki where Element: Comparable .

Klausa where adalah bagian dari sistem tipe Swift, yang akan segera kita bahas, tetapi singkatnya mari kita tambahkan persyaratan tambahan ke ekstensi yang kita tulis, seperti memerlukan tipe untuk mengimplementasikan protokol, untuk meminta dua tipe menjadi sama, atau membutuhkan kelas untuk memiliki superclass tertentu.

Element adalah tipe terkait dari elemen dalam tipe Collection -conforming. 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 diurutkan, ini tentu saja harus benar atau kita tidak tahu apakah akan mengulang / mengulang ke kiri atau ke 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 asumsi Anda memiliki gambaran umum tentang obat generik, mari kita lihat dengan cepat beberapa penggunaan lanjutan.

Jika 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 atas. Ini menambahkan alias bernama dari tipe yang ada ke dalam program Anda. Setelah alias tipe dideklarasikan, nama alias bisa digunakan sebagai pengganti tipe yang ada di mana pun di program Anda. Alias ​​tipe tidak membuat tipe baru; mereka hanya mengizinkan sebuah nama untuk merujuk ke tipe yang ada.

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

Bayangkan kita adalah toko komputer dengan persyaratan berikut pada laptop apa pun 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 dengan 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)]

Hebat! Sekarang kami dapat memfilter berdasarkan kendala laptop apa pun. Namun, kami hanya dapat memfilter Laptop .

Bagaimana dengan kemampuan menyaring apapun 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)]

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

Tips untuk desain API yang bagus

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

Sekarang setelah Anda memahami bagaimana protokol berperilaku, yang terbaik adalah membahas kapan Anda harus menggunakan protokol. Sekuat protokol, tidak selalu merupakan ide terbaik untuk menyelami dan segera memulai dengan protokol.

  • Mulailah dengan kasus penggunaan konkret:
    • Pertama-tama jelajahi kasus penggunaan dengan tipe konkret dan pahami kode apa yang ingin Anda bagikan dan temukan yang 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 ada yang ditentukan di pustaka standar. Lihat dokumentasi Apple berikut untuk contoh yang baik tentang ini.
  • Alih-alih protokol generik, pertimbangkan untuk mendefinisikan tipe generik.

Contoh: menentukan jenis 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 kita menyempurnakan 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 ke masing-masing 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 untuk mendefinisikan protokol, memberinya implementasi default, dan kemudian menambahkan kesesuaian ke beberapa jenis cukup berulang.

Apakah protokolnya diperlukan?

Fakta bahwa tidak ada jenis SIMD memiliki implementasi unik adalah tanda peringatan. Jadi dalam kasus ini, protokolnya tidak benar-benar memberi kita apapun.

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

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

Terkadang Anda mungkin tergoda untuk membuat hierarki jenis ini, tetapi ingatlah bahwa itu 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, itu mengenai masalah skalabilitas saat Anda mendesain API yang lebih besar.

Adalah? Mempunyai sebuah?

Sebelumnya kami mengatakan GeometricVector akan menyempurnakan SIMD . Tapi apakah ini hubungan is-a? Masalahnya adalah SIMD mendefinisikan operasi yang memungkinkan kita menambahkan skalar 1 ke vektor, tetapi tidak masuk akal untuk mendefinisikan operasi semacam 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 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 }
}

Kami kemudian dapat berhati-hati dan hanya menentukan 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 bisa menggunakan ekstensi generik untuk mendapatkan 3 operator sebelumnya yang ingin kita terapkan 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 menyempurnakan perilaku tiga operasi kami menjadi satu tipe hanya dengan menggunakan struct. Dengan protokol, kami menghadapi masalah penulisan kesesuaian berulang ke semua vektor SIMD , dan juga tidak dapat mencegah operator tertentu seperti Scalar + Vector tersedia (yang dalam hal ini tidak kami inginkan). Karena itu, ingatlah bahwa protokol bukanlah solusi segalanya dan solusi akhir segalanya. Namun terkadang solusi yang lebih tradisional terbukti lebih efektif.

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 di Swift 3, jadi beberapa kode mungkin perlu dimodifikasi agar berhasil dikompilasi, tetapi ini adalah sumber daya hebat lainnya.
  • WWDC 2019: Desain Swift API Modern : membahas perbedaan antara nilai dan jenis 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.