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

概述

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

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

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

  • 概述
  • [1. 完美世界崩塌了!](#1. 完美世界崩塌了!)
  • [2. 刨根问底:问题根源之所在](#2. 刨根问底:问题根源之所在)
  • 总结

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

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

Let's go!!!😉


1. 完美世界崩塌了!

在之前的这两篇博文里:

我们已经详细讨论过了,如何借助于精心设计的 Fetchable 约束协议成功摆脱 Swift 协议扩展中的"磨搅讹绷"。

其中,我们通过一步一步完善和重构代码,解决了 Swift 语言中颇为棘手的协议关联类型系统的匹配问题。


本文后续的讨论都将建立在上面两篇博文的故事和源代码之上,如果小伙伴们在接下来的旅程中有些 "云天雾地",请移步上述博文一探究竟。


让我们先帮助大家做一番简单的回忆,下面就是 App 中原有的 CoreData 数据库结构:Achievement 是成就基类,而 Achv_NoBreakVictory 作为成就实体类型派生于它:

swift 复制代码
@objc(Achievement)
public class Achievement: NSManagedObject {}

@objc(Achv_NoBreakVictory)
public class Achv_NoBreakVictory: Achievement {}

现在,我们需要为这一成就体系增加新的成就实体类型 Achv_MultipleSerialVictories:

swift 复制代码
@objc(Achv_MultipleSerialVictories)
public class Achv_MultipleSerialVictories: Achievement {}

在如法炮制让 Achv_MultipleSerialVictories 遵守 AchievementEvaluator 协议,并实现了所有相关方法之后,编译并运行代码我们会"惊恐"地发现 App "可耻的"崩溃了,提示如下:

Fatal error: NSArray element failed to match the Swift Array Element type

Expected Achv_MultipleSerialVictories but found Achv_NoBreakVictory

在 Xcode 调试器中可以看到,此崩溃发生的位置并不在一个"正经"的地方,搞得我们有些云里雾里,非常被动:

那么,到底是 App 中哪几行代码要作为"罪魁祸首",对此负责呢?

2. 刨根问底:问题根源之所在

为了找到问题的真正根源,我们需要再展示几小段代码,以补全缺失的拼图:

swift 复制代码
protocol Fetchable: Achievement {}

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

protocol AchievementEvaluator: Fetchable {
    associatedtype Evaluator: Fetchable & AchievementEvaluator

    static func spawnAll(context: NSManagedObjectContext) throws
}

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

extension Achievement {
    static func spawnIfNeed(context: NSManagedObjectContext) throws {
        try Achv_NoBreakVictory.spawnAll(context: context)
        try Achv_MultipleSerialVictories.spawnAll(context: context)
    }
}

在上面的代码中,我们主要做了这样几件事:

  • 用 Fetchable 协议作为 AchievementEvaluator 协议的约束;
  • 在 Fetchable 协议扩展中创建 fetchRequest() 方法以确保类型安全;
  • 在 AchievementEvaluator 协议扩展中创建了 calcCount() 和 queryAll() 方法,分别用来计算实体成就类型中实例的数量和查询实例的集合;
  • 在 Achievement 成就基类中创建 spawnIfNeed() 方法用来生成所有成就实体类的实例对象;

那么,问题究竟是出在哪里呢?

原来,App 的崩溃是由于 Core Data 实体继承模型与 Swift 类型系统的冲突导致的:

  1. Core Data 继承机制的特性
    Core Data 的实体继承在底层数据库中默认采用 单表继承 策略,所有子类实例都存储在基类对应的表中。当我们执行 Evaluator.fetchRequest() 时,实际上会查询基类 Achievement 的所有子类实例,导致返回数组中混合了不同子类的类型;
  2. 协议扩展的类型欺骗
    协议扩展中 queryAll()Evaluator.fetchRequest() 虽然表面上是针对子类(如 Achv_NoBreakVictory),但实际生成的 SQL 查询却是 SELECT * FROM Achievement,返回的数组元素实际类型是基类 Achievement,强制转换为错误的子类类型时必将触发崩溃;

但是先等等,我们不是已经在 Fetchable 协议扩展中的 fetchRequest() 方法里明确说明了必须按实际的子类名称来查询的吗:

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

这个疑问不难解答。

我们在上面 fetchRequest() 方法内插入断点,再次运行可以验证:fetchRequest() 方法压根就没有执行!这说明 AchievementEvaluator 协议扩展两个方法中 Evaluator.fetchRequest() 调用的根本不是 Fetchable 协议扩展中的方法,而是托管基类 Achievement 中的默认方法!

所以,这就是问题的根本原因:我们尝试对 Achievement.fetchRequest() 方法查询出来的多种成就实体类型的实例强行做类型转换,结果可想而知。

在仅有一个实体子类时这不会产生任何问题,但当我们的 Achievement 基类派生出多个成就子类时,这个潜伏着的"致命魔鬼"就会被释放出来"为祸人间"。

那么,我们此时又该何去何从呢?

在下一篇博文中,我们将继续 Swift 精进大冒险,给出两种迥然不同的解决之道,不见不散!

总结

在本篇博文中,我们讲述了利用 Swift 协议扩展试图搞定 CoreData 基类 + 子类多态场景却意外翻车的故事,随后我们深入讨论了问题的根源之所在。

感谢观赏,我们下一篇再见吧!😎

相关推荐
报错小能手11 小时前
ios开发方向——swift错误处理:do/try/catch、Result、throws
开发语言·学习·ios·swift
小夏子_riotous14 小时前
openstack的使用——5. Swift服务的基本使用
linux·运维·开发语言·分布式·云计算·openstack·swift
mCell18 小时前
MacOS 下实现 AI 操控电脑(Computer Use)的思考
macos·agent·swift
用户794572239541318 小时前
【DGCharts】iOS 图表渲染事实标准——8 种图表类型、高度可定制,3 行代码画出一条折线
swiftui·swift
chaoguo12341 天前
Any metadata 的内存布局
swift·metadata·value witness table
tangweiguo030519872 天前
SwiftUI布局完全指南:从入门到精通
ios·swift
用户79457223954133 天前
【RxSwift】Swift 版 ReactiveX,响应式编程优雅处理异步事件流
swift·rxswift
战族狼魂3 天前
XCode 发起视频 和 收到视频通话邀请实现双语功能 中文和俄语
swift
UXbot3 天前
2026年AI全链路产品开发工具对比:5款从创意到上线一站式平台深度解析
前端·ui·kotlin·软件构建·swift·原型模式
报错小能手4 天前
ios开发方向——swift并发进阶核心 @MainActor 与 DispatchQueue.main 解析
开发语言·ios·swift