วิธีเข้าถึง Wrappers สำหรับคุณสมบัติ Swift

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

กล่าวอย่างง่าย ๆ ว่า wrapper คุณสมบัติเป็นโครงสร้างทั่วไปที่สรุปการเข้าถึงแบบอ่านและเขียนไปยังคุณสมบัติ และเพิ่มลักษณะการทำงานเพิ่มเติมเข้าไป เราใช้มันหากต้องการจำกัดค่าคุณสมบัติที่มีอยู่ เพิ่มตรรกะพิเศษในการเข้าถึงแบบอ่าน/เขียน (เช่น การใช้ฐานข้อมูลหรือค่าเริ่มต้นของผู้ใช้) หรือเพิ่มวิธีการเพิ่มเติมบางอย่าง

Wrappers คุณสมบัติใน Swift 5.1

บทความนี้เกี่ยวกับแนวทางใหม่ของ Swift 5.1 ในการห่อคุณสมบัติ ซึ่งแนะนำไวยากรณ์ใหม่ที่สะอาดกว่า

แนวทางเก่า

ลองนึกภาพว่าคุณกำลังพัฒนาแอปพลิเคชัน และคุณมีวัตถุที่มีข้อมูลโปรไฟล์ผู้ใช้

 struct Account { var firstName: String var lastName: String var email: String? } let account = Account(firstName: "Test", lastName: "Test", email: "[email protected]") account.email = "[email protected]" print(account.email)

คุณต้องการเพิ่มการยืนยันอีเมล หากที่อยู่อีเมลของผู้ใช้ไม่ถูกต้อง คุณสมบัติของ email จะต้องเป็น nil จะเป็นกรณีที่ดีที่จะใช้ wrapper คุณสมบัติเพื่อสรุปตรรกะนี้

 struct Email<Value: StringProtocol> { private var _value: Value? init(initialValue value: Value?) { _value = value } var value: Value? { get { return validate(email: _value) ? _value : nil } set { _value = newValue } } private func validate(email: Value?) -> Bool { guard let email = email else { return false } let regex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-za-z]{2,64}" let pred = NSPredicate(format: "SELF MATCHES %@", regex) return pred.evaluate(with: email) } }

เราสามารถใช้ wrapper นี้ในโครงสร้างบัญชี:

 struct Account { var firstName: String var lastName: String var email: Email<String> }

ตอนนี้ เรามั่นใจว่าคุณสมบัติอีเมลสามารถมีได้เฉพาะที่อยู่อีเมลที่ถูกต้องเท่านั้น

ทุกอย่างดูดี ยกเว้นไวยากรณ์

 let account = Account(firstName: "Test", lastName: "Test", email: Email(initialValue: "[email protected]")) account.email.value = "[email protected]" print(account.email.value)

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

วิธีใหม่: คำอธิบายประกอบ @propertyWrapper

Swift 5.1 มอบโซลูชันที่หรูหรากว่าในการสร้าง Wrapper คุณสมบัติ ซึ่งอนุญาตให้ทำเครื่องหมาย Wrapper คุณสมบัติด้วยคำอธิบายประกอบ @propertyWrapper Wrapper ดังกล่าวมีรูปแบบที่กะทัดรัดกว่าเมื่อเทียบกับแบบดั้งเดิม ส่งผลให้โค้ดมีขนาดกะทัดรัดและเข้าใจได้ง่ายขึ้น คำอธิบายประกอบ @propertyWrapper มีข้อกำหนดเพียงข้อเดียว: อ็อบเจ็กต์ wrapper ของคุณต้องมีคุณสมบัติที่ไม่คงที่ที่เรียกว่า wrappedValue

 @propertyWrapper struct Email<Value: StringProtocol> { var value: Value? var wrappedValue: Value? { get { return validate(email: value) ? value : nil } set { value = newValue } } private func validate(email: Value?) -> Bool { guard let email = email else { return false } let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" let emailPred = NSPredicate(format:"SELF MATCHES %@", emailRegEx) return emailPred.evaluate(with: email) } }

ในการกำหนดคุณสมบัติที่ถูกห่อดังกล่าวในโค้ด เราจำเป็นต้องใช้ไวยากรณ์ใหม่

 @Email var email: String?

ดังนั้นเราจึงทำเครื่องหมายพร็อพเพอร์ตี้ด้วยคำอธิบายประกอบ @ . ประเภทพร็อพเพอร์ตี้ต้องตรงกับประเภท Wrapper ที่ "ห่อค่า" ตอนนี้คุณสามารถทำงานกับคุณสมบัตินี้ได้เช่นเดียวกับคุณสมบัติทั่วไป

 email = "[email protected]" print(email) // [email protected] email = "invalid" print(email) // nil

เยี่ยม ตอนนี้ดูดีกว่าวิธีการแบบเก่า แต่การใช้ wrapper ของเรามีข้อเสียอย่างหนึ่ง: ไม่อนุญาตให้มีค่าเริ่มต้นสำหรับค่าที่ห่อ

 @Email var email: String? = "[email protected]" //compilation error.

ในการแก้ไขปัญหานี้ เราต้องเพิ่มตัวเริ่มต้นต่อไปนี้ในเครื่องห่อ:

 init(wrappedValue value: Value?) { self.value = value }

และนั่นแหล่ะ

 @Email var email: String? = "[email protected]" print(email) // [email protected] @Email var email: String? = "invalid" print(email) // nil

รหัสสุดท้ายของ wrapper อยู่ด้านล่าง:

 @propertyWrapper struct Email<Value: StringProtocol> { var value: Value? init(wrappedValue value: Value?) { self.value = value } var wrappedValue: Value? { get { return validate(email: value) ? value : nil } set { value = newValue } } private func validate(email: Value?) -> Bool { guard let email = email else { return false } let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" let emailPred = NSPredicate(format:"SELF MATCHES %@", emailRegEx) return emailPred.evaluate(with: email) } }

Wrappers ที่กำหนดค่าได้

ลองมาอีกตัวอย่างหนึ่ง คุณกำลังเขียนเกม และคุณมีคุณสมบัติที่จะเก็บคะแนนของผู้ใช้ไว้ ข้อกำหนดคือค่านี้ควรมากกว่าหรือเท่ากับ 0 และน้อยกว่าหรือเท่ากับ 100 คุณสามารถบรรลุสิ่งนี้ได้โดยใช้ตัวห่อหุ้มคุณสมบัติ

 @propertyWrapper struct Scores { private let minValue = 0 private let maxValue = 100 private var value: Int init(wrappedValue value: Int) { self.value = value } var wrappedValue: Int { get { return max(min(value, maxValue), minValue) } set { value = newValue } } } @Scores var scores: Int = 0

รหัสนี้ใช้งานได้ แต่ดูเหมือนไม่ธรรมดา คุณไม่สามารถนำกลับมาใช้ใหม่ได้โดยมีข้อจำกัดต่างกัน (ไม่ใช่ 0 และ 100) นอกจากนี้ยังสามารถจำกัดเฉพาะค่าจำนวนเต็มเท่านั้น จะดีกว่าถ้ามีตัวห่อหุ้มที่กำหนดค่าได้หนึ่งตัวที่สามารถจำกัดประเภทใดก็ได้ที่สอดคล้องกับโปรโตคอลที่เปรียบเทียบได้ ในการทำให้แรปเปอร์ของเราสามารถกำหนดค่าได้ เราจำเป็นต้องเพิ่มพารามิเตอร์การกำหนดค่าทั้งหมดผ่านตัวเริ่มต้น หากตัวเริ่มต้นมีแอตทริบิวต์ wrappedValue (ค่าเริ่มต้นของคุณสมบัติของเรา) จะต้องเป็นพารามิเตอร์แรก

 @propertyWrapper struct Constrained<Value: Comparable> { private var range: ClosedRange<Value> private var value: Value init(wrappedValue value: Value, _ range: ClosedRange<Value>) { self.value = value self.range = range } var wrappedValue: Value { get { return max(min(value, range.upperBound), range.lowerBound) } set { value = newValue } } }

ในการเริ่มต้นคุณสมบัติที่ห่อหุ้ม เรากำหนดแอตทริบิวต์การกำหนดค่าทั้งหมดในวงเล็บหลังคำอธิบายประกอบ

 @Constrained(0...100) var scores: Int = 0

จำนวนของแอตทริบิวต์การกำหนดค่าไม่จำกัด คุณต้องกำหนดพวกมันในวงเล็บในลำดับเดียวกับตัวเริ่มต้น

เข้าถึง Wrapper ได้เอง

หากคุณต้องการเข้าถึงตัวห่อหุ้ม (ไม่ใช่ค่าที่ห่อหุ้ม) คุณต้องเพิ่มขีดล่างก่อนชื่อคุณสมบัติ ตัวอย่างเช่น มาดูโครงสร้างบัญชีของเรากัน

 struct Account { var firstName: String var lastName: String @Email var email: String? } let account = Account(firstName: "Test", lastName: "Test", email: "[email protected]") account.email // Wrapped value (String) account._email // Wrapper(Email<String>)

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

 extension Account: Equatable { static func ==(lhs: Account, rhs: Account) -> Bool { return lhs.email?.lowercased() == rhs.email?.lowercased() } }

ใช้งานได้ แต่ไม่ใช่ทางออกที่ดีที่สุดเพราะเราต้องไม่ลืมเพิ่มวิธีพิมพ์เล็ก () ทุกที่ที่เราเปรียบเทียบอีเมล วิธีที่ดีกว่าคือทำให้โครงสร้างอีเมลมีความเท่าเทียมกัน:

 extension Email: Equatable { static func ==(lhs: Email, rhs: Email) -> Bool { return lhs.wrappedValue?.lowercased() == rhs.wrappedValue?.lowercased() } }

และเปรียบเทียบ wrappers แทนค่าที่ห่อหุ้ม:

 extension Account: Equatable { static func ==(lhs: Account, rhs: Account) -> Bool { return lhs._email == rhs._email } }

มูลค่าประมาณการ

คำอธิบายประกอบ @propertyWrapper ให้ไวยากรณ์น้ำตาลอีกหนึ่งค่า - ค่าที่คาดการณ์ไว้ คุณสมบัตินี้สามารถมีได้ทุกประเภทที่คุณต้องการ ในการเข้าถึงคุณสมบัตินี้ คุณต้องเพิ่มคำนำหน้า $ ให้กับชื่อคุณสมบัติ เพื่ออธิบายวิธีการทำงาน เราใช้ตัวอย่างจากเฟรมเวิร์กการรวม

Wrapper คุณสมบัติ @Published สร้างผู้เผยแพร่สำหรับคุณสมบัติและส่งกลับเป็นค่าที่คาดการณ์ไว้

 @Published var message: String print(message) // Print the wrapped value $message.sink { print($0) } // Subscribe to the publisher

อย่างที่คุณเห็น เราใช้ข้อความเพื่อเข้าถึงคุณสมบัติที่ถูกห่อ และ $message เพื่อเข้าถึงผู้เผยแพร่ คุณควรทำอย่างไรเพื่อเพิ่มมูลค่าที่คาดการณ์ไว้ให้กับเสื้อคลุมของคุณ? ไม่มีอะไรพิเศษ แค่ประกาศ

 @propertyWrapper struct Published<Value> { private let subject = PassthroughSubject<Value, Never>() var wrappedValue: Value { didSet { subject.send(wrappedValue) } } var projectedValue: AnyPublisher<Value, Never> { subject.eraseToAnyPublisher() } }

ตามที่ระบุไว้ก่อนหน้านี้ คุณสมบัติ projectedValue สามารถมีชนิดใดก็ได้ตามความต้องการของคุณ

ข้อจำกัด

ไวยากรณ์ของ wrappers คุณสมบัติใหม่นั้นดูดี แต่ก็มีข้อจำกัดหลายประการ หลักๆ คือ:

  1. พวกเขาไม่สามารถมีส่วนร่วมในการจัดการข้อผิดพลาด ค่าที่ห่อเป็นคุณสมบัติ (ไม่ใช่วิธีการ) และเราไม่สามารถทำเครื่องหมาย getter หรือ setter เป็น throws ตัวอย่างเช่น ในตัวอย่าง Email ของเรา เป็นไปไม่ได้ที่จะสร้างข้อผิดพลาดหากผู้ใช้พยายามตั้งค่าอีเมลที่ไม่ถูกต้อง เราสามารถคืนค่า nil หรือทำให้แอปขัดข้องด้วยการเรียก fatalError() ซึ่งอาจไม่เป็นที่ยอมรับในบางกรณี
  2. ไม่อนุญาตให้ใช้เครื่องห่อหลายรายการกับพร็อพเพอร์ตี้ ตัวอย่างเช่น ควรใช้ wrapper @CaseInsensitive แยกต่างหากและรวมเข้ากับ wrapper @Email แทนที่จะทำให้ @Email wrapper ไม่สำคัญ แต่สิ่งปลูกสร้างเช่นนี้เป็นสิ่งต้องห้ามและนำไปสู่ข้อผิดพลาดในการรวบรวม
 @CaseInsensitive @Email var email: String?

วิธีแก้ปัญหาสำหรับกรณีนี้โดยเฉพาะ เราสามารถสืบทอด Wrapper Email จาก Wrapper CaseInsensitive อย่างไรก็ตาม การสืบทอดก็มีข้อจำกัดเช่นกัน มีเพียงคลาสที่รองรับการสืบทอด และคลาสฐานเดียวเท่านั้นที่ได้รับอนุญาต

บทสรุป

คำอธิบายประกอบ @propertyWrapper ช่วยลดความซับซ้อนของไวยากรณ์ของตัวตัดคุณสมบัติ และเราสามารถดำเนินการกับคุณสมบัติที่ห่อในลักษณะเดียวกับคุณสมบัติทั่วไป สิ่งนี้ทำให้โค้ดของคุณในฐานะนักพัฒนา Swift กระชับและเข้าใจได้ง่ายขึ้น ในขณะเดียวกันก็มีข้อจำกัดหลายประการที่เราต้องคำนึงถึง ฉันหวังว่าบางส่วนจะได้รับการแก้ไขในเวอร์ชัน Swift ในอนาคต

หากคุณต้องการเรียนรู้เพิ่มเติมเกี่ยวกับคุณสมบัติของ Swift โปรดดูเอกสารอย่างเป็นทางการ