Swift 协议扩展精进之路:解决 CoreData 托管实体子类的类型不匹配问题(下)

概述

在 Swift 开发语言中,各位秃头小码农们可以充分利用语法本身所带来的便利去劈荆斩棘。我们还可以恣意利用泛型、协议关联类型和协议扩展来进一步简化和优化我们复杂的代码需求。

不过,在涉及到多个子类派生于基类进行多态模拟的场景下,稍不留神可能就会产生恢诡谲怪的错误。这是怎么回事?又该如何解决呢?

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

  • 概述
  • [3. 一种略显麻烦的解决](#3. 一种略显麻烦的解决)
    • [3.1 步骤 1:修改 `Fetchable` 协议,增加类型过滤支持](#3.1 步骤 1:修改 Fetchable 协议,增加类型过滤支持)
    • [3.2 步骤 2:在 Core Data 模型中为基类 `Achievement` 添加实体类型字段](#3.2 步骤 2:在 Core Data 模型中为基类 Achievement 添加实体类型字段)
    • [3.3 步骤 3:修改协议扩展,移除强制转换](#3.3 步骤 3:修改协议扩展,移除强制转换)
  • [4. 一个方法换一方清净](#4. 一个方法换一方清净)
  • 总结

在学完本课后,相信小伙伴们都会在撸码实战中重新找回自信,并向更深一层的内功修为奋勇前进!

那还等什么呢?让我们马上开始 Swift 精进之旅吧!

Let's go!!!😉


3. 一种略显麻烦的解决

通过之前的讨论,我们可以很快给出应对措施,那就是通过 实体类型过滤 + 类型安全转换 解决这种混合类型问题:

3.1 步骤 1:修改 Fetchable 协议,增加类型过滤支持

swift 复制代码
import CoreData

protocol Fetchable: Achievement {
    static var entityTypeKey: String { get } // 每个子类定义唯一标识
}

extension Fetchable {
    static func fetchRequest() -> NSFetchRequest<Self> {
        let request = NSFetchRequest<Self>(entityName: "\(Self.self)")
        // 添加类型过滤谓词,确保仅返回当前子类的实例
        request.predicate = NSPredicate(format: "entityType == %@", entityTypeKey)
        return request
    }
}

3.2 步骤 2:在 Core Data 模型中为基类 Achievement 添加实体类型字段

  1. 在 Xcode 的 Core Data 模型编辑器中:
    • 选中 Achievement 实体
    • 添加属性 entityType(类型为 String,非可选,设置默认值为空)
  2. 在每个子类初始化时自动设置 entityType 字段正确的值:
swift 复制代码
// 基类扩展
extension Achievement {
    override func awakeFromInsert() {
        super.awakeFromInsert()
        // 子类需覆盖 entityTypeKey
        self.entityType = (self as! any Fetchable).type(of: self).entityTypeKey
    }
}

// 子类实现
extension Achv_NoBreakVictory: Fetchable {
    static let entityTypeKey = "noBreakVictory"
}

extension Achv_MultipleSerialVictories: Fetchable {
    static let entityTypeKey = "multipleSerialVictories"
}

3.3 步骤 3:修改协议扩展,移除强制转换

swift 复制代码
extension AchievementEvaluator where Evaluator: Fetchable {
    static func queryAll(context: NSManagedObjectContext) throws -> [Evaluator] {
        let request = Evaluator.fetchRequest()
        let results = try context.fetch(request)
        // 安全过滤 + 转换
        return results.compactMap { $0 as? Evaluator }
    }
}

在上面的解决方案中,我们主要做了如下几点改变:

机制 作用
entityType 字段 显式标记实例所属子类类型,避免混合查询
awakeFromInsert 自动为每个新实例设置类型标识
谓词过滤 entityType == %@ 确保 fetchRequest 仅返回当前子类实例
compactMap + as? 二次类型校验,防止意外数据污染

其实,我们还可以利用 索引优化 做进一步改进:为 entityType 字段添加数据库索引,提升查询效率。


关于 CoreData 索引机制的详细研究,请小伙伴们移步如下链接观赏精彩的内容:


4. 一个方法换一方清净

不过话又说回来,上面的解决方法仍有一些让人难以忍受的地方:

  • 要在表中新增一个 entityType 字段;
  • 需要介入托管对象的 awakeFromInsert 方法做额外的操作;

那么,有没有更简单的办法呢?

答案是肯定的!

回到 Fetchable 协议扩展的实现中,既然原来的 fetchRequest() 方法根本不会被调用,我们不如另外实现一个与它同名的重载方法:

swift 复制代码
extension Fetchable {
    static func fetchRequest(entityName: String) -> NSFetchRequest<Self> {
        NSFetchRequest<Self>(entityName: "\(Self.self)")
    }
}

可以看到,在新的 fetchRequest(entityName: String) 方法里,我们明确使用了子类的名称来创建正确的查询请求。

最后,再回到 AchievementEvaluator 的协议扩展里,替换原来两个方法中的 fetchRequest() 为新的 fetchRequest(entityName: String) 方法:

swift 复制代码
extension AchievementEvaluator where Evaluator: Fetchable {
    
    static func calcCount(context: NSManagedObjectContext) throws -> Int {
        let req = Evaluator.fetchRequest(entityName: "\(Self.self)")
        return try context.count(for: req)
    }
    
    static func queryAll(context: NSManagedObjectContext) throws -> [Evaluator] {
        let req = Evaluator.fetchRequest(entityName: "\(Self.self)")
        req.sortDescriptors = [
            .init(keyPath: \Achievement.orderNumber, ascending: true)
        ]
        // return try context.fetch(req) as! [Evaluator]
        return try context.fetch(req)
    }
}

如此一来,使用新的 fetchRequest(entityName: String) 方法不仅在调用时消除了成就基类和多个实体成就子类之间的转换歧义,而且在 queryAll() 方中,原本对于查询结果的强制转换代码也已不再需要,这进一步简化了代码逻辑并提高了运行安全,棒棒哒!💯

总结

在本篇博文中,我们介绍了利用 Swift 协议扩展解决 CoreData 基类 + 子类多态场景在运行时发生类型不匹配崩溃的两种解决方案,并逐一做了深入地讨论。

感谢观赏,再会啦!😎

相关推荐
小牛不牛的程序员21 小时前
开源一个 macOS 剪贴板「意图识别」工具——不只是存储,更是理解你复制了什么
macos·swift
apihz2 天前
经纬度制作高清卫星图片免费 API 接口详解
开发语言·ios·swift
00后程序员张3 天前
iOS 打包方式汇总 从 Xcode Archive 到轻量级工具链的 IPA 构建
ide·vscode·ios·objective-c·个人开发·swift·敏捷流程
sweet丶3 天前
Swift 黑魔法:`@dynamicMemberLookup`
swift·rxswift
大熊猫侯佩3 天前
SwiftUI 侦探笔记:iPhone 14 失踪的“刘海”与侯佩的像素诡计
swiftui·swift·apple
大熊猫侯佩3 天前
WWDC26 新 @ContentBuilder 修饰符:SwiftUI 终于有了户口本
swiftui·swift·wwdc
唐诺4 天前
OC中协议、分类、扩展:核心对比与总结
extension·protocol·category
东坡肘子4 天前
当 Linux 成为“空气”:容器、Agent 与不再重要的“桌面之争” -- 肘子的 Swift 周报 #143
人工智能·swiftui·swift
wabil6 天前
【LVGL】滑动切换页面的界面优化实践
开发语言·ios·swift
东坡肘子11 天前
SPI 加入 Apple,Swift 迈向自举 -- 肘子的 Swift 周报 #142
人工智能·swiftui·swift