این صفحه به‌وسیله ‏Cloud Translation API‏ ترجمه شده است.
Switch to English

برنامه نویسی و مقالات پروتکل گرا

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

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

پروتکل ها

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

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

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

انواع ارزش Swift!

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

در مرحله اول ، بیایید بررسی کنیم که 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

حال ، بیایید به چوبها نگاه کنیم. توجه کنید که ما نمی توانیم Stet ها را به ارث ببریم ، اما در عوض می توانیم از پروتکل ها استفاده کنیم:

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

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

رفتار پیش فرض غالب

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

بگذارید بگوییم ما یک نسخه قدیمی از یک ماشین الکتریکی داریم ، بنابراین سلامتی باتری به 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: 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 در یک نوع عمومی جدید که می تواند هر شماره نقطه شناور را کنترل کند ، رابطه ای بهتر باشد:

// 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: Modern Swift API Design : تفاوت بین مقدار و نوع مرجع را افزایش می دهد ، یک مورد استفاده از پروتکل ها می تواند به عنوان گزینه بدتر در طراحی API (همان بخش "نکات برای طراحی خوب API" در بالا) ، کلید جستجوی عضو مسیر ، و بسته بندی دارایی.
  • Generics : مستندات خود Swift برای Swift 5 همه چیز در مورد مواد غذایی است.