مشاهده در 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 همه چیز در مورد ژنریک.