“一人得道,雨燕升天”:Swift 协议扩展助力 CoreData 托管类型(下)

164 阅读5分钟

在这里插入图片描述

概述

相信各位似秃非秃小码农们都同意,Swift 是一门现代化、安全且表现力足够丰富的语言。不过,它毕竟还是一种偏静态的语言,灵活性无法和 Python、ruby 之类的动态语言相提并论。

在这里插入图片描述

不过话虽如此,通过巧妙的一步步重构源代码,我们也可以用 Swift 完成之前貌似不可能完成的任务,所需的只是那么一丢丢耐心和执着而已。

在本篇博文中,您将学到如下内容:

  1. 一种很“硬”的解决方案
  2. 不想回到最初的样子
  3. 让编译器乖乖听话

希望在亲眼目睹本系列文章中 Swift 代码那循序渐进的重构和升华之后,小伙伴们倘若再遇到与此类似的语言设计问题,必能胸有成竹、胜券在握!

无需等待,Let‘s go!!!;)


5. 一种很“硬”的解决方案

对于前文中的问题,一种简单粗暴的解决方法是:强行让两种类型“蛮来生作”。

extension AchievementEvaluator {
    static func queryAll(context: NSManagedObjectContext) throws -> [Evaluator] {
        let request = Evaluator.fetchRequest() as! NSFetchRequest<Evaluator> // ⚠️ 强制转换
        return try context.fetch(request)
    }
}

如您所见,我们通过 Swift 强制类型转换语法,将 Evaluator.fetchRequest 实际的类型与 Evaluator 类型强行匹配。

虽然,这可以让编译器暂时闭嘴,但是也同时置我们自己于“刀山火海”之上!

上述代码的风险是:我们需要自行确保类型转换的安全性,若 Evaluator.fetchRequest() 实际返回的请求类型与 Evaluator 不匹配,将立即导致运行时发生崩溃。

6. 不想回到最初的样子

除了强行转换以外,我们还可以采用迂回战术:创建约束协议从而绕过编译器的“桎梏”。

首先,新建一个约束协议 Fetchable:

// 定义核心约束协议
protocol Fetchable: NSManagedObject {
    static func fetchRequest() -> NSFetchRequest<Self>
}

接着,对原来的 AchievementEvaluator 协议定义稍作调整,让其关联类型遵守我们上面创建的约束协议:

// 原协议调整
protocol AchievementEvaluator {
    associatedtype Evaluator: Fetchable & AchievementEvaluator // 新增 Fetchable 约束
    
    static func queryAll(context: NSManagedObjectContext) throws -> [Evaluator]
}

随后,在 AchievementEvaluator 协议扩展中利用约束关系重新打造我们的 queryAll() 方法:

extension AchievementEvaluator where Evaluator: Fetchable {
    static func queryAll(context: NSManagedObjectContext) throws -> [Evaluator] {
        let request = Evaluator.fetchRequest() // ✅ 类型已明确为 NSFetchRequest<Evaluator>
        return try context.fetch(request)
    }
}

最后,让 Achv_NoBreakVictory 成就实体类遵守 Fetchable 约束协议即可:

extension Achv_NoBreakVictory: Fetchable, AchievementEvaluator {
    typealias Evaluator = Achv_NoBreakVictory
}

虽然这种思路本身没什么问题,但可惜的是编译器还是会义无反顾的再次大声说“我恨你!”:

在这里插入图片描述

Protocol 'Fetchable' requirement 'fetchRequest()' cannot be satisfied by a non-final class ('Achv_NoBreakVictory') because it uses 'Self' in a non-parameter, non-result type position

通过上面的错误信息不难发现:大家貌似又回到了之前的“故步自封”—— 我们仍然需要让 Achv_NoBreakVictory 类加上 final 成为“孤家寡人”才能得偿所愿,这是我们不希望看到的。

所以,我们又该如何随遇而安呢?

7. 让编译器乖乖听话

其实,解决之道并没有想象的那么复杂,我们只需重新设计 Fetchable 协议即可。

我们的核心思想是:

机制作用
entityName 属性动态获取实体名称,避免依赖自动生成的 fetchRequest()
手动构建 NSFetchRequest通过 NSFetchRequest<Self>(entityName:) 确保类型匹配
子类覆盖 entityName允许继承体系中的子类指定自己的实体名称

首先,通过 实体名称动态构建请求,绕过自动生成的 fetchRequest() 方法的限制:

protocol Fetchable: NSManagedObject {
    static var entityName: String { get } // 要求实体提供名称
}

extension Fetchable {
    static func fetchRequest() -> NSFetchRequest<Self> {
        // 手动构建请求,确保类型安全
        return NSFetchRequest<Self>(entityName: entityName)
    }
}

接下来,我们只要让 Achv_NoBreakVictory 类乖巧的提供 entityName 名称即可:

extension Achv_NoBreakVictory: Fetchable, AchievementEvaluator {
    static var entityName: String {
        "Achv_NoBreakVictory"
    }
    
    typealias Evaluator = Achv_NoBreakVictory
}

现在,编译源代码将如您所愿,一切都毫无问题,整个世界清净了!

通过 动态实体名称 + 手动构建请求,既能保持类的可继承性,又能满足 Core Data 类型安全要求。其关键点在于:

  1. 通过 entityName 属性解耦实体名称与类型推断。
  2. 子类必须显式覆盖 entityName 以正确映射数据库实体。

然而,我们还可以更进一步。

观察上面 Achv_NoBreakVictory 类中对应 entityName 属性的代码可以发现:每个成就实体类的 entityName 就是它们自己类的名称。既然如此,为什么不把 entityName 也直接放到协议扩展中去呢?

extension AchievementEvaluator where Evaluator: Fetchable {
    
    static var entityName: String {
        "\(Self.self)"
    }
    
    static func queryAll(context: NSManagedObjectContext) throws -> [Evaluator] {
        let request = Evaluator.fetchRequest() // ✅ 类型已明确为 NSFetchRequest<Evaluator>
        return try context.fetch(request)
    }
}

如上代码所示,我们将原本需要每个 AchievementEvaluator 实体类实现的 entityName 属性放到了 AchievementEvaluator 协议扩展中,大大减少了重复代码,这样的 DRY 和 KISS 谁能不爱呢?棒棒哒!

或者我们干脆彻底摆脱 entityName 属性的限制,直接将其嵌入到 Fetchable 协议扩展的 fetchRequest() 方法中,让实现百尺竿头、更入佳境:

extension Fetchable {
    static func fetchRequest() -> NSFetchRequest<Self> {
        // 手动构建请求,确保类型安全
        return NSFetchRequest<Self>(entityName: "\(Self.self)")
    }
}

至此,我们通过不断迭代重构,彻底摆脱了最初文章开头 CoreData 成就托管类实现的恼人纠缠,小伙伴们还不赶快给自己一个大大的赞吧!❤️

总结

在本篇博文中,我们借助于精心设计的 Fetchable 约束协议成功的摆脱了 Swift 协议扩展中的“磨搅讹绷”,小伙伴们值得拥有!

感谢观赏,再会啦!8-)