البرمجة الموجهة نحو البروتوكول & amp؛ الأدوية العامة

عرض على TensorFlow.org عرض المصدر على جيثب

سيتناول هذا البرنامج التعليمي البرمجة الموجهة نحو البروتوكول، وأمثلة مختلفة لكيفية استخدامها مع الأدوية العامة في الأمثلة اليومية.

البروتوكولات

يعد الوراثة طريقة قوية لتنظيم التعليمات البرمجية في لغات البرمجة التي تسمح لك بمشاركة التعليمات البرمجية بين مكونات متعددة للبرنامج.

في Swift، هناك طرق مختلفة للتعبير عن الميراث. ربما تكون على دراية بإحدى هذه الطرق من لغات أخرى: وراثة الفصل. ومع ذلك، لدى Swift طريقة أخرى: البروتوكولات.

في هذا البرنامج التعليمي، سوف نستكشف البروتوكولات - وهي بديل للتصنيف الفرعي الذي يسمح لك بتحقيق أهداف مماثلة من خلال مقايضات مختلفة. في Swift، تحتوي البروتوكولات على أعضاء مجردين متعددين. يمكن أن تتوافق الفئات والبنيات والتعدادات مع بروتوكولات متعددة ويمكن إنشاء علاقة التوافق بأثر رجعي. كل ذلك يمكّن بعض التصميمات التي لا يمكن التعبير عنها بسهولة في Swift باستخدام الفئات الفرعية. سنتعرف على المصطلحات التي تدعم استخدام البروتوكولات (الامتدادات وقيود البروتوكول)، بالإضافة إلى القيود المفروضة على البروتوكولات.

أنواع قيمة Swift 💖!

بالإضافة إلى الفئات التي تحتوي على دلالات مرجعية، يدعم Swift التعدادات والبنيات التي يتم تمريرها حسب القيمة. تدعم التعدادات والبنيات العديد من الميزات التي توفرها الفئات. لنلقي نظرة!

أولاً، دعونا نلقي نظرة على مدى تشابه التعدادات مع الفئات:

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 جزءًا من نظام الكتابة الخاص بـ Swift، والذي سنغطيه قريبًا، ولكن باختصار تتيح لنا إضافة متطلبات إضافية إلى الامتداد الذي نكتبه، مثل مطالبة النوع بتنفيذ بروتوكول، أو طلب نوعين ليكونا نفس الشيء، أو أن تطلب من الفصل أن يكون له فئة فائقة معينة.

Element هو النوع المرتبط بالعناصر في النوع المطابق Collection . يتم تعريف Element ضمن بروتوكول Sequence ، ولكن بما أن Collection ترث من Sequence ، فإنها ترث نوع Element المرتبط.

Comparable هو بروتوكول يحدد "النوع الذي يمكن مقارنته باستخدام العوامل العلائقية < , <= , >= , و > ." . نظرًا لأننا نجري بحثًا ثنائيًا على Collection مرتبة، فيجب أن يكون هذا بالطبع صحيحًا وإلا فإننا لا نعرف ما إذا كان يجب التكرار/التكرار يسارًا أو يمينًا في البحث الثنائي.

كملاحظة جانبية حول التنفيذ، لمزيد من المعلومات حول وظيفة index(_:offsetBy:) التي تم استخدامها، راجع الوثائق التالية.

الأدوية العامة + البروتوكولات = 💥

يمكن أن تكون الأدوية العامة والبروتوكولات أداة قوية إذا تم استخدامها بشكل صحيح لتجنب تكرار التعليمات البرمجية.

أولاً، قم بإلقاء نظرة على برنامج تعليمي آخر، جولة Swift ، والذي يغطي لفترة وجيزة الأدوية العامة في نهاية كتاب 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 talk.

الآن بعد أن فهمت كيفية عمل البروتوكولات، فمن الأفضل أن تعرف متى يجب عليك استخدام البروتوكولات. على الرغم من قوة البروتوكولات، إلا أنها ليست دائمًا أفضل فكرة للتعمق فيها والبدء فورًا في البروتوكولات.

  • ابدأ بحالات الاستخدام الملموسة:
    • استكشف أولاً حالة الاستخدام مع أنواع محددة وافهم الكود الذي تريد مشاركته واكتشف أنه يتم تكراره. بعد ذلك، قم بتحليل هذا الرمز المشترك مع الأدوية العامة. قد يعني إنشاء بروتوكولات جديدة. اكتشف الحاجة إلى كود عام.
  • فكر في إنشاء بروتوكولات جديدة من البروتوكولات الموجودة المحددة في المكتبة القياسية. راجع وثائق 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

إذا كتبنا العوامل الثلاثة في امتداد لبروتوكول 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 .

في بعض الأحيان قد تميل إلى إنشاء هذا التسلسل الهرمي للأنواع، ولكن تذكر أنه ليس ضروريًا دائمًا. وهذا يعني أيضًا أن الحجم الثنائي لبرنامجك المترجم سيكون أصغر، وسيكون تجميع التعليمات البرمجية الخاصة بك أسرع.

ومع ذلك، يعد أسلوب الامتداد هذا رائعًا عندما يكون لديك عدد قليل من الطرق التي تريد إضافتها. ومع ذلك، فإنها تواجه مشكلة قابلية التوسع عندما تقوم بتصميم واجهة برمجة تطبيقات أكبر.

هو؟ لديه أ؟

قلنا سابقًا أن 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)
    }
}

ولا يزال بإمكاننا استخدام الامتدادات العامة للحصول على العوامل الثلاثة السابقة التي أردنا تنفيذها والتي تبدو تمامًا كما كانت من قبل:

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: تصميم واجهة برمجة تطبيقات Swift الحديث : يتناول الاختلافات بين أنواع القيمة والمراجع، وهي حالة استخدام عندما تثبت البروتوكولات أنها الخيار الأسوأ في تصميم واجهة برمجة التطبيقات (مثل قسم "نصائح لتصميم واجهة برمجة التطبيقات الجيد" أعلاه)، المفتاح البحث عن أعضاء المسار وأغلفة الخصائص.
  • الأدوية العامة : وثائق Swift الخاصة بـ Swift 5 تدور حول الأدوية العامة.