หน้านี้ได้รับการแปลโดย Cloud Translation API
Switch to English

การเขียนโปรแกรมและข้อมูลทั่วไปเกี่ยวกับโปรโตคอล

ดูใน TensorFlow.org เรียกใช้ใน Google Colab ดูแหล่งที่มาบน GitHub

บทช่วยสอนนี้จะกล่าวถึงการเขียนโปรแกรมที่เน้นโปรโตคอลและตัวอย่างต่างๆของวิธีการใช้งานกับ generics ในตัวอย่างแบบวันต่อวัน

โปรโตคอล

การสืบทอดเป็นวิธีที่มีประสิทธิภาพในการจัดระเบียบโค้ดในภาษาโปรแกรมที่ช่วยให้คุณสามารถแชร์โค้ดระหว่างส่วนประกอบต่างๆของโปรแกรมได้

ใน Swift มีหลายวิธีในการแสดงการถ่ายทอดทางพันธุกรรม คุณอาจคุ้นเคยกับวิธีใดวิธีหนึ่งจากภาษาอื่นอยู่แล้วนั่นคือการสืบทอดคลาส อย่างไรก็ตาม Swift มีอีกวิธีหนึ่งคือโปรโตคอล

ในบทช่วยสอนนี้เราจะสำรวจโปรโตคอลซึ่งเป็นอีกทางเลือกหนึ่งของคลาสย่อยที่ช่วยให้คุณบรรลุเป้าหมายที่คล้ายกันผ่านการแลกเปลี่ยนที่แตกต่างกัน ใน Swift โปรโตคอลประกอบด้วยสมาชิกนามธรรมหลายคน คลาสโครงสร้างและ enums สามารถเป็นไปตามโปรโตคอลหลายแบบและสามารถสร้างความสัมพันธ์แบบย้อนหลังได้ ทั้งหมดนี้ช่วยให้การออกแบบบางอย่างไม่สามารถแสดงออกได้อย่างง่ายดายใน Swift โดยใช้คลาสย่อย เราจะอธิบายสำนวนที่สนับสนุนการใช้โปรโตคอล (ส่วนขยายและข้อ จำกัด ของโปรโตคอล) ตลอดจนข้อ จำกัด ของโปรโตคอล

ประเภทมูลค่าของ Swift 💖!

นอกเหนือจากคลาสที่มีความหมายอ้างอิงแล้ว Swift ยังสนับสนุน enums และโครงสร้างที่ส่งผ่านด้วยค่า Enums และโครงสร้างสนับสนุนคุณสมบัติมากมายที่จัดเตรียมโดยคลาส มาดูกัน!

ประการแรกมาดูกันว่า enums มีความคล้ายคลึงกับคลาสอย่างไร:

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

ตอนนี้เรามาดูโครงสร้าง สังเกตว่าเราไม่สามารถสืบทอดโครงสร้างได้ แต่สามารถใช้โปรโตคอลแทนได้:

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

สุดท้ายเรามาดูกันว่าพวกมันถูกส่งผ่านประเภทค่าต่างจากคลาสอย่างไร:

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

มาใช้โปรโตคอลกันเถอะ

เริ่มต้นด้วยการสร้างโปรโตคอลสำหรับรถยนต์ประเภทต่างๆ:

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

ในโลกเชิงวัตถุ (โดยไม่มีการสืบทอดหลายรายการ) คุณอาจสร้างคลาสนามธรรม Electric และ Gas จากนั้นใช้การสืบทอดคลาสเพื่อทำให้ทั้งสองได้รับมรดกจาก Car จากนั้นให้โมเดลรถเฉพาะเป็นคลาสพื้นฐาน อย่างไรก็ตามทั้งสองอย่างนี้เป็นโปรโตคอลที่แยกจากกันโดยสิ้นเชิงโดย ไม่มีการ เชื่อมต่อ! ทำให้ระบบทั้งหมดมีความยืดหยุ่นในการออกแบบ

มากำหนด 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)

สิ่งนี้ระบุโครงสร้าง TeslaModelS ใหม่ที่สอดคล้องกับโปรโตคอลทั้ง Car และ Electric

ตอนนี้เรามากำหนดรถที่ใช้แก๊ส:

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)

ขยายโปรโตคอลที่มีพฤติกรรมเริ่มต้น

สิ่งที่คุณสังเกตได้จากตัวอย่างคือเรามีความซ้ำซ้อน ทุกครั้งที่เราชาร์จรถยนต์ไฟฟ้าเราต้องตั้งค่าระดับเปอร์เซ็นต์แบตเตอรี่เป็น 100 เนื่องจากรถยนต์ไฟฟ้าทุกคันมีความจุสูงสุด 100% แต่รถยนต์ที่ใช้ก๊าซจะแตกต่างกันไปตามความจุถังก๊าซเราจึงสามารถตั้งค่าระดับเป็น 100 สำหรับรถยนต์ไฟฟ้าได้ .

นี่คือสิ่งที่ส่วนขยายใน Swift มีประโยชน์:

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

ตอนนี้รถยนต์ไฟฟ้าใหม่ที่เราสร้างขึ้นจะตั้งค่าแบตเตอรี่เป็น 100 เมื่อเราชาร์จใหม่ ดังนั้นเราจึงสามารถตกแต่งคลาสโครงสร้างและ enums ด้วยพฤติกรรมที่เป็นเอกลักษณ์และเป็นค่าเริ่มต้นได้

การ์ตูนโปรโตคอล

ขอบคุณ Ray Wenderlich สำหรับการ์ตูนเรื่องนี้!

อย่างไรก็ตามสิ่งหนึ่งที่ต้องระวังคือสิ่งต่อไปนี้ ในการใช้งานครั้งแรกเรากำหนดให้ foo() เป็นการใช้งานเริ่มต้นบน A แต่ไม่จำเป็นต้องใช้ในโปรโตคอล ดังนั้นเมื่อเราเรียก a.foo() เราจะพิมพ์ " 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

อย่างไรก็ตามหากเราสร้าง foo() จำเป็นใน A เราจะได้รับ " 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

สิ่งนี้เกิดขึ้นเนื่องจากความแตกต่างระหว่างการจัดส่งแบบคงที่ในตัวอย่างแรกและการจัดส่งแบบคงที่ในวินาทีบนโปรโตคอลใน Swift สำหรับข้อมูลเพิ่มเติมโปรดดู โพสต์ขนาดกลาง นี้

การลบล้างพฤติกรรมเริ่มต้น

อย่างไรก็ตามหากเราต้องการเรายังคงสามารถลบล้างพฤติกรรมเริ่มต้นได้ สิ่งสำคัญอย่างหนึ่งที่ควรทราบก็คือสิ่งนี้ ไม่รองรับการจัดส่งแบบไดนามิก

สมมติว่าเรามีรถยนต์ไฟฟ้ารุ่นเก่าดังนั้นความสมบูรณ์ของแบตเตอรี่จึงลดลงเหลือ 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
    }
}

ไลบรารีมาตรฐานใช้โปรโตคอล

ตอนนี้เรามีความคิดว่าโปรโตคอลใน Swift ทำงานอย่างไรเรามาดูตัวอย่างทั่วไปของการใช้โปรโตคอลไลบรารีมาตรฐาน

ขยายไลบรารีมาตรฐาน

มาดูกันว่าเราสามารถเพิ่มฟังก์ชันเพิ่มเติมให้กับประเภทที่มีอยู่ใน Swift ได้อย่างไร เนื่องจากประเภทใน Swift ไม่ได้อยู่ในตัว แต่เป็นส่วนหนึ่งของไลบรารีมาตรฐานเป็นโครงสร้างจึงทำได้ง่าย

มาลองค้นหาไบนารีกับอาร์เรย์ขององค์ประกอบในขณะเดียวกันก็ตรวจสอบให้แน่ใจว่าเรียงลำดับอาร์เรย์แล้ว:

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

เราทำสิ่งนี้โดยการขยายโปรโตคอลการ Collection ซึ่งกำหนด "ลำดับที่องค์ประกอบสามารถข้ามผ่านได้หลายครั้งไม่ทำลายและเข้าถึงได้โดยตัวห้อยที่จัดทำดัชนี" เนื่องจากอาร์เรย์สามารถสร้างดัชนีได้โดยใช้สัญลักษณ์วงเล็บเหลี่ยมนี่คือโปรโตคอลที่เราต้องการขยาย

ในทำนองเดียวกันเราต้องการเพิ่มฟังก์ชันยูทิลิตี้นี้ในอาร์เรย์ที่สามารถเปรียบเทียบองค์ประกอบได้เท่านั้น นี่คือเหตุผลว่าทำไมเราถึงมี where Element: Comparable

โดย where ประโยคเป็นส่วนหนึ่งของระบบประเภทของ Swift ซึ่งเราจะกล่าวถึงในเร็ว ๆ นี้ แต่ในระยะสั้นช่วยให้เราสามารถเพิ่มข้อกำหนดเพิ่มเติมให้กับส่วนขยายที่เรากำลังเขียนเช่นกำหนดประเภทเพื่อใช้โปรโตคอลเพื่อกำหนดให้สองประเภทคือ เดียวกันหรือต้องการให้คลาสมีซูเปอร์คลาสเฉพาะ

Element คือประเภทที่เกี่ยวข้องขององค์ประกอบในประเภทการกำหนดรูปแบบ Collection Element ถูกกำหนดภายในโปรโตคอล Sequence แต่เนื่องจาก Collection สืบทอดมาจาก Sequence จึงสืบทอดประเภทที่เกี่ยวข้องกับ Element

Comparable คือโปรโตคอลที่กำหนด "ประเภทที่สามารถเปรียบเทียบได้โดยใช้ตัวดำเนินการเชิงสัมพันธ์ < , <= , >= และ > " . เนื่องจากเราทำการค้นหาไบนารีใน Collection เรียงลำดับแน่นอนว่านี่จะต้องเป็นจริงไม่เช่นนั้นเราไม่รู้ว่าจะเรียกคืน / วนซ้ำทางซ้ายหรือขวาในการค้นหาแบบไบนารี

สำหรับข้อมูลเพิ่มเติมเกี่ยวกับฟังก์ชัน index(_:offsetBy:) ที่ใช้โปรดดู เอกสารประกอบ ต่อไปนี้

Generics + โปรโตคอล = 💥

Generics และโปรโตคอลอาจเป็นเครื่องมือที่มีประสิทธิภาพหากใช้อย่างถูกต้องเพื่อหลีกเลี่ยงรหัสซ้ำ

ประการแรกดูบทช่วยสอนอื่น A Swift Tour ซึ่งครอบคลุมสั้น ๆ เกี่ยวกับชื่อสามัญในตอนท้ายของหนังสือ Colab

สมมติว่าคุณมีความคิดทั่วไปเกี่ยวกับยาชื่อสามัญมาดูการใช้งานขั้นสูงกันอย่างรวดเร็ว

เมื่อประเภทเดียวมีข้อกำหนดหลายประการเช่นประเภทที่สอดคล้องกับโปรโตคอลหลายแบบคุณมีตัวเลือกมากมายให้คุณเลือกใช้:

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

สังเกตการใช้ตัว typealias ที่ด้านบน สิ่งนี้จะเพิ่มนามแฝงของประเภทที่มีอยู่ลงในโปรแกรมของคุณ หลังจากประกาศประเภทนามแฝงแล้วสามารถใช้ชื่อนามแฝงแทนประเภทที่มีอยู่ได้ทุกที่ในโปรแกรมของคุณ นามแฝงประเภทไม่สร้างประเภทใหม่ พวกเขาเพียงแค่อนุญาตให้ชื่ออ้างถึงประเภทที่มีอยู่

ตอนนี้เรามาดูกันว่าเราจะใช้โปรโตคอลและยาสามัญร่วมกันได้อย่างไร

สมมติว่าเราเป็นร้านขายคอมพิวเตอร์ที่มีข้อกำหนดต่อไปนี้สำหรับแล็ปท็อปทุกเครื่องที่เราขายเพื่อพิจารณาว่าเราจัดระเบียบอย่างไรที่ด้านหลังของร้าน:

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

อย่างไรก็ตามเรามีข้อกำหนดใหม่ในการจัดกลุ่ม Laptop ของเราตามจำนวนมากเนื่องจากชั้นวางมีข้อ จำกัด ด้านน้ำหนัก

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

อย่างไรก็ตามถ้าเราต้องการกรองสิ่งอื่นที่ไม่ใช่ Mass ล่ะ?

ทางเลือกหนึ่งคือดำเนินการดังต่อไปนี้:

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

! น่ากลัว ตอนนี้เราสามารถกรองตามข้อ จำกัด ของแล็ปท็อปได้แล้ว อย่างไรก็ตามเราสามารถกรอง Laptop ได้เท่านั้น

แล้วความสามารถในการกรองสิ่งที่อยู่ในกล่องและมีมวลล่ะ? บางทีคลังสินค้าของแล็ปท็อปนี้อาจถูกใช้สำหรับเซิร์ฟเวอร์ที่มีฐานลูกค้าต่างกัน:

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

เราได้รับในขณะนี้สามารถกรองอาร์เรย์โดยไม่เพียง แต่ทรัพย์สินของที่เฉพาะเจาะจงใด ๆ struct แต่ยังสามารถกรอง struct ใด ๆ ซึ่งมีคุณสมบัติที่!

เคล็ดลับสำหรับการออกแบบ API ที่ดี

ส่วนนี้นำมาจาก การ พูดคุยเกี่ยวกับ WWDC 2019: Modern Swift API Design

ตอนนี้คุณเข้าใจแล้วว่าโปรโตคอลทำงานอย่างไรจึงเป็นการดีที่สุดเมื่อคุณควรใช้โปรโตคอล มีประสิทธิภาพพอ ๆ กับโปรโตคอลไม่ใช่ความคิดที่ดีที่สุดเสมอไปในการดำน้ำและเริ่มต้นด้วยโปรโตคอลในทันที

  • เริ่มต้นด้วยกรณีการใช้งานคอนกรีต:
    • ก่อนอื่นให้สำรวจกรณีการใช้งานกับประเภทคอนกรีตและทำความเข้าใจว่าคุณต้องการแชร์รหัสใดและพบว่ามีการทำซ้ำ จากนั้นแยกส่วนที่แชร์โค้ดออกด้วยข้อมูลทั่วไป อาจหมายถึงการสร้างโปรโตคอลใหม่ ค้นพบความต้องการรหัสทั่วไป
  • พิจารณาสร้างโปรโตคอลใหม่จากโปรโตคอลที่มีอยู่ซึ่งกำหนดไว้ในไลบรารีมาตรฐาน ดู เอกสารของ Apple ต่อไปนี้สำหรับตัวอย่างที่ดี
  • แทนที่จะเป็นโปรโตคอลทั่วไปให้พิจารณากำหนดประเภททั่วไปแทน

ตัวอย่าง: การกำหนดประเภทเวกเตอร์ที่กำหนดเอง

สมมติว่าเราต้องการกำหนดโปรโตคอล GeometricVector บนตัวเลขทศนิยมเพื่อใช้ในแอพเรขาคณิตบางตัวที่เรากำลังสร้างขึ้นซึ่งกำหนดการดำเนินการเวกเตอร์ที่สำคัญ 3 อย่าง:

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

สมมติว่าเราต้องการจัดเก็บขนาดของเวกเตอร์ซึ่งโปรโตคอล SIMD สามารถช่วยเราได้ดังนั้นเราจะสร้างประเภทใหม่ของเราเพื่อปรับแต่งโปรโตคอล SIMD เวกเตอร์ SIMD สามารถคิดว่าเป็นเวกเตอร์ขนาดคงที่ซึ่งเร็วมากเมื่อคุณใช้เพื่อดำเนินการเวกเตอร์:

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

ตอนนี้ให้เรากำหนดการใช้งานเริ่มต้นของการดำเนินการด้านบน:

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

จากนั้นเราต้องเพิ่มความสอดคล้องให้กับแต่ละประเภทที่เราต้องการเพิ่มความสามารถเหล่านี้:

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

กระบวนการสามขั้นตอนในการกำหนดโปรโตคอลโดยให้การใช้งานเริ่มต้นจากนั้นการเพิ่มความสอดคล้องกับหลายประเภทนั้นค่อนข้างซ้ำซาก

โปรโตคอลจำเป็นหรือไม่?

ความจริงที่ว่าไม่มี SIMD ประเภทใดที่มีการใช้งานที่ไม่ซ้ำกันถือเป็นสัญญาณเตือน ดังนั้นในกรณีนี้โปรโตคอลไม่ได้ให้อะไรเราเลย

กำหนดไว้ในส่วนขยายของ SIMD

หากเราเขียนตัวดำเนินการ 3 ตัวในส่วนขยายของโปรโตคอล SIMD สิ่งนี้จะช่วยแก้ปัญหาได้อย่างรวบรัดมากขึ้น:

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

การใช้โค้ดน้อยลงเราได้เพิ่มการใช้งานเริ่มต้นทั้งหมดให้กับ SIMD ทุกประเภท

บางครั้งคุณอาจอยากสร้างลำดับชั้นประเภทนี้ แต่อย่าลืมว่าไม่จำเป็นเสมอไป นอกจากนี้ยังหมายความว่าขนาดไบนารีของโปรแกรมที่คอมไพล์ของคุณจะเล็กลงและโค้ดของคุณจะรวบรวมได้เร็วขึ้น

อย่างไรก็ตามแนวทางการขยายนี้เหมาะสำหรับเมื่อคุณมีวิธีการหลายอย่างที่คุณต้องการเพิ่ม อย่างไรก็ตามมันประสบปัญหาเรื่องความสามารถในการปรับขนาดเมื่อคุณออกแบบ API ที่ใหญ่ขึ้น

คือ? มี?

ก่อนหน้านี้เรากล่าวว่า GeometricVector จะปรับแต่ง SIMD แต่นี่เป็นความสัมพันธ์หรือไม่? ปัญหาคือ SIMD กำหนดการดำเนินการซึ่งทำให้เราสามารถเพิ่มสเกลาร์ 1 ลงในเวกเตอร์ได้ แต่มันไม่สมเหตุสมผลที่จะกำหนดการดำเนินการดังกล่าวในบริบทของเรขาคณิต

ดังนั้นความสัมพันธ์แบบ has-a อาจจะดีกว่าโดยการรวม SIMD ในประเภททั่วไปใหม่ที่สามารถจัดการกับตัวเลขทศนิยมใดก็ได้:

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

จากนั้นเราสามารถระมัดระวังและกำหนดการดำเนินการที่เหมาะสมเฉพาะในบริบทของรูปทรงเรขาคณิตเท่านั้น:

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

และเรายังสามารถใช้ส่วนขยายทั่วไปเพื่อรับ 3 ตัวดำเนินการก่อนหน้านี้ที่เราต้องการนำไปใช้ซึ่งมีลักษณะเกือบจะเหมือนเดิม:

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

โดยรวมแล้วเราสามารถปรับแต่งพฤติกรรมของการดำเนินการทั้งสามของเราให้เป็นประเภทหนึ่งได้โดยใช้โครงสร้าง ด้วยโปรโตคอลเราประสบปัญหาในการเขียนความสอดคล้องซ้ำ ๆ กับเวกเตอร์ SIMD ทั้งหมดและยังไม่สามารถป้องกันไม่ให้ตัวดำเนินการบางอย่างเช่น Scalar + Vector พร้อมใช้งาน (ซึ่งในกรณีนี้เราไม่ต้องการ) ด้วยเหตุนี้โปรดจำไว้ว่าโปรโตคอลไม่ใช่วิธีแก้ปัญหาทั้งหมดและสิ้นสุด แต่บางครั้งวิธีแก้ปัญหาแบบเดิม ๆ ก็สามารถพิสูจน์ได้ว่ามีประสิทธิภาพมากกว่า

ทรัพยากรการเขียนโปรแกรมที่เน้นโปรโตคอลเพิ่มเติม

นี่คือแหล่งข้อมูลเพิ่มเติมเกี่ยวกับหัวข้อที่กล่าวถึง:

  • WWDC 2015: Protocol-Oriented Programming ใน Swift : สิ่งนี้นำเสนอโดยใช้ Swift 2 ดังนั้นจึงมีการเปลี่ยนแปลงมากมายตั้งแต่นั้นมา (เช่นชื่อของโปรโตคอลที่ใช้ในการนำเสนอ) แต่นี่ก็ยังเป็นแหล่งข้อมูลที่ดีสำหรับทฤษฎีและใช้เบื้องหลัง .
  • ขอแนะนำ Protocol-Oriented Programming ใน Swift 3 : สิ่งนี้ถูกเขียนขึ้นใน Swift 3 ดังนั้นโค้ดบางส่วนอาจต้องได้รับการแก้ไขเพื่อให้คอมไพล์สำเร็จ แต่ก็เป็นอีกแหล่งข้อมูลที่ยอดเยี่ยม
  • WWDC 2019: Modern Swift API Design : กล่าวถึงความแตกต่างระหว่างค่าและประเภทอ้างอิงกรณีการใช้งานเมื่อโปรโตคอลสามารถพิสูจน์ได้ว่าเป็นตัวเลือกที่แย่กว่าในการออกแบบ API (เช่นเดียวกับส่วน "Tips for Good API Design" ด้านบน) การค้นหาสมาชิกเส้นทางและตัวตัดคุณสมบัติ
  • Generics : เอกสารของ Swift เองสำหรับ Swift 5 เกี่ยวกับยาสามัญ