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

news2025/6/9 9:23:17

在这里插入图片描述

概述

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

在这里插入图片描述

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

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

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

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

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


1. 完美世界崩塌了!

在之前的这两篇博文里:

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

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

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


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


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

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

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

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

@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. 刨根问底:问题根源之所在

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

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() 方法里明确说明了必须按实际的子类名称来查询的吗:

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 基类 + 子类多态场景却意外翻车的故事,随后我们深入讨论了问题的根源之所在。

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2405182.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

华为云Flexus+DeepSeek征文 | 基于DeepSeek-V3构建企业知识库问答机器人实战

作者简介 我是摘星&#xff0c;一名专注于云计算和AI技术的开发者。本次通过华为云MaaS平台体验DeepSeek系列模型&#xff0c;将实际使用经验分享给大家&#xff0c;希望能帮助开发者快速掌握华为云AI服务的核心能力。 目录 作者简介 1. 引言 2. 技术选型与架构设计 2.1 技…

【Docker 01】Docker 简介

&#x1f308; 一、虚拟化、容器化 ⭐ 1. 什么是虚拟化、容器化 物理机&#xff1a;真实存在的服务器 / 计算机&#xff0c;对于虚拟机来说&#xff0c;物理机为虚拟机提供了硬件环境。虚拟化&#xff1a;通过虚拟化技术将一台计算机虚拟为 1 ~ n 台逻辑计算机。在一台计算机…

AUTOSAR实战教程--DoIP_02_诊断链路建立流程

第一步&#xff1a;DoIP实体车辆声明/诊断仪车辆识别请求 打开激活线以后&#xff0c;DoIP实体发的三帧车辆声明报文。其中包含了DoIP实体的诊断逻辑地址&#xff08;可以类比DoCAN的物理请求/响应地址&#xff09;&#xff0c;对应车辆的VIN码&#xff08;若已配置&#xff0…

音频剪辑软件少之又少好用

我们平时见到的图片以及视频编辑工具非常多&#xff0c;但是音频剪辑软件却是少之又少&#xff0c;更不用说有没有好用的&#xff0c;今天&#xff0c;给大家带来一款非常专业的音频剪辑软件&#xff0c;而且是会员喔。 软件简介 一款手机号登录即可以享受会员的超专业音频剪…

客户端和服务器已成功建立 TCP 连接【输出解析】

文章目录 图片**1. 连接状态解析****第一条记录&#xff08;服务器监听&#xff09;****第二条记录&#xff08;客户端 → 服务器&#xff09;****第三条记录&#xff08;服务器 → 客户端&#xff09;** **2. 关键概念澄清****(1) 0.0.0.0 的含义****(2) 端口号的分配规则** *…

day26-计算机网络-4

1. tcp的11种状态 ss -ant -a 表示看所有状态 -n 表示不将ip解析为主机名 -t 表示tcp 1.1. closed状态&#xff08;客户端、服务端&#xff09; 客户端发起建立连接前的状态服务端启动服务前的状态 1.2. listen状态&#xff08;服务端&#xff09; 服务端软件运行的时候状…

国防科技大学计算机基础慕课课堂学习笔记

1.信息论 香农作为信息论的这个创始人&#xff0c;给出来了这个信息熵的计算方法&#xff0c;为我们现在的这个生活的很多领域奠定了基础&#xff0c;我第一次听说这个信息熵是在这个数学建模里面的理论学习中有关于这个&#xff1a;决策树的模型&#xff0c;在那个问题里面&a…

【第七篇】 SpringBoot项目的热部署

简介 本文介绍了热部署&#xff08;Hot Deployment&#xff09;的概念、使用场景及在IDEA中的配置方法。热部署可在不重启应用的情况下动态更新代码&#xff0c;提升开发效率&#xff0c;适用于调试、微服务架构和自动化测试等场景。文章详细说明了热部署的实现步骤&#xff08…

解决pycharm同一个文件夹下from *** import***仍显示No module named

1、&#xff0c;from ***import *&#xff0c;同文件夹中已有.py文件但是仍然报错No module named 原因是因为pycharm没有把文件夹设置为根目录&#xff0c;只需要在文件夹的上一级设置为根目录即可&#xff0c;测试过如果仅仅将当前的文件夹设置仍然报错&#xff0c;如果把最上…

使用 Redisson 实现分布式锁—解决方案详解

Redisson 是 Redis 官方推荐的 Java 客户端&#xff0c;提供了一系列分布式服务实现&#xff0c;其中分布式锁是其核心功能之一。本文将深入解析 Redisson 分布式锁的实现原理、高级特性和最佳实践。 一、Redisson 分布式锁的优势 与传统实现的对比 特性手动实现Redisson 实现…

结合三维基因建模与智能体技术打造工业软件无码平台

通过深度整合 Protocol Buffers (Protobuf)、gRPC 及 Microsoft AI 技术&#xff0c;构建面向智能制造的高性能、智能化 PLM 平台。 一、Protocol Buffers 深度集成 1. 基因模型标准化定义 三维基因容器 Protobuf 规范&#xff1a; protobuf syntax "proto3"; pa…

Python Day46

Task&#xff1a; 1.不同CNN层的特征图&#xff1a;不同通道的特征图 2.什么是注意力&#xff1a;注意力家族&#xff0c;类似于动物园&#xff0c;都是不同的模块&#xff0c;好不好试了才知道。 3.通道注意力&#xff1a;模型的定义和插入的位置 4.通道注意力后的特征图和热力…

基于PostGIS的各地级市路网长度统计及Echarts图表可视化实践-以湖南省为例

目录 前言 一、路网长度计算 1、地级市列表查询 2、地级市路网长度查询 二、Echarts可视化实现 1、Echarts后端生成 2、引入Colormap配色 3、前端微调 三、总结 前言 在当今快速发展的社会中&#xff0c;交通路网的建设与布局对于一个地区的经济发展、居民生活以及城市…

mac版excel如何制作时长版环形图

设置辅助列 创建簇状柱形图 将辅助列绘制在次坐标轴 工作时长在主坐标轴&#xff0c;右键分别更改图表类型为圆环。 辅助列圆环全部为灰色&#xff0c;边框为白色 辅助列设置透明度100% 设置辅助列和工作时长列同样的圆环大小 可得 核心&#xff1a;只要辅助列边框不透明…

【MySQL系列】MySQL 执行 SQL 文件

博客目录 一、MySQL 执行 SQL 文件的常见场景二、MySQL 执行 SQL 文件的主要方法1. 使用 MySQL 命令行客户端2. 在 MySQL 交互界面中使用 source 命令3. 使用 MySQL Workbench 等图形化工具4. 使用编程语言接口 三、执行 SQL 文件时的注意事项1. 字符集问题2. 事务处理3. 错误处…

论文MR-SVD

每个像素 7 个 FLOPs意思&#xff1a; FLOPs&#xff08;浮点运算次数&#xff09;&#xff1a;衡量算法计算复杂度的指标&#xff0c;数值越小表示运算越高效。含义&#xff1a;对图像中每个像素进行处理时&#xff0c;仅需执行7 次浮点运算&#xff08;如加减乘除等&#xf…

Java 日期时间类全面解析

Java 日期时间类全面解析&#xff1a;从传统到现代的演进 一、发展历程概览 二、传统日期类&#xff08;Java 8前&#xff09; 1. java.util.Date - 日期表示类 Date now new Date(); // 当前日期时间 System.out.println(now); // Wed May 15 09:30:45 CST 2023// 特定时间…

【工具-Wireshark 抓包工具】

工具-Wireshark 抓包工具 ■ Wireshark 抓包工具■ 通过IP指定查看■■ ■ Wireshark 抓包工具 抓包工具】win 10 / win 11&#xff1a;WireShark 下载、安装、使用 Wireshark下载 阿里云镜像 ■ 通过IP指定查看 ■ ■

设备驱动与文件系统:06 目录与文件

磁盘使用的最后一层抽象&#xff1a;文件系统 今天我们讲第31讲&#xff0c;这一讲将完成磁盘对磁盘使用的最后一层抽象。对此板使用最后一层抽象&#xff0c;抽象出来的是什么呢&#xff1f; 实际上我们使用过磁盘&#xff0c;大家应该有这样的认识&#xff0c;最后不管这个磁…

Linux 系统中的算法技巧与性能优化

引言​ Linux 系统以其开源、稳定和高度可定制的特性&#xff0c;在服务器端、嵌入式设备以及开发环境中得到了极为广泛的应用。对于开发者而言&#xff0c;不仅要掌握在 Linux 环境下实现各类算法的方法&#xff0c;更要知晓如何利用系统特性对算法进行优化&#xff0c;以提升…