บทนำสู่การเขียนโปรแกรมเชิงโปรโตคอลใน Swift
เผยแพร่แล้ว: 2022-03-11โปรโตคอลเป็นคุณลักษณะที่มีประสิทธิภาพมากของภาษาโปรแกรม Swift
โปรโตคอลใช้เพื่อกำหนด "พิมพ์เขียวของวิธีการ คุณสมบัติ และข้อกำหนดอื่นๆ ที่เหมาะสมกับงานหรือฟังก์ชันเฉพาะ"
Swift ตรวจสอบปัญหาความสอดคล้องของโปรโตคอลในเวลาคอมไพล์ ทำให้นักพัฒนาสามารถค้นพบข้อบกพร่องร้ายแรงบางอย่างในโค้ดได้ก่อนจะรันโปรแกรม โปรโตคอลช่วยให้นักพัฒนาเขียนโค้ดที่ยืดหยุ่นและขยายได้ใน Swift โดยไม่ต้องประนีประนอมกับความหมายของภาษา
Swift เพิ่มความสะดวกในการใช้โปรโตคอลไปอีกขั้นด้วยการจัดเตรียมวิธีแก้ไขปัญหาเฉพาะหน้าสำหรับปัญหาทั่วไปและข้อจำกัดบางประการของอินเทอร์เฟซที่ทำให้เกิดปัญหากับภาษาการเขียนโปรแกรมอื่นๆ
ใน Swift เวอร์ชันก่อนหน้า เป็นไปได้ที่จะขยายคลาส โครงสร้าง และ enums เท่านั้น ดังที่เป็นจริงในภาษาการเขียนโปรแกรมสมัยใหม่หลายภาษา อย่างไรก็ตาม เนื่องจาก Swift เวอร์ชัน 2 จึงสามารถขยายโปรโตคอลได้เช่นกัน
บทความนี้จะตรวจสอบวิธีการใช้โปรโตคอลใน Swift ในการเขียนโค้ดที่ใช้ซ้ำได้และบำรุงรักษาได้ และวิธีที่การเปลี่ยนแปลงในโค้ดเบสเชิงโปรโตคอลขนาดใหญ่สามารถรวมไว้ในที่เดียวผ่านการใช้ส่วนขยายโปรโตคอล
โปรโตคอล
โปรโตคอลคืออะไร?
ในรูปแบบที่ง่ายที่สุด โปรโตคอลคืออินเทอร์เฟซที่อธิบายคุณสมบัติและวิธีการบางอย่าง ประเภทใดก็ตามที่สอดคล้องกับโปรโตคอลควรกรอกคุณสมบัติเฉพาะที่กำหนดไว้ในโปรโตคอลด้วยค่าที่เหมาะสมและใช้วิธีการที่จำเป็น ตัวอย่างเช่น:
protocol Queue { var count: Int { get } mutating func push(_ element: Int) mutating func pop() -> Int }
โปรโตคอลคิวอธิบายคิวที่มีรายการจำนวนเต็ม ไวยากรณ์ค่อนข้างตรงไปตรงมา
ภายในบล็อคโปรโตคอล เมื่อเราอธิบายคุณสมบัติ เราต้องระบุว่าคุณสมบัตินั้นเป็นเพียง gettable { get }
หรือทั้ง gettable และ settable { get set }
ในกรณีของเรา ตัวแปร Count (ประเภท Int
) จะได้รับเท่านั้น
หากโปรโตคอลต้องการให้คุณสมบัติสามารถตั้งค่าได้และตั้งค่าได้ ความต้องการนั้นไม่สามารถทำได้โดยคุณสมบัติที่เก็บไว้คงที่หรือคุณสมบัติที่คำนวณได้แบบอ่านอย่างเดียว
หากโปรโตคอลต้องการเพียงคุณสมบัติที่จะได้รับ ความต้องการสามารถบรรลุตามคุณสมบัติประเภทใดก็ได้ และถูกต้องสำหรับคุณสมบัติที่จะตั้งค่าได้เช่นกัน หากสิ่งนี้มีประโยชน์สำหรับรหัสของคุณเอง
สำหรับฟังก์ชันที่กำหนดไว้ในโปรโตคอล สิ่งสำคัญคือต้องระบุว่าฟังก์ชันจะเปลี่ยนเนื้อหาด้วยคีย์เวิร์ด mutating
หรือไม่ นอกจากนั้น ลายเซ็นของฟังก์ชันก็เพียงพอแล้วสำหรับคำจำกัดความ
เพื่อให้สอดคล้องกับโปรโตคอล ประเภทต้องจัดเตรียมคุณสมบัติของอินสแตนซ์ทั้งหมดและใช้วิธีการทั้งหมดที่อธิบายไว้ในโปรโตคอล ตัวอย่างเช่น ด้านล่างเป็นโครงสร้าง Container
ที่สอดคล้องกับโปรโตคอล Queue
ของเรา struct จัดเก็บผลัก Int
ไว้ใน items
อาร์เรย์ส่วนตัว
struct Container: Queue { private var items: [Int] = [] var count: Int { return items.count } mutating func push(_ element: Int) { items.append(element) } mutating func pop() -> Int { return items.removeFirst() } }
อย่างไรก็ตาม โปรโตคอลคิวปัจจุบันของเรามีข้อเสียที่สำคัญ
เฉพาะคอนเทนเนอร์ที่จัดการกับ Int
เท่านั้นที่สามารถปฏิบัติตามโปรโตคอลนี้ได้
เราสามารถลบข้อจำกัดนี้ได้โดยใช้คุณลักษณะ "ประเภทที่เกี่ยวข้อง" ประเภทที่เกี่ยวข้องทำงานเหมือนยาสามัญ ในการสาธิต ให้เปลี่ยนโปรโตคอลคิวเพื่อใช้ประเภทที่เกี่ยวข้องกัน:
protocol Queue { associatedtype ItemType var count: Int { get } func push(_ element: ItemType) func pop() -> ItemType }
ตอนนี้โปรโตคอลคิวอนุญาตให้จัดเก็บรายการประเภท ใดก็ได้
ในการใช้งานโครงสร้าง Container
คอมไพเลอร์จะกำหนดประเภทที่เกี่ยวข้องจากบริบท (เช่น ประเภทการส่งคืนเมธอด และประเภทพารามิเตอร์) วิธีนี้ช่วยให้เราสร้างโครงสร้าง Container
ด้วยประเภทรายการทั่วไปได้ ตัวอย่างเช่น:
class Container<Item>: Queue { private var items: [Item] = [] var count: Int { return items.count } func push(_ element: Item) { items.append(element) } func pop() -> Item { return items.removeFirst() } }
การใช้โปรโตคอลทำให้การเขียนโค้ดง่ายขึ้นในหลายกรณี
ตัวอย่างเช่น อ็อบเจ็กต์ใดๆ ที่แสดงถึงข้อผิดพลาดสามารถสอดคล้องกับโปรโตคอล Error
(หรือ LocalizedError
ในกรณีที่เราต้องการให้คำอธิบายที่แปลเป็นภาษาท้องถิ่น)
ตรรกะการจัดการข้อผิดพลาดเดียวกันนี้สามารถนำไปใช้กับออบเจ็กต์ข้อผิดพลาดใดๆ ก็ได้ตลอดทั้งโค้ดของคุณ ดังนั้น คุณไม่จำเป็นต้องใช้วัตถุเฉพาะใดๆ (เช่น NSError ใน Objective-C) เพื่อแสดงข้อผิดพลาด คุณสามารถใช้ประเภทใดก็ได้ที่สอดคล้องกับโปรโตคอล Error
หรือ LocalizedError
คุณยังสามารถขยายประเภทสตริงเพื่อให้สอดคล้องกับโปรโตคอล LocalizedError
และโยนสตริงเป็นข้อผิดพลาดได้
extension String: LocalizedError { public var errorDescription: String? { Return NSLocalizedString(self, comment:””) } } throw “Unfortunately something went wrong” func handle(error: Error) { print(error.localizedDescription) }
ส่วนขยายโปรโตคอล
ส่วนขยายโปรโตคอลสร้างขึ้นจากความยอดเยี่ยมของโปรโตคอล พวกเขาอนุญาตให้เรา:
จัดเตรียมการใช้งานเริ่มต้นของวิธีโปรโตคอลและค่าดีฟอลต์ของคุณสมบัติของโปรโตคอล ซึ่งทำให้เป็น "ทางเลือก" ประเภทที่สอดคล้องกับโปรโตคอลสามารถจัดเตรียมการใช้งานของตนเองหรือใช้ค่าเริ่มต้นได้
เพิ่มการใช้งานวิธีการเพิ่มเติมที่ไม่ได้อธิบายไว้ในโปรโตคอล และ "ตกแต่ง" ประเภทใดๆ ที่สอดคล้องกับโปรโตคอลด้วยวิธีการเพิ่มเติมเหล่านี้ คุณลักษณะนี้ช่วยให้เราสามารถเพิ่มวิธีการเฉพาะสำหรับหลายประเภทที่สอดคล้องกับโปรโตคอลแล้วโดยไม่ต้องแก้ไขแต่ละประเภททีละรายการ
การนำวิธีการเริ่มต้นไปใช้
มาสร้างอีกหนึ่งโปรโตคอลกัน:
protocol ErrorHandler { func handle(error: Error) }
โปรโตคอลนี้อธิบายอ็อบเจ็กต์ที่รับผิดชอบในการจัดการข้อผิดพลาดที่เกิดขึ้นในแอปพลิเคชัน ตัวอย่างเช่น:
struct Handler: ErrorHandler { func handle(error: Error) { print(error.localizedDescription) } }
ที่นี่เราเพียงแค่พิมพ์คำอธิบายของข้อผิดพลาดที่แปลแล้ว ด้วยส่วนขยายโปรโตคอล เราสามารถทำให้การใช้งานนี้เป็นค่าเริ่มต้นได้
extension ErrorHandler { func handle(error: Error) { print(error.localizedDescription) } }
การทำเช่นนี้จะทำให้วิธี handle
เป็นทางเลือกโดยจัดเตรียมการใช้งานเริ่มต้น
ความสามารถในการขยายโปรโตคอลที่มีอยู่ด้วยการทำงานเริ่มต้นนั้นค่อนข้างมีประสิทธิภาพ ทำให้โปรโตคอลสามารถขยายและขยายได้โดยไม่ต้องกังวลเกี่ยวกับการทำลายความเข้ากันได้ของโค้ดที่มีอยู่
ส่วนขยายตามเงื่อนไข
ดังนั้นเราจึงได้จัดเตรียมการใช้งานเริ่มต้นของวิธีการ handle
แต่การพิมพ์ไปยังคอนโซลไม่เป็นประโยชน์อย่างยิ่งต่อผู้ใช้ปลายทาง
เราอาจต้องการแสดงมุมมองการแจ้งเตือนบางประเภทพร้อมคำอธิบายที่แปลแล้ว ในกรณีที่ตัวจัดการข้อผิดพลาดเป็นตัวควบคุมมุมมอง ในการทำเช่นนี้ เราสามารถขยายโปรโตคอล ErrorHandler
ได้ แต่สามารถจำกัดส่วนขยายให้ใช้ได้เฉพาะในบางกรณีเท่านั้น (เช่น เมื่อประเภทเป็นตัวควบคุมการดู)
Swift ช่วยให้เราสามารถเพิ่มเงื่อนไขดังกล่าวให้กับส่วนขยายโปรโตคอลโดยใช้คีย์เวิร์ด where
extension ErrorHandler where Self: UIViewController { func handle(error: Error) { let alert = UIAlertController(title: nil, message: error.localizedDescription, preferredStyle: .alert) let action = UIAlertAction(title: "OK", style: .cancel, handler: nil) alert.addAction(action) present(alert, animated: true, completion: nil) } }
ตนเอง (ด้วยตัวพิมพ์ใหญ่ "S") ในข้อมูลโค้ดด้านบนหมายถึงประเภท (โครงสร้าง คลาส หรือ enum) โดยการระบุว่าเราขยายโปรโตคอลสำหรับประเภทที่สืบทอดจาก UIViewController
เท่านั้น เราจึงสามารถใช้วิธีการเฉพาะของ UIViewController
(เช่น present(viewControllerToPresnt: animated: completion)
) ได้

ในตอนนี้ ตัวควบคุมมุมมองใดๆ ที่สอดคล้องกับโปรโตคอล ErrorHandler
มีการนำวิธีการ handle
ที่เป็นค่าเริ่มต้นไปใช้งานซึ่งแสดงมุมมองการแจ้งเตือนพร้อมคำอธิบายที่แปลเป็นภาษาท้องถิ่น
การนำวิธีการที่คลุมเครือ
สมมติว่ามีสองโปรโตคอล ซึ่งทั้งสองวิธีมีวิธีการที่มีลายเซ็นเดียวกัน
protocol P1 { func method() //some other methods } protocol P2 { func method() //some other methods }
โปรโตคอลทั้งสองมีส่วนขยายพร้อมการใช้งานเริ่มต้นของวิธีนี้
extension P1 { func method() { print("Method P1") } } extension P2 { func method() { print("Method P2") } }
ทีนี้ สมมุติว่ามีประเภทหนึ่งที่สอดคล้องกับโปรโตคอลทั้งสอง
struct S: P1, P2 { }
ในกรณีนี้ เรามีปัญหากับการใช้วิธีการที่คลุมเครือ ประเภทไม่ได้ระบุชัดเจนว่าควรใช้วิธีการที่ควรใช้ ด้วยเหตุนี้ เราจึงได้รับข้อผิดพลาดในการรวบรวม ในการแก้ไขปัญหานี้ เราต้องเพิ่มการนำวิธีการไปใช้กับประเภท
struct S: P1, P2 { func method() { print("Method S") } }
ภาษาโปรแกรมเชิงอ็อบเจ็กต์จำนวนมากถูกรบกวนด้วยข้อจำกัดเกี่ยวกับความละเอียดของส่วนขยายที่คลุมเครือ Swift จัดการสิ่งนี้ได้ค่อนข้างหรูหราผ่านส่วนขยายโปรโตคอลโดยอนุญาตให้โปรแกรมเมอร์ควบคุมตำแหน่งที่คอมไพเลอร์ขาดหายไป
เพิ่มวิธีการใหม่
มาดูโปรโตคอล Queue
กันอีกครั้ง
protocol Queue { associatedtype ItemType var count: Int { get } func push(_ element: ItemType) func pop() -> ItemType }
แต่ละประเภทที่สอดคล้องกับโปรโตคอล Queue
มีคุณสมบัติอินสแตนซ์การ count
ที่กำหนดจำนวนรายการที่เก็บไว้ ซึ่งทำให้เราสามารถเปรียบเทียบประเภทดังกล่าวเพื่อตัดสินใจว่าประเภทใดใหญ่กว่า เราสามารถเพิ่มวิธีนี้ผ่านส่วนขยายโปรโตคอล
extension Queue { func compare<Q>(queue: Q) -> ComparisonResult where Q: Queue { if count < queue.count { return .orderedDescending } if count > queue.count { return .orderedAscending } return .orderedSame } }
วิธีนี้ไม่ได้อธิบายไว้ในโปรโตคอล Queue
เนื่องจากไม่เกี่ยวข้องกับฟังก์ชันคิว
ดังนั้นจึงไม่ใช่การใช้งานเริ่มต้นของวิธีโปรโตคอล แต่เป็นการนำวิธีการใหม่ที่ "ตกแต่ง" ทุกประเภทที่สอดคล้องกับโปรโตคอล Queue
หากไม่มีส่วนขยายโปรโตคอล เราจะต้องเพิ่มวิธีนี้ในแต่ละประเภทแยกกัน
ส่วนขยายโปรโตคอลเทียบกับคลาสพื้นฐาน
ส่วนขยายโปรโตคอลอาจดูคล้ายกับการใช้คลาสพื้นฐาน แต่มีประโยชน์หลายประการของการใช้ส่วนขยายโปรโตคอล ซึ่งรวมถึงแต่ไม่จำกัดเฉพาะ:
เนื่องจากคลาส โครงสร้าง และ enum สามารถสอดคล้องกับโปรโตคอลมากกว่าหนึ่งโปรโตคอล จึงสามารถใช้การนำโปรโตคอลหลายตัวไปใช้งานโดยปริยายได้ แนวคิดนี้คล้ายกับการสืบทอดหลายภาษาในภาษาอื่นๆ
โปรโตคอลสามารถนำมาใช้โดยคลาส โครงสร้าง และ enums ในขณะที่คลาสฐานและการสืบทอดมีให้สำหรับคลาสเท่านั้น
ส่วนขยายไลบรารีมาตรฐาน Swift
นอกจากการขยายโปรโตคอลของคุณเองแล้ว คุณยังสามารถขยายโปรโตคอลจากไลบรารีมาตรฐาน Swift ได้อีกด้วย ตัวอย่างเช่น หากเราต้องการค้นหาขนาดเฉลี่ยของคอลเลกชันของคิว เราสามารถทำได้โดยขยายโปรโตคอลการ Collection
มาตรฐาน
โครงสร้างข้อมูลตามลำดับที่จัดเตรียมโดยไลบรารีมาตรฐานของ Swift ซึ่งองค์ประกอบสามารถข้ามผ่านและเข้าถึงได้ผ่านตัวห้อยที่จัดทำดัชนี มักจะสอดคล้องกับโปรโตคอล Collection
ด้วยการขยายโปรโตคอล มันเป็นไปได้ที่จะขยายโครงสร้างข้อมูลไลบรารีมาตรฐานทั้งหมดหรือขยายบางส่วนของพวกเขาแบบเลือกสรร
หมายเหตุ: โปรโตคอลเดิมชื่อ
CollectionType
ใน Swift 2.x ถูกเปลี่ยนชื่อเป็นCollection
ใน Swift 3
extension Collection where Iterator.Element: Queue { func avgSize() -> Int { let size = map { $0.count }.reduce(0, +) return Int(round(Double(size) / Double(count.toIntMax()))) } }
ตอนนี้เราสามารถคำนวณขนาดเฉลี่ยของคอลเลกชันของคิว ( Array
, Set
ฯลฯ ) หากไม่มีส่วนขยายโปรโตคอล เราจำเป็นต้องเพิ่มวิธีนี้ให้กับคอลเล็กชันแต่ละประเภทแยกกัน
ในไลบรารีมาตรฐาน Swift ส่วนขยายโปรโตคอลจะใช้ในการดำเนินการ เช่น วิธีการต่างๆ เช่น map
, filter
, reduce
เป็นต้น
extension Collection { public func map<T>(_ transform: (Self.Iterator.Element) throws -> T) rethrows -> [T] { } }
โปรโตคอลส่วนขยายและความหลากหลาย
อย่างที่ฉันพูดไปก่อนหน้านี้ ส่วนขยายโปรโตคอลทำให้เราเพิ่มการใช้งานเริ่มต้นของวิธีการบางอย่าง และเพิ่มการใช้งานวิธีการใหม่ด้วย แต่อะไรคือความแตกต่างระหว่างคุณสมบัติทั้งสองนี้? กลับไปที่ตัวจัดการข้อผิดพลาดและค้นหา
protocol ErrorHandler { func handle(error: Error) } extension ErrorHandler { func handle(error: Error) { print(error.localizedDescription) } } struct Handler: ErrorHandler { func handle(error: Error) { fatalError("Unexpected error occurred") } } enum ApplicationError: Error { case other } let handler: Handler = Handler() handler.handle(error: ApplicationError.other)
ผลที่ได้คือข้อผิดพลาดร้ายแรง
ตอนนี้ลบการประกาศเมธอด handle(error: Error)
ออกจากโปรโตคอล
protocol ErrorHandler { }
ผลลัพธ์จะเหมือนกัน: ข้อผิดพลาดร้ายแรง
หมายความว่าไม่มีความแตกต่างระหว่างการเพิ่มการใช้งานเริ่มต้นของวิธีการโปรโตคอลและการเพิ่มการใช้งานวิธีการใหม่ให้กับโปรโตคอลหรือไม่
ไม่! มีความแตกต่างกัน และคุณสามารถดูได้โดยการเปลี่ยนประเภทของ handler
ตัวแปรจาก Handler
เป็น ErrorHandler
let handler: ErrorHandler = Handler()
ตอนนี้ผลลัพธ์ไปยังคอนโซลคือ: ไม่สามารถดำเนินการให้เสร็จสิ้นได้ (ApplicationError ข้อผิดพลาด 0)
แต่ถ้าเราส่งคืนการประกาศของวิธีจัดการ (ข้อผิดพลาด: ข้อผิดพลาด) ไปยังโปรโตคอล ผลลัพธ์จะเปลี่ยนกลับเป็นข้อผิดพลาดร้ายแรง
protocol ErrorHandler { func handle(error: Error) }
ลองดูลำดับของสิ่งที่เกิดขึ้นในแต่ละกรณี
เมื่อมีการประกาศเมธอดในโปรโตคอล:
โปรโตคอลประกาศวิธี handle(error: Error)
และจัดเตรียมการใช้งานเริ่มต้น วิธีการนี้ถูกแทนที่ในการใช้งาน Handler
ดังนั้น การใช้งานเมธอดที่ถูกต้องจึงถูกเรียกใช้ขณะรันไทม์ โดยไม่คำนึงถึงชนิดของตัวแปร
เมื่อไม่มีการประกาศเมธอดในโปรโตคอล:
เนื่องจากวิธีการไม่ได้ประกาศในโปรโตคอล ชนิดจึงไม่สามารถแทนที่ได้ นั่นคือเหตุผลที่การใช้วิธีการที่เรียกว่าขึ้นอยู่กับชนิดของตัวแปร
หากตัวแปรเป็นประเภท Handler
จะมีการเรียกใช้เมธอดการใช้จากประเภท ในกรณีที่ตัวแปรเป็นประเภท ErrorHandler
จะเรียกใช้เมธอดการใช้จากส่วนขยายโปรโตคอล
รหัสที่เน้นโปรโตคอล: ปลอดภัยแต่แสดงออกได้
ในบทความนี้ เราได้สาธิตพลังของส่วนขยายโปรโตคอลบางส่วนใน Swift
ไม่เหมือนกับภาษาการเขียนโปรแกรมอื่นๆ ที่มีอินเทอร์เฟซ Swift ไม่จำกัดโปรโตคอลที่มีข้อจำกัดที่ไม่จำเป็น Swift ทำงานกับลักษณะนิสัยทั่วไปของภาษาการเขียนโปรแกรมเหล่านั้นโดยอนุญาตให้นักพัฒนาแก้ไขความกำกวมตามความจำเป็น
ด้วยโปรโตคอล Swift และส่วนขยายโปรโตคอล โค้ดที่คุณเขียนสามารถสื่อความหมายได้เหมือนกับภาษาโปรแกรมไดนามิกส่วนใหญ่ และยังปลอดภัยต่อการพิมพ์ในขณะคอมไพล์ วิธีนี้ช่วยให้คุณมั่นใจได้ว่าโค้ดของคุณสามารถนำมาใช้ซ้ำได้และบำรุงรักษาได้ และทำการเปลี่ยนแปลงโค้ดเบสของแอป Swift ได้อย่างมั่นใจมากขึ้น
เราหวังว่าบทความนี้จะเป็นประโยชน์กับคุณและยินดีรับข้อเสนอแนะหรือข้อมูลเชิงลึกเพิ่มเติม