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

在这里插入图片描述

概述

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

在这里插入图片描述

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

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

无需等待,Let‘s go!!!😉


1. 背景故事

我们的项目基于 SwiftUI + CoreData 构建,在数据库中我们需要为用户创建各种各样的成就(Achievements),因为每种成就本身有很大的不同(字段、获取手段等),所以考虑在 CoreData 数据库中使用抽象基类 + 实体类的组成方法:

  • Achievement 类是成就的抽象基类,其中包含所有成就都共有的字段和方法;
  • Achv_NoBreakVictory 类和其它实体类都“派生”于 Achievement 基类,对应于每一种具体的成就,它们包含自己独有的字段和方法;

Achievement 和 Achv_NoBreakVictory 类的定义如下所示:

@objc(Achievement)
public class Achievement: NSManagedObject {

}

@objc(Achv_NoBreakVictory)
public class Achv_NoBreakVictory: Achievement {

}

对于 Achv_NoBreakVictory 这一成就实体托管类来说,我们往往需要查询它的所有实例,所以有必要写一个方法来达成此目的:

static func queryAll(context: NSManagedObjectContext) throws -> [Achv_NoBreakVictory] {
        let req: NSFetchRequest<Achv_NoBreakVictory> = fetchRequest()
        return try context.fetch(req)
    }

但是问题来了:如果我们有一大堆这样的实体类,难道要不厌其烦的在每个类中实现上面的方法吗?

答案当然是大大的 NO!

2. 想法不错,无奈编译器不允许!

因为 Achievement 会派生出很多不同的成就实体子类,这些子类同样需要上面的 queryAll 方法来查询它们各自的所有实例,为了规范它们共同的“言行”,我们决定创建一个协议让它们来遵守:

protocol AchievementEvaluator {
    static func queryAll(context: NSManagedObjectContext) throws -> [Self]
}

接下来,我们需要让 Achv_NoBreakVictory 实体类遵守 AchievementEvaluator 协议:

extension Achv_NoBreakVictory: AchievementEvaluator {
    static func queryAll(context: NSManagedObjectContext) throws -> [Achv_NoBreakVictory] {
        let req: NSFetchRequest<Achv_NoBreakVictory> = fetchRequest()
        return try context.fetch(req)
    }
}

不幸的是,这样做的话编译器会立即大声抱怨:

在这里插入图片描述

Protocol ‘AchievementEvaluator’ requirement ‘queryAll(context:)’ cannot be satisfied by a non-final class (‘Achv_NoBreakVictory’) because it uses ‘Self’ in a non-parameter, non-result type position

这个编译错误是由于 Swift 协议中 Self 类型与类继承体系之间的冲突引起的。要解决这个问题,需要理解以下核心机制:

  1. 协议中 Self 的严格性
    Swift 协议中的 Self 代表「实现该协议的具体类型」。当协议方法返回 [Self] 时,要求实现该方法的类型必须在编译时明确自身类型
  2. final 类的继承风险
    如果 Achv_NoBreakVictory 是非 final 类,它可以被继承(如 class SubAchv: Achv_NoBreakVictory)。此时子类 SubAchv 必须实现 spawnAll() -> [Self],但继承自父类的 spawnAll() 实际返回的是 [Achv_NoBreakVictory] 而非 [SubAchv],所以这会导致类型不匹配,违背协议要求。

那我们该如何解决呢?

3. “不情愿”的 final

经过查看上面的错误提示,我们可以幡然醒悟,一种简单的解决方案应运而生,即将 Achv_NoBreakVictory 类变为 final 类,可以让编译器“敢怒不敢言”:

public final class Achv_NoBreakVictory: Achievement {}

不过,或许我们的 Achv_NoBreakVictory 类是“委托” CoreData 模型编辑器自动生成的,这样的话每次更新 Achv_NoBreakVictory 类的内容都需要费劲手动再添加 final 关键字,不烦吗?

除了强制让 Achv_NoBreakVictory 类“后继无人”以外,另一种颇为 Nice 的解决方法是为 AchievementEvaluator 协议添加关联类型:

protocol AchievementEvaluator {
    associatedtype Evaluator
    static func queryAll(context: NSManagedObjectContext) throws -> [Evaluator]
}

通过上面一番操作之后,我们 Achv_NoBreakVictory 类扩展中 queryAll() 方法的代码已经可以顺利通过编译了,厉害了我的秃们!

4. DRY 制胜法宝:协议扩展(Protocol Extension)

通过仔细观察上面 Achv_NoBreakVictory 类扩展中的 queryAll() 方法,聪明的小伙伴们不难发现:每个 Achievement 实体类 queryAll() 方法的代码实际上都大同小异,我们实在没必要“痴鼠拖姜”的一一重复实现它们。

侵淫苹果撸码多年的秃头小码农们都知道,Swift 协议有一种机制专注于解决此事,它就是协议扩展(Protocol Extension)

简单来说,我们可以将 queryAll() 方法直接放在 AchievementEvaluator 协议扩展里,而不是在遵守它的每个类里:

extension AchievementEvaluator {
    static func queryAll(context: NSManagedObjectContext) throws -> [Evaluator] {
        let req: NSFetchRequest<Evaluator> = Evaluator.fetchRequest()
        return try context.fetch(req)
    }
}

extension Achv_NoBreakVictory: AchievementEvaluator {
    typealias Evaluator = Achv_NoBreakVictory
    
    /*
    static func queryAll(context: NSManagedObjectContext) throws -> [Achv_NoBreakVictory] {
        let req: NSFetchRequest<Achv_NoBreakVictory> = fetchRequest()
        return try context.fetch(req)
    }*/
}

在上面的代码中,我们将原本位于实体类 Achv_NoBreakVictory 中的 queryAll 方法调皮地瞬移到了 AchievementEvaluator 协议扩展里面。

不过这样一来,编译器的抱怨也会再次“卷土重来”:

在这里插入图片描述

Cannot assign value of type ‘NSFetchRequest<any NSFetchRequestResult>’ to type ‘NSFetchRequest<Self.Evaluator>’

造成这种错误的根本原因是:在 Swift 中处理 Core Data 的 NSFetchRequest 泛型类型时,没有确保类型系统的严格匹配。

  • NSFetchRequest<Evaluator> 的泛型要求:Core Data 的 fetchRequest() 默认返回 NSFetchRequest<NSFetchRequestResult>,而协议中定义的 Evaluator 关联类型要求返回具体的 Evaluator 类型,导致类型不匹配。
  • 协议扩展的泛型约束不足:编译器无法确认 Evaluator.fetchRequest() 返回的请求类型是否与 Evaluator 类型一致。

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

在下一篇博文中,我们将继续 AchievementEvaluator 协议扩展的进化之旅,敬请期待吧!


想要进一步系统地学习 Swift 武功秘笈的小伙伴们,可以来我的《Swift 语言开发精讲》专栏逛一逛哦:

在这里插入图片描述


总结

在本篇博文中,我们讨论了在用 Swift 协议扩展优化和重构 CoreData 托管类型功能遇到的问题,并初步提供了一些“不尽如人意”的解决方法。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大熊猫侯佩

赏点钱让我买杯可乐好吗 ;)

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值