ดูบน TensorFlow.org | ทำงานใน Google Colab | ดูแหล่งที่มาบน GitHub |
บทช่วยสอนนี้จะกล่าวถึงการเขียนโปรแกรมเชิงโปรโตคอล และตัวอย่างต่างๆ ของวิธีการใช้กับข้อมูลทั่วไปในตัวอย่างประจำวัน
โปรโตคอล
การสืบทอดเป็นวิธีที่มีประสิทธิภาพในการจัดระเบียบโค้ดในภาษาการเขียนโปรแกรมที่ช่วยให้คุณสามารถแชร์โค้ดระหว่างส่วนประกอบต่างๆ ของโปรแกรมได้
ใน Swift มีหลายวิธีในการแสดงมรดก คุณอาจคุ้นเคยกับวิธีใดวิธีหนึ่งจากภาษาอื่นแล้ว: การสืบทอดคลาส อย่างไรก็ตาม Swift มีวิธีอื่น: โปรโตคอล
ในบทช่วยสอนนี้ เราจะสำรวจโปรโตคอล - ทางเลือกนอกเหนือจากคลาสย่อยที่ช่วยให้คุณบรรลุเป้าหมายที่คล้ายกันผ่านการแลกเปลี่ยนที่แตกต่างกัน ใน Swift โปรโตคอลประกอบด้วยสมาชิกนามธรรมหลายตัว คลาส โครงสร้าง และแจงนับสามารถสอดคล้องกับโปรโตคอลหลายรายการ และสามารถสร้างความสัมพันธ์ของความสอดคล้องย้อนหลังได้ ทั้งหมดนี้ทำให้มีการออกแบบบางอย่างที่ไม่สามารถแสดงออกได้ง่ายใน Swift โดยใช้คลาสย่อย เราจะอธิบายสำนวนที่สนับสนุนการใช้โปรโตคอล (ส่วนขยายและข้อจำกัดของโปรโตคอล) รวมถึงข้อจำกัดของโปรโตคอล
ประเภทค่านิยมของ Swift 💖!
นอกจากคลาสที่มีความหมายอ้างอิงแล้ว Swift ยังรองรับ enums และ structs ที่ส่งผ่านด้วยค่าอีกด้วย Enums และ structs รองรับคุณสมบัติมากมายที่คลาสมีให้ มาดูกันดีกว่า!
อันดับแรก มาดูกันว่า enums มีความคล้ายคลึงกับคลาสอย่างไร:
enum Color: String {
case red = "red"
case green = "green"
case blue = "blue"
// A computed property. Note that enums cannot contain stored properties.
var hint: String {
switch self {
case .red:
return "Roses are this color."
case .green:
return "Grass is this color."
case .blue:
return "The ocean is this color."
}
}
// An initializer like for classes.
init?(color: String) {
switch color {
case "red":
self = .red
case "green":
self = .green
case "blue":
self = .blue
default:
return nil
}
}
}
// Can extend the enum as well!
extension Color {
// A function.
func hintFunc() -> String {
return self.hint
}
}
let c = Color.red
print("Give me a hint for c: \(c.hintFunc())")
let invalidColor = Color(color: "orange")
print("is invalidColor nil: \(invalidColor == nil)")
Give me a hint for c: Roses are this color. is invalidColor nil: true
ทีนี้มาดูโครงสร้างกัน โปรดสังเกตว่าเราไม่สามารถสืบทอดโครงสร้างได้ แต่สามารถใช้โปรโตคอลแทนได้:
struct FastCar {
// Can have variables and constants as stored properties.
var color: Color
let horsePower: Int
// Can have computed properties.
var watts: Float {
return Float(horsePower) * 745.7
}
// Can have lazy variables like in classes!
lazy var titleCaseColorString: String = {
let colorString = color.rawValue
return colorString.prefix(1).uppercased() +
colorString.lowercased().dropFirst()
}()
// A function.
func description() -> String {
return "This is a \(color) car with \(horsePower) horse power!"
}
// Can create a variety of initializers.
init(color: Color, horsePower: Int) {
self.color = color
self.horsePower = horsePower
}
// Can define extra initializers other than the default one.
init?(color: String, horsePower: Int) {
guard let enumColor = Color(color: color) else {
return nil
}
self.color = enumColor
self.horsePower = horsePower
}
}
var car = FastCar(color: .red, horsePower: 250)
print(car.description())
print("Horse power in watts: \(car.watts)")
print(car.titleCaseColorString)
This is a red car with 250 horse power! Horse power in watts: 186425.0 Red
สุดท้าย เรามาดูกันว่าพวกมันส่งผ่านตามประเภทค่าที่แตกต่างจากคลาสอย่างไร:
// Notice we have no problem modifying a constant class with
// variable properties.
class A {
var a = "a"
}
func foo(_ a: A) {
a.a = "foo"
}
let a = A()
print(a.a)
foo(a)
print(a.a)
/*
Uncomment the following code to see how an error is thrown.
Structs are implicitly passed by value, so we cannot modify it.
> "error: cannot assign to property: 'car' is a 'let' constant"
*/
// func modify(car: FastCar, toColor color: Color) -> Void {
// car.color = color
// }
// car = FastCar(color: .red, horsePower: 250)
// print(car.description())
// modify(car: &car, toColor: .blue)
// print(car.description())
a foo
ลองใช้โปรโตคอลกัน
เริ่มต้นด้วยการสร้างโปรโตคอลสำหรับรถยนต์ที่แตกต่างกัน:
protocol Car {
var color: Color { get set }
var price: Int { get }
func turnOn()
mutating func drive()
}
protocol Electric {
mutating func recharge()
// percentage of the battery level, 0-100%.
var batteryLevel: Int { get set }
}
protocol Gas {
mutating func refill()
// # of liters the car is holding, varies b/w models.
var gasLevelLiters: Int { get set }
}
ในโลกเชิงวัตถุ (ไม่มีการสืบทอดหลายรายการ) คุณอาจสร้างคลาสนามธรรมของ Electric
และ Gas
จากนั้นใช้การสืบทอดคลาสเพื่อสร้างทั้งคู่สืบทอดจาก Car
จากนั้นให้รุ่นรถเฉพาะเป็นคลาสพื้นฐาน อย่างไรก็ตาม ที่นี่ทั้งสองเป็นโปรโตคอลที่แยกจากกันโดยสิ้นเชิงโดย ไม่มี การเชื่อมต่อกัน! สิ่งนี้ทำให้ทั้งระบบมีความยืดหยุ่นมากขึ้นในการออกแบบของคุณ
มานิยาม Tesla กัน:
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 เมื่อเราชาร์จใหม่ ดังนั้นเราจึงสามารถตกแต่งคลาส โครงสร้าง และแจงนับด้วยพฤติกรรมที่ไม่ซ้ำใครและเป็นค่าเริ่มต้นได้
ขอบคุณ 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
สิ่งนี้เกิดขึ้นเนื่องจากความแตกต่างระหว่างการจัดส่งแบบคงที่ในตัวอย่างแรกและการจัดส่งแบบคงที่ในตัวอย่างที่สองบนโปรโตคอลใน 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
clause เป็นส่วนหนึ่งของระบบประเภทของ Swift ซึ่งเราจะพูดถึงเร็วๆ นี้ แต่สรุปสั้นๆ ให้เราเพิ่มข้อกำหนดเพิ่มเติมให้กับส่วนขยายที่เรากำลังเขียน เช่น กำหนดให้ประเภทต้องใช้โปรโตคอล เพื่อกำหนดให้มีสองประเภท เหมือนกันหรือต้องการให้คลาสมีซูเปอร์คลาสเฉพาะ
Element
เป็นประเภทที่เกี่ยวข้องขององค์ประกอบใน Collection
- ประเภทที่สอดคล้อง Element
ถูกกำหนดไว้ภายในโปรโตคอล Sequence
แต่เนื่องจาก Collection
สืบทอดมาจาก Sequence
จึงสืบทอดประเภทที่เกี่ยวข้องของ Element
Comparable
เป็นโปรโตคอลที่กำหนด "ประเภทที่สามารถเปรียบเทียบได้โดยใช้ตัวดำเนินการเชิงสัมพันธ์ <
, <=
, >=
และ >
" . เนื่องจากเรากำลังดำเนินการค้นหาแบบไบนารี่ใน Collection
ที่เรียงลำดับ แน่นอนว่าสิ่งนี้จะต้องเป็นจริง ไม่เช่นนั้นเราไม่รู้ว่าจะเรียกซ้ำ/วนซ้ำไปทางซ้ายหรือขวาในการค้นหาแบบไบนารี
หมายเหตุด้านข้างเกี่ยวกับการนำไปใช้งาน หากต้องการข้อมูลเพิ่มเติมเกี่ยวกับฟังก์ชัน index(_:offsetBy:)
ที่ใช้ โปรดดู เอกสาร ประกอบต่อไปนี้
ข้อมูลทั่วไป + โปรโตคอล = 💥
ข้อมูลทั่วไปและโปรโตคอลสามารถเป็นเครื่องมือที่มีประสิทธิภาพได้หากใช้อย่างถูกต้องเพื่อหลีกเลี่ยงโค้ดที่ซ้ำกัน
ขั้นแรก ให้ดูบทช่วยสอนอื่น A Swift Tour ซึ่งครอบคลุมข้อมูลทั่วไปโดยย่อในตอนท้ายของหนังสือ Colab
สมมติว่าคุณมีความคิดทั่วไปเกี่ยวกับยาชื่อสามัญแล้ว เรามาดูการใช้งานขั้นสูงกันดีกว่า
เมื่อประเภทเดียวมีข้อกำหนดหลายประการ เช่น ประเภทที่สอดคล้องกับหลายโปรโตคอล คุณจะมีตัวเลือกมากมายให้เลือก:
typealias ComparableReal = Comparable & FloatingPoint
func foo1<T: ComparableReal>(a: T, b: T) -> Bool {
return a > b
}
func foo2<T: Comparable & FloatingPoint>(a: T, b: T) -> Bool {
return a > b
}
func foo3<T>(a: T, b: T) -> Bool where T: ComparableReal {
return a > b
}
func foo4<T>(a: T, b: T) -> Bool where T: Comparable & FloatingPoint {
return a > b
}
func foo5<T: FloatingPoint>(a: T, b: T) -> Bool where T: Comparable {
return a > b
}
print(foo1(a: 1, b: 2))
print(foo2(a: 1, b: 2))
print(foo3(a: 1, b: 2))
print(foo4(a: 1, b: 2))
print(foo5(a: 1, b: 2))
false false false false false
สังเกตการใช้ typealias
ที่ด้านบน สิ่งนี้จะเพิ่มนามแฝงที่มีชื่อประเภทที่มีอยู่ในโปรแกรมของคุณ หลังจากประกาศประเภทนามแฝงแล้ว ชื่อนามแฝงจะสามารถนำมาใช้แทนประเภทที่มีอยู่ได้ทุกที่ในโปรแกรมของคุณ นามแฝงประเภทไม่ได้สร้างประเภทใหม่ พวกเขาเพียงแค่อนุญาตให้ชื่ออ้างอิงถึงประเภทที่มีอยู่
ตอนนี้เรามาดูกันว่าเราจะใช้โปรโตคอลและข้อมูลทั่วไปร่วมกันได้อย่างไร
สมมติว่าเราเป็นร้านคอมพิวเตอร์ที่มีข้อกำหนดต่อไปนี้สำหรับแล็ปท็อปที่เราขายเพื่อพิจารณาว่าจะจัดระเบียบแล็ปท็อปที่ด้านหลังร้านอย่างไร:
enum Box {
case small
case medium
case large
}
enum Mass {
case light
case medium
case heavy
}
// Note: `CustomStringConvertible` protocol lets us pretty-print a `Laptop`.
struct Laptop: CustomStringConvertible {
var name: String
var box: Box
var mass: Mass
var description: String {
return "(\(self.name) \(self.box) \(self.mass))"
}
}
อย่างไรก็ตาม เรามีข้อกำหนดใหม่ในการจัด Laptop
ตามมวล เนื่องจากชั้นวางมีข้อจำกัดด้านน้ำหนัก
func filtering(_ laptops: [Laptop], by mass: Mass) -> [Laptop] {
return laptops.filter { $0.mass == mass }
}
let laptops: [Laptop] = [
Laptop(name: "a", box: .small, mass: .light),
Laptop(name: "b", box: .large, mass: .medium),
Laptop(name: "c", box: .medium, mass: .heavy),
Laptop(name: "d", box: .large, mass: .light)
]
let filteredLaptops = filtering(laptops, by: .light)
print(filteredLaptops)
[(a small light), (d large light)]
อย่างไรก็ตาม จะเป็นอย่างไรหากเราต้องการกรองด้วยสิ่งอื่นที่ไม่ใช่ Mass
?
ทางเลือกหนึ่งคือทำสิ่งต่อไปนี้:
// Define a protocol which will act as our comparator.
protocol DeviceFilterPredicate {
associatedtype Device
func shouldKeep(_ item: Device) -> Bool
}
// Define the structs we will use for passing into our filtering function.
struct BoxFilter: DeviceFilterPredicate {
typealias Device = Laptop
var box: Box
func shouldKeep(_ item: Laptop) -> Bool {
return item.box == box
}
}
struct MassFilter: DeviceFilterPredicate {
typealias Device = Laptop
var mass: Mass
func shouldKeep(_ item: Laptop) -> Bool {
return item.mass == mass
}
}
// Make sure our filter conforms to `DeviceFilterPredicate` and that we are
// filtering `Laptop`s.
func filtering<F: DeviceFilterPredicate>(
_ laptops: [Laptop],
by filter: F
) -> [Laptop] where Laptop == F.Device {
return laptops.filter { filter.shouldKeep($0) }
}
// Let's test the function out!
print(filtering(laptops, by: BoxFilter(box: .large)))
print(filtering(laptops, by: MassFilter(mass: .heavy)))
[(b large medium), (d large light)] [(c medium heavy)]
สุดยอด! ตอนนี้เราสามารถกรองตามข้อจำกัดของแล็ปท็อปได้แล้ว อย่างไรก็ตาม เราสามารถกรองได้เฉพาะ Laptop
เท่านั้น
แล้วความสามารถในการกรองสิ่งที่อยู่ในกล่องและมีมวลล่ะ? บางทีคลังแล็ปท็อปนี้อาจใช้สำหรับเซิร์ฟเวอร์ที่มีฐานลูกค้าที่แตกต่างกัน:
// Define 2 new protocols so we can filter anything in a box and which has mass.
protocol Weighable {
var mass: Mass { get }
}
protocol Boxed {
var box: Box { get }
}
// Define the new Laptop and Server struct which have mass and a box.
struct Laptop: CustomStringConvertible, Boxed, Weighable {
var name: String
var box: Box
var mass: Mass
var description: String {
return "(\(self.name) \(self.box) \(self.mass))"
}
}
struct Server: CustomStringConvertible, Boxed, Weighable {
var isWorking: Bool
var name: String
let box: Box
let mass: Mass
var description: String {
if isWorking {
return "(working \(self.name) \(self.box) \(self.mass))"
} else {
return "(notWorking \(self.name) \(self.box) \(self.mass))"
}
}
}
// Define the structs we will use for passing into our filtering function.
struct BoxFilter<T: Boxed>: DeviceFilterPredicate {
var box: Box
func shouldKeep(_ item: T) -> Bool {
return item.box == box
}
}
struct MassFilter<T: Weighable>: DeviceFilterPredicate {
var mass: Mass
func shouldKeep(_ item: T) -> Bool {
return item.mass == mass
}
}
// Define the new filter function.
func filtering<F: DeviceFilterPredicate, T>(
_ elements: [T],
by filter: F
) -> [T] where T == F.Device {
return elements.filter { filter.shouldKeep($0) }
}
// Let's test the function out!
let servers = [
Server(isWorking: true, name: "serverA", box: .small, mass: .heavy),
Server(isWorking: false, name: "serverB", box: .medium, mass: .medium),
Server(isWorking: true, name: "serverC", box: .large, mass: .light),
Server(isWorking: false, name: "serverD", box: .medium, mass: .light),
Server(isWorking: true, name: "serverE", box: .small, mass: .heavy)
]
let products = [
Laptop(name: "a", box: .small, mass: .light),
Laptop(name: "b", box: .large, mass: .medium),
Laptop(name: "c", box: .medium, mass: .heavy),
Laptop(name: "d", box: .large, mass: .light)
]
print(filtering(servers, by: BoxFilter(box: .small)))
print(filtering(servers, by: MassFilter(mass: .medium)))
print(filtering(products, by: BoxFilter(box: .small)))
print(filtering(products, by: MassFilter(mass: .medium)))
[(working serverA small heavy), (working serverE small heavy)] [(notWorking serverB medium medium)] [(a small light)] [(b large medium)]
ตอนนี้เราสามารถกรองอาร์เรย์ด้วยคุณสมบัติใดๆ ของ struct
เฉพาะเจาะจงเท่านั้น แต่ยังสามารถกรองโครงสร้างใดๆ ที่มีคุณสมบัตินั้นได้อีกด้วย
เคล็ดลับสำหรับการออกแบบ API ที่ดี
ส่วนนี้นำมาจากการพูดคุย เรื่อง WWDC 2019: Modern Swift API Design
เมื่อคุณเข้าใจวิธีการทำงานของโปรโตคอลแล้ว วิธีที่ดีที่สุดคือพิจารณาว่าคุณควรใช้โปรโตคอลเมื่อใด แม้ว่าโปรโตคอลจะมีประสิทธิภาพ แต่ก็ไม่ใช่ความคิดที่ดีที่สุดเสมอไปที่จะเริ่มต้นใช้งานโปรโตคอลทันที
- เริ่มต้นด้วยกรณีการใช้งานที่เป็นรูปธรรม:
- ขั้นแรก ให้สำรวจกรณีการใช้งานด้วยประเภทที่เป็นรูปธรรม และทำความเข้าใจว่าคุณต้องการแชร์โค้ดใดและพบว่ามีการทำซ้ำ จากนั้นแยกปัจจัยที่แชร์โค้ดกับยาชื่อสามัญ อาจหมายถึงการสร้างโปรโตคอลใหม่ ค้นพบความต้องการโค้ดทั่วไป
- พิจารณาสร้างโปรโตคอลใหม่จากโปรโตคอลที่มีอยู่ซึ่งกำหนดไว้ในไลบรารีมาตรฐาน โปรดดูตัวอย่างที่ดีใน เอกสารประกอบของ Apple ต่อไปนี้
- แทนที่จะใช้โปรโตคอลทั่วไป ให้พิจารณากำหนดประเภททั่วไปแทน
ตัวอย่าง: การกำหนดประเภทเวกเตอร์ที่กำหนดเอง
สมมติว่าเราต้องการกำหนดโปรโตคอล GeometricVector
บนตัวเลขทศนิยมเพื่อใช้ในแอปเรขาคณิตบางตัวที่เรากำลังสร้าง ซึ่งกำหนดการดำเนินการเวกเตอร์ที่สำคัญ 3 รายการ:
protocol GeometricVector {
associatedtype Scalar: FloatingPoint
static func dot(_ a: Self, _ b: Self) -> Scalar
var length: Scalar { get }
func distance(to other: Self) -> Scalar
}
สมมติว่าเราต้องการจัดเก็บขนาดของเวกเตอร์ ซึ่งโปรโตคอล SIMD
สามารถช่วยเราได้ ดังนั้นเราจะทำให้ประเภทใหม่ของเราปรับแต่งโปรโตคอล SIMD
เวกเตอร์ SIMD
ถือได้ว่าเป็นเวกเตอร์ขนาดคงที่ซึ่งทำงานได้เร็วมากเมื่อคุณใช้เวกเตอร์เหล่านี้เพื่อดำเนินการกับเวกเตอร์:
protocol GeometricVector: SIMD {
associatedtype Scalar: FloatingPoint
static func dot(_ a: Self, _ b: Self) -> Scalar
var length: Scalar { get }
func distance(to other: Self) -> Scalar
}
ตอนนี้ให้เรากำหนดการใช้งานเริ่มต้นของการดำเนินการข้างต้น:
extension GeometricVector {
static func dot(_ a: Self, _ b: Self) -> Scalar {
(a * b).sum()
}
var length: Scalar {
Self.dot(self, self).squareRoot()
}
func distance(to other: Self) -> Scalar {
(self - other).length
}
}
จากนั้นเราจำเป็นต้องเพิ่มความสอดคล้องให้กับแต่ละประเภทที่เราต้องการเพิ่มความสามารถเหล่านี้:
extension SIMD2: GeometricVector where Scalar: FloatingPoint { }
extension SIMD3: GeometricVector where Scalar: FloatingPoint { }
extension SIMD4: GeometricVector where Scalar: FloatingPoint { }
extension SIMD8: GeometricVector where Scalar: FloatingPoint { }
extension SIMD16: GeometricVector where Scalar: FloatingPoint { }
extension SIMD32: GeometricVector where Scalar: FloatingPoint { }
extension SIMD64: GeometricVector where Scalar: FloatingPoint { }
กระบวนการสามขั้นตอนในการกำหนดโปรโตคอล ให้เป็นการใช้งานเริ่มต้น จากนั้นเพิ่มความสอดคล้องให้กับหลายประเภทนั้นค่อนข้างจะซ้ำซาก
โปรโตคอลจำเป็นหรือไม่?
ความจริงที่ว่าไม่มี SIMD
ประเภทใดที่มีการใช้งานเฉพาะถือเป็นสัญญาณเตือน ดังนั้นในกรณีนี้ โปรโตคอลไม่ได้ให้อะไรเราเลยจริงๆ
กำหนดไว้ในส่วนขยายของ SIMD
หากเราเขียนโอเปอเรเตอร์ 3 ตัวในส่วนขยายของโปรโตคอล SIMD
สิ่งนี้สามารถแก้ปัญหาได้กระชับยิ่งขึ้น:
extension SIMD where Scalar: FloatingPoint {
static func dot(_ a: Self, _ b: Self) -> Scalar {
(a * b).sum()
}
var length: Scalar {
Self.dot(self, self).squareRoot()
}
func distance(to other: Self) -> Scalar {
(self - other).length
}
}
ด้วยการใช้โค้ดน้อยลง เราได้เพิ่มการใช้งานเริ่มต้นทั้งหมดให้กับ SIMD
ทุกประเภท
บางครั้งคุณอาจถูกล่อลวงให้สร้างลำดับชั้นของประเภทนี้ แต่จำไว้ว่าไม่จำเป็นเสมอไป นอกจากนี้ยังหมายความว่าขนาดไบนารี่ของโปรแกรมที่คอมไพล์จะเล็กลง และโค้ดของคุณก็จะคอมไพล์ได้เร็วขึ้น
อย่างไรก็ตาม วิธีการขยายนี้เหมาะสำหรับเมื่อคุณมีหลายวิธีที่ต้องการเพิ่ม อย่างไรก็ตาม จะประสบปัญหาด้านความสามารถในการปรับขนาดเมื่อคุณออกแบบ API ที่มีขนาดใหญ่ขึ้น
คือ? มี?
ก่อนหน้านี้เราได้กล่าวว่า GeometricVector
จะปรับแต่ง SIMD
แต่นี่คือความสัมพันธ์ใช่ไหม? ปัญหาคือ SIMD
กำหนดการดำเนินการซึ่งทำให้เราเพิ่มสเกลาร์ 1 ให้กับเวกเตอร์ได้ แต่มันไม่สมเหตุสมผลเลยที่จะกำหนดการดำเนินการดังกล่าวในบริบทของเรขาคณิต
ดังนั้น บางทีความสัมพันธ์แบบมี-ความสัมพันธ์อาจดีกว่าด้วยการห่อ SIMD
ให้เป็นประเภททั่วไปใหม่ที่สามารถรองรับจำนวนจุดลอยตัวใดๆ ได้:
// NOTE: `Storage` is the underlying type that is storing the values,
// just like in a `SIMD` vector.
struct GeometricVector<Storage: SIMD> where Storage.Scalar: FloatingPoint {
typealias Scalar = Storage.Scalar
var value: Storage
init(_ value: Storage) { self.value = value }
}
จากนั้นเราสามารถระมัดระวังและกำหนดเฉพาะการดำเนินการที่เหมาะสมในบริบทของเรขาคณิตเท่านั้น:
extension GeometricVector {
static func + (a: Self, b: Self) -> Self {
Self(a.value + b.value)
}
static func - (a: Self, b: Self) -> Self {
Self(a.value - b.value)
}
static func * (a: Self, b: Scalar) -> Self {
Self(a.value * b)
}
}
และเรายังคงสามารถใช้ส่วนขยายทั่วไปเพื่อรับโอเปอเรเตอร์ 3 ตัวก่อนหน้านี้ที่เราต้องการใช้ ซึ่งมีลักษณะเกือบจะเหมือนเดิมทุกประการ:
extension GeometricVector {
static func dot(_ a: Self, _ b: Self) -> Scalar {
(a.value * b.value).sum()
}
var length: Scalar {
Self.dot(self, self).squareRoot()
}
func distance(to other: Self) -> Scalar {
(self - other).length
}
}
โดยรวมแล้ว เราสามารถปรับแต่งพฤติกรรมของการดำเนินการทั้งสามของเราให้เป็นประเภทใดประเภทหนึ่งได้เพียงแค่ใช้โครงสร้าง ด้วยโปรโตคอล เราประสบปัญหาในการเขียนความสอดคล้องซ้ำๆ กับเวกเตอร์ SIMD
ทั้งหมด และยังไม่สามารถป้องกันไม่ให้ตัวดำเนินการบางอย่าง เช่น Scalar + Vector
ใช้งานได้ (ซึ่งในกรณีนี้เราไม่ต้องการ) ด้วยเหตุนี้ โปรดจำไว้ว่าโปรโตคอลไม่ใช่วิธีแก้ปัญหาสำหรับทุกคน แต่บางครั้งโซลูชันแบบเดิมๆ ก็สามารถพิสูจน์ได้ว่ามีประสิทธิภาพมากกว่า
ทรัพยากรการเขียนโปรแกรมเชิงโปรโตคอลเพิ่มเติม
ต่อไปนี้เป็นแหล่งข้อมูลเพิ่มเติมในหัวข้อที่กล่าวถึง:
- WWDC 2015: การเขียนโปรแกรมเชิงโปรโตคอลใน Swift : สิ่งนี้ถูกนำเสนอโดยใช้ Swift 2 ดังนั้นมีการเปลี่ยนแปลงมากมายตั้งแต่นั้นมา (เช่น ชื่อของโปรโตคอลที่ใช้ในการนำเสนอ) แต่นี่ยังคงเป็นแหล่งข้อมูลที่ดีสำหรับทฤษฎีและใช้เบื้องหลัง .
- ขอแนะนำการเขียนโปรแกรมเชิงโปรโตคอลใน Swift 3 : สิ่งนี้เขียนด้วย Swift 3 ดังนั้นโค้ดบางส่วนอาจต้องได้รับการแก้ไขเพื่อให้สามารถคอมไพล์ได้สำเร็จ แต่ก็เป็นแหล่งข้อมูลที่ดีอีกแหล่งหนึ่ง
- WWDC 2019: การออกแบบ Swift API สมัยใหม่ : กล่าวถึงความแตกต่างระหว่างค่าและประเภทการอ้างอิง กรณีการใช้งานเมื่อโปรโตคอลสามารถพิสูจน์ได้ว่าเป็นตัวเลือกที่แย่กว่าในการออกแบบ API (เหมือนกับส่วน "เคล็ดลับสำหรับการออกแบบ API ที่ดี" ด้านบน) คีย์ การค้นหาสมาชิกพาธ และ wrappers คุณสมบัติ
- ข้อมูลทั่วไป : เอกสารของ Swift สำหรับ Swift 5 ทั้งหมดเกี่ยวกับข้อมูลทั่วไป