概述
相信各位似秃非秃小码农们都同意,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
类型与类继承体系之间的冲突引起的。要解决这个问题,需要理解以下核心机制:
- 协议中
Self
的严格性
Swift 协议中的Self
代表「实现该协议的具体类型」。当协议方法返回[Self]
时,要求实现该方法的类型必须在编译时明确自身类型。 - 非
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 托管类型功能遇到的问题,并初步提供了一些“不尽如人意”的解决方法。
感谢观赏,我们下一篇再会!😎