You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 05:55:16