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 基类 + 子类多态场景在运行时发生类型不匹配崩溃的两种解决方案,并逐一做了深入地讨论。

感谢观赏,再会啦!😎

相关推荐
YungFan10 小时前
SwiftUI-自定义与扩展
swiftui·swift
东坡肘子13 小时前
WWDC 2025 开发者特辑 | 肘子的 Swift 周报 #088
swiftui·swift·wwdc
大熊猫侯佩14 小时前
Swift 中强大的 Key Paths(键路径)机制趣谈(下)
swift·编程语言·apple
season_zhu14 小时前
RxSwift:dispose() 和 disposed(by:) 以及NSObject+Rx
ios·swift·rxswift
健了个平_2420 小时前
iOS 26 适配笔记
ios·swift·wwdc
大熊猫侯佩1 天前
Swift 协议扩展精进之路:解决 CoreData 托管实体子类的类型不匹配问题(上)
swift·协议·protocol·coredata·协议扩展·托管基类·协议关联类型
我现在不喜欢coding1 天前
SwiftUI何时为值类型的视图提供持久标识
swiftui·swift
songgeb1 天前
viewWillAppear与viewWillDisappear不匹配问题
ios·objective-c·swift