Swift Macros 元编程为Codable解码供给默许值
前语
在WWDC2023中,Apple向咱们介绍了Swift官方的元编程东西”Swift Macros”,与之前社区中的Sourcery相比,具有以下几个长处:
- 调用快捷:运用Swift Macros时,编译器会供给提示,无需硬编码。
- 支撑注解:类似Sourcery的注解,Swift Macros支撑经过自界说宏完成注解,几乎能够完成任何功用。
- 宏打开细节躲藏:Swift Macros彻底躲藏了宏打开之后的代码,并支撑随时在XCode中打开宏以查看详细代码。
- 运用Swift编写:编写宏的过程彻底运用Swift言语,上手难度低,并且支撑在单元测试中进行断点调试,便利调试。
- 支撑单元测试:Swift Macros供给了对单元测试的支撑。
- 彻底开源:swift-syntax 是一个开源库。
可是现在Swift Macros的依靠库 github.com/apple/swift… 不支撑Cocoapods,现在仅支撑SPM集成。
本文介绍了如何运用 Swift Macros 去解决 Swift Codable 解码时难以设置默许值的问题。
Codable
当将一个 JSON/Dictionary 数据转化为 Swift 的 Model 时,咱们优先考虑运用Codable,可是假如在解码过程中,原数据中短少某个值(container中key不存在)或许某个值解码失利(key存在,可是对应的值类型不对)都会导致整个解析链失利。理想的状态下咱们期望为一个特点设置一个默许值,当其解码失利时,直接取默许值即可。
下面是一个运用Codable解码的比方:
struct People: Codable {
let name: String
let age: Int
}
let peopleDic: [String: Any] = ["name": "lfc", "age": 25]
do {
let value: People = try decode(peopleDic)
print(value)
} catch {
print("Error: \(error)")
}
func decode<T: Codable>(_ dic: [String: Any]) throws -> T {
let jsonData = try JSONSerialization.data(withJSONObject: dic, options: [])
return try JSONDecoder().decode(T.self, from: jsonData)
}
可是假如字典中短少某个特点对应的key,比方:
let dic: [String: Any] = ["name": "lfc"]
或许某个key对应的值无法正常解析:
let dic: [String: Any] = ["name": 123, age: true]
都会导致整个值解析失利。关于短少key的状况,一种简略的计划是运用可选值:
struct People: Codable {
let name: String?
let age: Int?
}
这样在从container寻觅不到key的时分,会默许赋值为nil,但这样导致运用起来适当麻烦,每次运用此可选值都要进行解包,或许在上层进行封装。
而且假如container 中 key存在但对应的值是个错误类型(或其他状况导致此值解码失利),整个值依旧会解析失利,因为只要key在container中存在,对应的解码就会产生。
一种最牢靠的办法是手动为People完成Codable:
extension People: Codable {
public enum CodingKeys: String, CodingKey {
case name
case age
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = (try? container.decode(String.self, forKey: .name)) ?? ""
age = (try? container.decode(Int.self, forKey: .age)) ?? 0
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(age, forKey: .age)
}
}
这样不光能够在某个值解析失利的时分赋一个默许值,还能够自界说 codingKey。但需求为每一个需求编解码的模型完成这部分的代码,虽然新版的XCode现已能够主动补全这些办法,可是后续改动数据结构,都需求去从头更改每一个办法的细节,想一想便是一件很恶心的事情。那么有没有更好的办法去为 Decodable 的特点添加解码的默许值呢。
特点包装器
有一种办法是经过特点包装器在 Decodable 特点的 getter 办法中进行处理,详细细节能够参考:onevcat.com/2020/11/cod…
但这种办法有以下坏处:
- 需求手动为每一个特点添加特点包装器。
- 无法自界说 CodingKeys。
- 由于默许值在
init(from: Decoder)
中赋值,此刻无法获取property wrapper传入的参数值,所以默许值只能提早界说好大局静态变量,不够灵活,关于不同的事务场景需求不同的默许值时,只能不停的添加新的大局静态变量。
元编程Sourcery
Sourcery 是一个 Swift 代码生成的开源命令行东西,它 (经过 SourceKitten) 运用 Apple 的 SourceKit 框架,来剖析你的源码中的各种声明和标注,然后套用你预先界说的 Stencil 模板 (一种语法和 Mustache 很类似的 Swift 模板言语) 进行代码生成。
装置 Sourcery 非常简略,brew install sourcery
即可。不过,假如你想要在实际项目中运用这个东西的话,主张直接从发布页面下载二进制文件,放到 Xcode 项目目录中,然后添加 Run Script 的 Build Phase 来在每次编译的时分主动生成。
能够经过编写模版代码的办法为Codale的类型主动生成CodingKeys
init(from: Decoder)
以及encode(to: Encoder)
。
Codable 替换计划
sourcery的缺陷如下:
- 编写模版时不能调试,编写/上手困难,后续保护难度大。
- 多个cocoapod库依靠同一个模版时,偶然产生将A库的代码生成的B库中的错误。
Swift Macros
下面咱们详细介绍如何运用 Swift Macros 为Codable添加默许值。
主动生成默许值的宏
界说 IcarusCodable 协议
循序渐进,咱们首要自界说一个协议 IcarusCodable
这个协议承继自Codable并包括一个默许值的办法:
public portocol IcarusCodable: Codable {
static var defaultValue: Self { get }
}
一切遵从此协议的办法都能够经过 .defaultVaule
获取默许值。
为根本类型完成 IcarusCodable
咱们为一些常用的根本类型完成此协议,这很有用,使得接下来的完成中,一切由这些根本类型构成的类型都能够主动生成自己的默许值:
extension Int: IcarusCodable {
public static var defaultValue: Int { 0 }
}
extension String: IcarusCodable {
public static var defaultValue: String { "" }
}
extension Double: IcarusCodable {
public static var defaultValue: Double { 0.0 }
}
extension Bool: IcarusCodable {
public static var defaultValue: Bool { false }
}
extension Optional: IcarusCodable where Wrapped: IcarusCodable {
public static var defaultValue: Optional<Wrapped> { .none }
}
extension Dictionary: IcarusCodable where Value: IcarusCodable, Key: IcarusCodable {
public static var defaultValue: Dictionary<Key, Value> { [:] }
}
extension Array: IcarusCodable where Element: IcarusCodable {
public static var defaultValue: Array<Element> { [] }
}
完成添加默许值的宏
下面咱们就能够为一切特点都为 IcarusCodable
的类型完成一个主动添加默许值的宏,作用大约像这样:
@icarusCodable
struct Student {
let name: String
let age: Int
let address: String?
let isBoarder: Bool
// === 宏打开开端 ===
private init(_name: String, _age: Int, _address: String?, _isBoarder: Bool) {
self.name = _name
self.age = _age
self.address = _address
self.isBoarder = _isBoarder
}
public static var defaultValue: Self {
Self (_name: String.defaultValue, _age: Int.defaultValue, _address: String?.defaultValue, _isBoarder: Bool.defaultValue)
}
// === 宏打开完毕 ===
}
// === 宏打开开端 ===
extension Student : IcarusCodable
// === 宏打开完毕 ===
-
首要完成 ConformanceMacro 主动打开 extension XXX : IcarusCodable { }:
public struct AutoCodableMacro: ConformanceMacro { public static func expansion( of node: AttributeSyntax, providingConformancesOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext) throws -> [(TypeSyntax, GenericWhereClauseSyntax?)] { let type = TypeSyntax(stringLiteral: "IcarusCodable") return [(type, nil)] } }
ConformanceMacro 用于使某个类型遵从某个协议。
需求在 expansion 办法中回来
遵从的协议类型
和协议泛型限定描绘
的元组的一个数组[(TypeSyntax, GenericWhereClauseSyntax?)]
,每一个元组都会为类型添加一个 conformance。这里咱们只需求让类型遵从
IcarusCodable
即可,所以只需求回来[(TypeSyntax(stringLiteral: "IcarusCodable"), nil)]
-
然后完成 defaultValue,由于class/struct 不一定存在默许通用的结构器,咱们自界说一个私有结构器,并运用’_’防止重名:
public struct AutoCodableMacro: MemberMacro { public static func expansion( of node: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext ) throws -> [DeclSyntax] { ... } }
MemberMacro 用于为目标添加成员特点/办法。
咱们一共需求添加两个 member,一个私有结构器,以及一个get only的核算特点。
-
首要咱们从 DeclGroupSyntax 中获取类型的存储特点:
// get stored properties let storedProperties: [VariableDeclSyntax] = try { if let classDeclaration = declaration.as(ClassDeclSyntax.self) { return classDeclaration.storedProperties() } else if let structDeclaration = declaration.as(StructDeclSyntax.self) { return structDeclaration.storedProperties() } else { throw CodableError.invalidInputType } }()
此处咱们将 DeclGroupSyntax 转化为 ClassDeclSyntax或StructDeclSyntax 并获取其存储特点。(storedProperties()是一个拓宽办法,详细见完好代码)
-
然后咱们获取存储特点的特点名和类型:
// unpacking the property name and type of a stored property let arguments = storedProperties.compactMap { property -> (name: String, type: TypeSyntax)? in guard let name = property.name, let type = property.type else { return nil } return (name: name, type: type)
其中 property.name 和 property.type 为拓宽核算特点,详细见完好代码。
-
然后结构私有结构器:
// MARK: - _init let _initBody: ExprSyntax = "\(raw: arguments.map { "self.\($0.name) = _\($0.name)" }.joined(separator: "\n"))" let _initDeclSyntax = try InitializerDeclSyntax( PartialSyntaxNodeString(stringLiteral: "private init(\(arguments.map { "_\($0.name): \($0.type)" }.joined(separator: ", ")))"), bodyBuilder: { _initBody } )
InitializerDeclSyntax 用于描绘结构器节点,ExprSyntax 用于描绘表达式节点。
-
结构核算特点:
// MARK: - defaultValue let defaultBody: ExprSyntax = "Self(\(raw: arguments.map { "_\($0.name): \($0.defaultValue ?? $0.type.defaultValueExpression)" }.joined(separator: ",")))" let defaultDeclSyntax: VariableDeclSyntax = try VariableDeclSyntax("public static var defaultValue: Self") { defaultBody }
VariableDeclSyntax 用于描绘变量节点
-
终究将这两个 DeclSyntax 回来即可:
return [ DeclSyntax(_initDeclSyntax), DeclSyntax(defaultDeclSyntax) ]
-
-
终究咱们需求在桥接文件中声明这个宏:
@attached(conformance) @attached(member, names: named(`init`), named(defaultValue)) public macro icarusCodable() = #externalMacro(module: "IcarusMacros", type: "AutoCodableMacro")
这表明这个宏不需求参数,并且为类型添加了新的 conformance,以及添加了两个新的成员:
init
和defaultValue
。
语法树
假如你不了解 StructDeclSyntax 的详细结构,能够经过编写单元测试,然后进行断点调试:
func testAutoCodable() {
assertMacroExpansion(
#"""
struct People { }
@icarusCodable
struct Student{
let name: String
let age: Int
let address: String?
let isBoarder: Bool
}
"""#
,
expandedSource: "",
macros: testMacros)
}
经过断点调试输出语法树,咱们能够看到 StructDeclSyntax 的语法树大约长这样:
其中每个节点的意义:
- AttributeListSyntax:添加的宏列表,你能够在这里找到一切添加的宏以及描绘宏的参数,比方上面的比方中能够看到一个 name 为 icarusCodable 的宏。
- structKeyword:表明其类型为 struct。
- identifier:目标名。
- MemberDeclBlockSyntax: 成员结构节点。
- MemberDeclListSyntax:成员列表。
- MemberDeclListItemSyntax: 每一个成员的详细信息。
在其中能够解析出每个存储特点的特点名和类型。
经过自界说注解宏注入参数
现在咱们完成了一个能够主动生成默许值的宏,可是生成的默许值都是提早界说好的不变的值,咱们期望能够恣意自界说默许值以应对不同的事务场景。
经过语法树解析,能够知道在类型节点的 AttributeListSyntax 中能够获取到自界说宏的详细信息,所以咱们自界说一个不做打开的注解宏:
// Annotation macro, unexpanded
public struct AutoCodableAnnotation: PeerMacro {
public static func expansion(
of node: AttributeSyntax,
providingPeersOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext) throws -> [DeclSyntax] {
return []
}
}
在桥接文件中咱们声明这个宏:
@attached(peer)
public macro icarusAnnotation<T: IcarusCodable>(default: T) = #externalMacro(module: "IcarusMacros", type: "AutoCodableAnnotation")
这表明这个宏承受一个 IcarusCodable 类型的参数。
接下来咱们对 AutoCodableMacro 稍作修改,去获取添加在存储特点上的 icarusAnnotation
。
let arguments = storedProperties.compactMap { property -> (name: String, type: TypeSyntax, defaultValue: String?)? in
guard let name = property.name, let type = property.type
else { return nil }
var defaultValue: String?
// find the icarusAnnotation annotation tag
guard let attribute = property.attributes?.first(where: { $0.as(AttributeSyntax.self)!.attributeName.description == "icarusAnnotation" })?.as(AttributeSyntax.self),
let arguments = attribute.argument?.as(TupleExprElementListSyntax.self)
else { return (name: name, type: type, defaultValue: defaultValue) }
// extracting the key and default values from the annotation and parsing them according to the syntax tree structure.
arguments.forEach {
let argument = $0.as(TupleExprElementSyntax.self)
let expression = argument?.expression.as(StringLiteralExprSyntax.self)
let segments = expression?.segments.first?.as(StringSegmentSyntax.self)
switch argument?.label?.text {
case "default": defaultValue = argument?.expression.description
default: break
}
}
// the property name is used as the default key
return (name: name, type: type, defaultValue: defaultValue)
}
经过property.attributes
咱们获取到添加在存储特点上,名为icarusAnnotation
的宏的参数列表中 default
的值。当然,这是一个可选值,当它不存在时,默许运用IcarusCoable.defaultValue。
let defaultBody: ExprSyntax = "Self(\(raw: arguments.map { "_\($0.name): \($0.defaultValue ?? $0.type.defaultValueExpression)" }.joined(separator: ",")))"
作用如下:
@icarusCodable
struct Student {
let name: String
@icarusAnnotation(default: 100)
let age: Int
let address: String?
@icarusAnnotation(default: true)
let isBoarder: Bool
// === 宏打开开端 ===
private init(_name: String, _age: Int, _address: String?, _isBoarder: Bool) {
self.name = _name
self.age = _age
self.address = _address
self.isBoarder = _isBoarder
}
public static var defaultValue: Self {
Self (_name: String.defaultValue, _age: 100, _address: String?.defaultValue, _isBoarder: true)
}
// === 宏打开完毕 ===
}
// === 宏打开开端 ===
extension Student : IcarusCodable {}
// === 宏打开完毕 ===
为 IcarusCodable 完成 Codable
在此基础之上,咱们继续为 AutoCodableMacro 添加三个成员: CodingKeys
init(from: Decoder)
encode(to: Encoder)
,同时为了能够完成自界说CodingKeys,首要给注解宏添加一个参数:
@attached(peer)
public macro icarusAnnotation<T: IcarusCodable>(key: String? = nil, default: T) = #externalMacro(module: "IcarusMacros", type: "AutoCodableAnnotation")
@attached(peer)
public macro icarusAnnotation(key: String) = #externalMacro(module: "IcarusMacros", type: "AutoCodableAnnotation")
新增参数 key: String? 表明此特点对应的编码键值,为可选类型,未设置时,默许取变量名。
-
CodingKeys:
// MARK: - CodingKeys let defineCodingKeys = try EnumDeclSyntax(PartialSyntaxNodeString(stringLiteral: "public enum CodingKeys: String, CodingKey"), membersBuilder: { DeclSyntax(stringLiteral: "\(arguments.map { "case \($0.key)" }.joined(separator: "\n"))") })
-
Decoder:
// MARK: - Decoder let decoder = try InitializerDeclSyntax(PartialSyntaxNodeString(stringLiteral: "public init(from decoder: Decoder) throws"), bodyBuilder: { DeclSyntax(stringLiteral: "let container = try decoder.container(keyedBy: CodingKeys.self)") for argument in arguments { ExprSyntax(stringLiteral: "\(argument.name) = (try? container.decode(\(argument.type).self, forKey: .\(argument.key))) ?? \(argument.defaultValue ?? argument.type.defaultValueExpression)") } })
-
Encoder:
// MARK: - Encoder let encoder = try FunctionDeclSyntax(PartialSyntaxNodeString(stringLiteral: "public func encode(to encoder: Encoder) throws"), bodyBuilder: { let expr: String = "var container = encoder.container(keyedBy: CodingKeys.self)\n\(arguments.map { "try container.encode(\($0.name), forKey: .\($0.key))" }.joined(separator: "\n"))" DeclSyntax(stringLiteral: expr) })
拓宽桥接文件的宏声明:
@attached(conformance)
@attached(member, names: named(`init`), named(defaultValue), named(CodingKeys), named(encode(to:)), named(init(from:)))
public macro icarusCodable() = #externalMacro(module: "IcarusMacros", type: "AutoCodableMacro")
终究作用:
@icarusCodable
struct Student {
@icarusAnnotation(key: "new_name")
let name: String
@icarusAnnotation(default: 100)
let age: Int
@icarusAnnotation(key: "new_address", default: "abc")
let address: String?
@icarusAnnotation(default: true)
let isBoarder: Bool
// === 宏打开开端 ===
public enum CodingKeys: String, CodingKey {
case new_name
case age
case new_address
case isBoarder
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = (try? container.decode(String.self, forKey: .new_name)) ?? String.defaultValue
age = (try? container.decode(Int.self, forKey: .age)) ?? 100
address = (try? container.decode(String?.self, forKey: .new_address)) ?? "abc"
isBoarder = (try? container.decode(Bool.self, forKey: .isBoarder)) ?? true
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .new_name)
try container.encode(age, forKey: .age)
try container.encode(address, forKey: .new_address)
try container.encode(isBoarder, forKey: .isBoarder)
}
private init(_name: String, _age: Int, _address: String?, _isBoarder: Bool) {
self.name = _name
self.age = _age
self.address = _address
self.isBoarder = _isBoarder
}
public static var defaultValue: Self {
Self (_name: String.defaultValue, _age: 100, _address: "abc", _isBoarder: true)
}
// === 宏打开完毕 ===
}
// === 宏打开开端 ===
extension Student : IcarusCodable {}
// === 宏打开完毕 ===
能够看到,在从container中解码失利时,会主动赋值为默许值,而不会抛出错误导致整个类型解码失利。
关于默许的默许值
默许的默许值是指没有在注解中特别声明默许值时,宏所获取的默许值。它默许从IcarusCodable协议中的 defaultValue 中取值,所以在运用时,需求提早为一切的根本类型完成此协议,并声明这些 默许的默许值
。
你能够在你的项目中自由的界说这些默许的默许值。
类型中包括自界说类型
只要包括的自界说类型也是 IcarusCodable 类型,那么宏就能够正常打开,不然,你需求手动为此类型完成 IcarusCodable 协议。
@icarusCodable
struct Address {
let country: String
let province: String
let city: String
}
@icarusCodable
struct Student {
@icarusAnnotation(key: "new_name")
let name: String
@icarusAnnotation(default: 100)
let age: Int
let address: Address
@icarusAnnotation(default: true)
let isBoarder: Bool
}
关于枚举
宏不适用于枚举类型,但假如自界说类型中包括枚举类型,请使此枚举遵从并手动完成 IcarusCodable:
enum Sex: IcarusCodable {
case male
case female
static var defaultValue: Sex { .male }
}
@icarusCodable
struct Student {
@icarusAnnotation(key: "new_name")
let name: String
@icarusAnnotation(default: 100)
let age: Int
let address: Address
@icarusAnnotation(default: true)
let isBoarder: Bool
let sex: Sex
}
总结
运用宏为codable主动生成默许值的优缺陷:
-
长处:
- 能够自界说CodingKeys。
- 能够恣意自界说默许值。
- 开发宏时支撑断点调试、支撑宏的单元测试,后续保护简略。
- 宏彻底运用swift编写。
-
缺陷:
- swift-syntax 现在不支撑 cocoapods,仅能经过SPM集成。
假如你的项目支撑 SPM,那么运用宏会让你的代码愈加简洁美丽。
文中运用的完好代码:github.com/ssly1997/Ic…