برنامه نویسی پروتکل محور & ژنریک ها

مشاهده در TensorFlow.org در Google Colab اجرا شود مشاهده منبع در GitHub

این آموزش به برنامه‌نویسی پروتکل‌محور و مثال‌های مختلف از نحوه استفاده از آنها با ژنریک‌ها در مثال‌های روزمره می‌پردازد.

پروتکل ها

وراثت یک راه قدرتمند برای سازماندهی کد در زبان های برنامه نویسی است که به شما امکان می دهد کد را بین چندین مؤلفه برنامه به اشتراک بگذارید.

در سوئیفت روش های مختلفی برای بیان وراثت وجود دارد. ممکن است قبلاً با یکی از آن راه‌ها از زبان‌های دیگر آشنا باشید: وراثت کلاس. با این حال، سوئیفت راه دیگری نیز دارد: پروتکل ها.

در این آموزش، پروتکل‌ها را بررسی می‌کنیم - جایگزینی برای طبقه‌بندی فرعی که به شما امکان می‌دهد از طریق معاوضه‌های مختلف به اهداف مشابه برسید. در سوئیفت، پروتکل ها حاوی چندین عضو انتزاعی هستند. کلاس‌ها، ساختارها و فهرست‌ها می‌توانند با چندین پروتکل مطابقت داشته باشند و رابطه انطباق را می‌توان به صورت ماسبق برقرار کرد. همه اینها برخی از طرح‌ها را قادر می‌سازد که به راحتی در سوئیفت با استفاده از طبقه‌بندی فرعی قابل بیان نیستند. ما از طریق اصطلاحاتی که از استفاده از پروتکل ها (افزونه ها و محدودیت های پروتکل) و همچنین محدودیت های پروتکل ها پشتیبانی می کنند، صحبت خواهیم کرد.

انواع ارزش Swift 💖!

سوئیفت علاوه بر کلاس هایی که معنایی مرجع دارند، از enum ها و ساختارهایی پشتیبانی می کند که با مقدار ارسال می شوند. Enum ها و ساختارها از بسیاری از ویژگی های ارائه شده توسط کلاس ها پشتیبانی می کنند. بیا یک نگاهی بیندازیم!

ابتدا، بیایید ببینیم که چگونه enum ها به کلاس ها شبیه هستند:

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 قرار دهیم. .

اینجاست که برنامه‌های افزودنی در سوئیفت می‌توانند مفید باشند:

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

بنابراین در حال حاضر، هر ماشین الکتریکی جدیدی که بسازیم، وقتی آن را شارژ می کنیم، باتری را روی 100 تنظیم می کند. بنابراین، ما به تازگی توانسته ایم کلاس ها، ساختارها و فهرست ها را با رفتار منحصر به فرد و پیش فرض تزئین کنیم.

پروتکل کمیک

با تشکر از 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

این به دلیل تفاوت بین ارسال استاتیک در مثال اول و ارسال استاتیک در پروتکل های دوم در سوئیفت رخ می دهد. برای اطلاعات بیشتر به این پست مدیوم مراجعه کنید.

نادیده گرفتن رفتار پیش فرض

با این حال، اگر بخواهیم، ​​همچنان می‌توانیم رفتار پیش‌فرض را لغو کنیم. نکته مهمی که باید به آن توجه کنید این است که این از ارسال پویا پشتیبانی نمی کند .

فرض کنید ما یک نسخه قدیمی از یک ماشین الکتریکی داریم، بنابراین سلامت باتری به 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
    }
}

استفاده های استاندارد کتابخانه ای از پروتکل ها

اکنون که ایده ای داریم که پروتکل ها در سوئیفت چگونه کار می کنند، اجازه دهید چند نمونه معمولی از استفاده از پروتکل های استاندارد کتابخانه را مرور کنیم.

گسترش کتابخانه استاندارد

بیایید ببینیم چگونه می‌توانیم قابلیت‌های اضافی را به انواعی که از قبل در سوئیفت وجود دارند اضافه کنیم. از آنجایی که انواع در سوئیفت داخلی نیستند، اما به عنوان ساختار بخشی از کتابخانه استاندارد هستند، انجام این کار آسان است.

بیایید سعی کنیم جستجوی باینری را روی آرایه ای از عناصر انجام دهیم، در حالی که مطمئن شویم که آرایه مرتب شده است:

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 گرفته شده است.

اکنون که فهمیدید پروتکل ها چگونه رفتار می کنند، بهتر است به این موضوع بپردازید که چه زمانی باید از پروتکل ها استفاده کنید. همانطور که پروتکل ها می توانند قدرتمند باشند، همیشه بهترین ایده نیست که وارد آن شوید و بلافاصله با پروتکل ها شروع کنید.

  • با موارد استفاده بتن شروع کنید:
    • ابتدا مورد استفاده را با انواع بتن کاوش کنید و درک کنید که چه کدی را می خواهید به اشتراک بگذارید و پیدا کنید که در حال تکرار است. سپس، کد مشترک را با ژنریک فاکتور کنید. ممکن است به معنای ایجاد پروتکل های جدید باشد. نیاز به کد عمومی را کشف کنید.
  • ایجاد پروتکل های جدید از پروتکل های موجود تعریف شده در کتابخانه استاندارد را در نظر بگیرید. برای مثال خوبی در این زمینه به مستندات اپل زیر مراجعه کنید.
  • به جای یک پروتکل عمومی، به جای آن یک نوع عمومی را تعریف کنید.

مثال: تعریف یک نوع برداری سفارشی

فرض کنید می‌خواهیم یک پروتکل 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 در یک نوع عمومی جدید که بتواند هر عدد ممیز شناور را کنترل کند، یک رابطه دارای-a بهتر باشد:

// 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: برنامه نویسی پروتکل گرا در سوئیفت : این برنامه با استفاده از سوئیفت 2 ارائه شد، بنابراین از آن زمان تا کنون چیزهای زیادی تغییر کرده است (مثلاً نام پروتکل هایی که در ارائه استفاده کردند) اما این هنوز منبع خوبی برای تئوری و استفاده های پشت سر آن است. .
  • معرفی برنامه نویسی پروتکل گرا در سوئیفت 3 : این در سوئیفت 3 نوشته شده است، بنابراین ممکن است برخی از کدها برای کامپایل شدن با موفقیت نیاز به اصلاح داشته باشند، اما منبع عالی دیگری است.
  • WWDC 2019: طراحی مدرن Swift API : تفاوت‌های بین انواع مقدار و مرجع را مورد بررسی قرار می‌دهد، یک مورد استفاده از زمانی که پروتکل‌ها می‌توانند بدترین انتخاب در طراحی API باشند (همانطور که در بخش «نکاتی برای طراحی خوب API» در بالا، کلید جستجوی اعضای مسیر و بسته‌بندی‌های ویژگی.
  • Generics : مستندات خود سوئیفت برای Swift 5 همه چیز در مورد ژنریک.