บทนำสู่การเขียนโปรแกรมเชิงโปรโตคอลใน Swift

เผยแพร่แล้ว: 2022-03-11

โปรโตคอลเป็นคุณลักษณะที่มีประสิทธิภาพมากของภาษาโปรแกรม Swift

โปรโตคอลใช้เพื่อกำหนด "พิมพ์เขียวของวิธีการ คุณสมบัติ และข้อกำหนดอื่นๆ ที่เหมาะสมกับงานหรือฟังก์ชันเฉพาะ"

Swift ตรวจสอบปัญหาความสอดคล้องของโปรโตคอลในเวลาคอมไพล์ ทำให้นักพัฒนาสามารถค้นพบข้อบกพร่องร้ายแรงบางอย่างในโค้ดได้ก่อนจะรันโปรแกรม โปรโตคอลช่วยให้นักพัฒนาเขียนโค้ดที่ยืดหยุ่นและขยายได้ใน 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) }

ส่วนขยายโปรโตคอล

ส่วนขยายโปรโตคอลสร้างขึ้นจากความยอดเยี่ยมของโปรโตคอล พวกเขาอนุญาตให้เรา:

  1. จัดเตรียมการใช้งานเริ่มต้นของวิธีโปรโตคอลและค่าดีฟอลต์ของคุณสมบัติของโปรโตคอล ซึ่งทำให้เป็น "ทางเลือก" ประเภทที่สอดคล้องกับโปรโตคอลสามารถจัดเตรียมการใช้งานของตนเองหรือใช้ค่าเริ่มต้นได้

  2. เพิ่มการใช้งานวิธีการเพิ่มเติมที่ไม่ได้อธิบายไว้ในโปรโตคอล และ "ตกแต่ง" ประเภทใดๆ ที่สอดคล้องกับโปรโตคอลด้วยวิธีการเพิ่มเติมเหล่านี้ คุณลักษณะนี้ช่วยให้เราสามารถเพิ่มวิธีการเฉพาะสำหรับหลายประเภทที่สอดคล้องกับโปรโตคอลแล้วโดยไม่ต้องแก้ไขแต่ละประเภททีละรายการ

การนำวิธีการเริ่มต้นไปใช้

มาสร้างอีกหนึ่งโปรโตคอลกัน:

 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 หากไม่มีส่วนขยายโปรโตคอล เราจะต้องเพิ่มวิธีนี้ในแต่ละประเภทแยกกัน

ส่วนขยายโปรโตคอลเทียบกับคลาสพื้นฐาน

ส่วนขยายโปรโตคอลอาจดูคล้ายกับการใช้คลาสพื้นฐาน แต่มีประโยชน์หลายประการของการใช้ส่วนขยายโปรโตคอล ซึ่งรวมถึงแต่ไม่จำกัดเฉพาะ:

  1. เนื่องจากคลาส โครงสร้าง และ enum สามารถสอดคล้องกับโปรโตคอลมากกว่าหนึ่งโปรโตคอล จึงสามารถใช้การนำโปรโตคอลหลายตัวไปใช้งานโดยปริยายได้ แนวคิดนี้คล้ายกับการสืบทอดหลายภาษาในภาษาอื่นๆ

  2. โปรโตคอลสามารถนำมาใช้โดยคลาส โครงสร้าง และ 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 ได้อย่างมั่นใจมากขึ้น

เราหวังว่าบทความนี้จะเป็นประโยชน์กับคุณและยินดีรับข้อเสนอแนะหรือข้อมูลเชิงลึกเพิ่มเติม