Swift:如何让公开协议属性在实现类中设为private(set)并强制规范设置?
问题
我有一个用于NSManagedObject的Nameable协议,该协议在独立的Swift包中声明,需公开供其他多个包导入,因此协议必须是公开的。我还需要坚持使用协议方案而非基类继承。我希望通过默认实现来验证名称并抛出错误,但似乎无法强制开发者使用set(_ name: String)方法,而非直接修改name属性。
原协议代码:
import CoreData public protocol Nameable: NSManagedObject { /// The name property of the entity /// - Warning: **don't** set this directly, use `set(_ name: String)` to ensure the name is validated /// /// - Note: This needs to be an `@NSManaged` var name: String { get set } static var defaultName: String { get } static var maxNameLength: Int { get } func set(_ name: String) throws } public extension Nameable { // TODO: Localize static var defaultName: String { "Untitled" } static var maxNameLength: Int { 128 } func set(_ name: String) throws { guard !name.isEmpty else { throw NameError.nameEmpty } guard name.count <= Self.maxNameLength else { throw NameError.nameTooLong } self.name = name try managedObjectContext?.save() } } public enum NameError: Error { case nameEmpty case nameTooLong }
我希望按如下方式使用该协议:
@objc(MyObject) class MyObject: NSManagedObject, Nameable { @NSManaged public private(set) var name: String }
但由于协议是公开的,name属性也必须是公开可写的。我想到的唯一解决方案都是“软方案”(即注释警告,或类似@NSManaged var unsafeName: String { get set }的方式)。是否存在可由编译器强制执行的方法来实现预期效果?
编译器强制执行的解决方案
1. 拆分属性权限:对外只读 + 内部可写
修改协议定义,将name拆分为对外只读的公开属性,以及仅在包内可见的可写内部属性,通过协议内部扩展约束实现类必须提供该内部属性:
import CoreData public protocol Nameable: NSManagedObject { /// 对外只读的实体名称 var name: String { get } static var defaultName: String { get } static var maxNameLength: Int { get } func set(_ name: String) throws } // 包内可见的内部扩展,定义供默认实现使用的可写属性 internal extension Nameable { var _name: String { get set } } public extension Nameable { static var defaultName: String { "Untitled" } static var maxNameLength: Int { 128 } func set(_ name: String) throws { guard !name.isEmpty else { throw NameError.nameEmpty } guard name.count <= Self.maxNameLength else { throw NameError.nameTooLong } self._name = name try managedObjectContext?.save() } } public enum NameError: Error { case nameEmpty case nameTooLong }
实现类中,将name作为只读属性返回内部的_name,同时限制_name的访问权限:
@objc(MyObject) class MyObject: NSManagedObject, Nameable { // 对外只读的name属性 public var name: String { _name } // 包内可写的@NSManaged属性,仅协议默认实现能访问 @NSManaged internal var _name: String }
外部代码无法直接赋值name(编译器会提示只读),也无法访问_name(包外不可见),必须通过set(_:)方法修改名称,完全由编译器强制执行。
2. 使用属性包装器封装验证逻辑
创建公开的属性包装器,将名称验证逻辑内置其中,协议要求实现类必须使用该包装器修饰name属性:
import CoreData @propertyWrapper public struct ValidatedName { private var value: String private let maxLength: Int public init(wrappedValue: String, maxLength: Int = 128) { self.maxLength = maxLength do { try validate(wrappedValue) self.value = wrappedValue } catch { fatalError(error.localizedDescription) } } public var wrappedValue: String { get { value } set { do { try validate(newValue) value = newValue if let context = (self.enclosingInstance as? NSManagedObject)?.managedObjectContext { try context.save() } } catch { fatalError(error.localizedDescription) } } } // 关联宿主实例,用于访问CoreData上下文 private weak var enclosingInstance: NSObject? public mutating func setUpEnclosingInstance(_ instance: NSObject) { enclosingInstance = instance } private func validate(_ name: String) throws { guard !name.isEmpty else { throw NameError.nameEmpty } guard name.count <= maxLength else { throw NameError.nameTooLong } } } public protocol Nameable: NSManagedObject { @ValidatedName var name: String { get set } static var defaultName: String { get } static var maxNameLength: Int { get } func set(_ name: String) throws } public extension Nameable { static var defaultName: String { "Untitled" } static var maxNameLength: Int { 128 } func set(_ name: String) throws { // 直接赋值即可触发属性包装器的验证逻辑 self.name = name } } public enum NameError: Error { case nameEmpty case nameTooLong }
实现类中使用属性包装器修饰name:
@objc(MyObject) class MyObject: NSManagedObject, Nameable { @ValidatedName(maxLength: MyObject.maxNameLength) @NSManaged public var name: String // 初始化时关联宿主实例 override public init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) { super.init(entity: entity, insertInto: context) _name.setUpEnclosingInstance(self) } }
外部代码无论直接赋值name还是调用set(_:),都会触发属性包装器的验证逻辑,编译器强制实现类必须使用该包装器,否则无法符合协议。
内容的提问来源于stack exchange,提问作者hidden-username
相关产品推荐
相关产品推荐

