תכנות מונחה פרוטוקול & גנריות

הצג באתר TensorFlow.org הפעל ב-Google Colab צפה במקור ב-GitHub

מדריך זה יעבור על תכנות מונחה פרוטוקול, ודוגמאות שונות כיצד ניתן להשתמש בהן עם גנריות בדוגמאות יומיומיות.

פרוטוקולים

ירושה היא דרך רבת עוצמה לארגן קוד בשפות תכנות המאפשרת לך לשתף קוד בין מספר רכיבים של התוכנית.

בסוויפט יש דרכים שונות לבטא ירושה. ייתכן שאתה כבר מכיר את אחת מהדרכים הללו, משפות אחרות: ירושה כיתתית. עם זאת, לסוויפט יש דרך נוספת: פרוטוקולים.

במדריך זה, נחקור פרוטוקולים - חלופה לסיווג משנה המאפשר לך להשיג מטרות דומות באמצעות פשרות שונות. בסוויפט, הפרוטוקולים מכילים מספר חברים מופשטים. מחלקות, מבנים ומונים יכולים להתאים לפרוטוקולים מרובים וניתן לבסס את יחסי ההתאמה רטרואקטיבית. כל מה שמאפשר כמה עיצובים שלא ניתן להביע בקלות ב-Swift באמצעות סיווג משנה. נעבור על הניבים התומכים בשימוש בפרוטוקולים (הרחבות ומגבלות פרוטוקולים), כמו גם על מגבלות הפרוטוקולים.

סוגי הערך של Swift 💖!

בנוסף למחלקות שיש להן סמנטיקה של התייחסות, סוויפט תומכת ב-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 , ולאחר מכן שדגם רכב ספציפי יהיה מחלקה בסיסית. עם זאת, כאן שניהם פרוטוקולים נפרדים לחלוטין עם אפס צימוד! זה הופך את המערכת כולה לגמישה יותר באופן שבו אתה מעצב אותה.

בואו נגדיר טסלה:

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 כשנטען אותה מחדש. לפיכך, זה עתה הצלחנו לקשט כיתות, מבנים ומונים בהתנהגות ייחודית וברירת מחדל.

פרוטוקול קומיקס

תודה לריי ונדרליך על הקומיקס!

עם זאת, דבר אחד שצריך להיזהר ממנו הוא הדבר הבא. ביישום הראשון שלנו, אנו מגדירים את 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 הוא חלק ממערכת הטיפוס של סוויפט, שאותה נסקור בקרוב, אך בקצרה מאפשר לנו להוסיף דרישות נוספות להרחבה שאנו כותבים, כגון לדרוש מהטיפוס ליישם פרוטוקול, לדרוש שני סוגים להיות ה- זהה, או לדרוש מכיתה שתהיה מעמד-על מסוים.

Element הוא הסוג המשויך של האלמנטים בסוג תואם Collection . Element מוגדר בתוך פרוטוקול Sequence , אך מכיוון ש- Collection יורש מ- Sequence , הוא יורש את הסוג המשויך ל- Element .

Comparable הוא פרוטוקול המגדיר "סוג שניתן להשוות באמצעות האופרטורים היחסיים < , <= , >= ו- > ." . מכיוון שאנו מבצעים חיפוש בינארי על Collection ממוין, זה כמובן חייב להיות נכון, אחרת אנחנו לא יודעים אם לחזור/לחזור על שמאל או ימין בחיפוש הבינארי.

כהערת צד לגבי היישום, למידע נוסף על הפונקציה index(_:offsetBy:) שהייתה בשימוש, עיין בתיעוד הבא.

גנריות + פרוטוקולים = 💥

גנריות ופרוטוקולים יכולים להיות כלי רב עוצמה אם משתמשים בהם נכון כדי למנוע קוד כפול.

ראשית, עיין במדריך אחר, 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 ספציפי , אלא גם להיות מסוגל לסנן כל מבנה שיש לו מאפיין זה!

טיפים לעיצוב 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 סקלרי לווקטור, אבל לא הגיוני להגדיר פעולה כזו בהקשר של גיאומטריה.

אז, אולי מערכת יחסים ישנה תהיה טובה יותר על ידי עטיפת 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: תכנות מונחה פרוטוקול ב- Swift : זה הוצג באמצעות Swift 2, אז הרבה השתנה מאז (למשל שם הפרוטוקולים שבהם השתמשו במצגת) אבל זה עדיין משאב טוב לתיאוריה ולשימושים שמאחוריה .
  • היכרות עם תכנות מונחה פרוטוקול ב-Swift 3 : זה נכתב ב-Swift 3, אז ייתכן שיהיה צורך לשנות חלק מהקוד כדי שהקומפלציה תהיה מוצלחת, אבל זה עוד משאב נהדר.
  • WWDC 2019: עיצוב ממשק API מודרני של Swift : עובר על ההבדלים בין סוגי ערך והתייחסות, מקרה שימוש שבו פרוטוקולים יכולים להוכיח את עצמם כבחירה הגרועה יותר בעיצוב API (זהה לסעיף "טיפים לעיצוב API טוב" למעלה), מפתח חיפוש חברי נתיב, ועטיפות נכסים.
  • גנריות : התיעוד של סוויפט עצמו עבור Swift 5, הכל על גנריות.