聊聊Swift中的宏
聊聊Swift中的宏
宏,Macros是一种常见的编程技术,传统的C语言中,即包含了宏功能。宏这种功能,简单来说是在代码的预编译阶段进行静态替换,是一种非运行时的特性。但是往复杂了说,宏实际上也提供了一种”元编程“方式,即对程序本身进行编程。如果真正掌握宏的应用,又比较复杂,以C语言中的宏为例,宏可以有参数,可以进行嵌套展开,要编写质量高的宏,还是非常有难度。这里附上之前的一篇关于Objective-C下宏的应用博文,以供需要的朋友参考:
https://my.oschina.net/u/2340880/blog/3357392
Swift宏简介
最初的Swift版本其实并不支持宏,这其实也和Swift语言的设计理念有关,C语言中的宏应用广泛,但是编译时展开的特性会是代码的可读性下降,也会增加代码的漏洞风险。Swift秉承安全、易理解、易使用的设计初衷,并没有引入宏的概念。但宏的元编程能力可以大大的提高编程的灵活性和复用性,Swift在5.9版本中重新引入了宏功能,并且是以一种全新的方式来定义和实现宏,在提供灵活性的同时保证代码的安全性和可靠性。但这也有一些缺陷,相比与C语言的宏,Swift中的宏的定义非常抽象,实现复杂,不太利于开发者进行理解。本篇文章即基于这一前提,希望可以系统简介的对Swift中的宏进行介绍,帮助更多开发者了解它,使用它。
首先,在做详细介绍前,我们需要先牢记几个核心原理:
1 - 宏会在编译代码前进行代码转换,即预编译阶段进行处理。
2 - 宏在展开时,永远只会增加代码,不会修改或删除原始的代码。(重点)
3 - 宏的输入和输出都会经过编译器的检查,保证其语法正确,并且如果宏展开后的实现发现异常,也会被处理为编译时异常。
上述的原理1和原理3无需特别关注,只需要知道宏是一个编译时的特性即可,原理2是非常重要的,当我们想将某个功能点编写为宏时,首先要考虑的是我们是要附加功能还是删改功能,如果是增加功能则非常适合使用宏,如果是删改逻辑则应该早早放弃,宏永远不应该删改原本的代码。
Swift中的宏分为两类:
1 - 独立宏
2 - 附加宏
其中,独立宏单独出现,单独使用,不会附加到任何声明(可以理解为原始代码)上。附加宏则需要配合声明一起使用,通常是为了向原代码中增加一些功能。从特性上看,独立宏与C语言的宏有些类似,做简单的代码展开或静态替换很方便。附加宏则更像是一种装饰器模式的应用,为原始逻辑进行包装,附加功能。这两种宏从声明到用法上都有区别。
独立宏
独立宏使用"#"来调用,因此当你在代码中看到#相关的语法时,就要意识到这是一个宏,且是一个独立宏。标准库中默认提供了一些独立宏可以直接使用,例如:
class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() print(#file, #function, #line) #warning("系统宏,显示警告信息") } }
上面代码中,#file,#function,#line和#warning都是独立宏,前3个宏无参数,在编译时分别替换为当前文件名、当前函数名和当前行号,#warning宏有参数,用来为告诉编译器这里展示一条警告信息。这些宏因为是标准库中的,我们无法查看展开后的样子,如果是自定义宏则可以直接展开查看,后面我们再介绍。
附加宏
使用”@“来调用附加宏,附加宏用来补充其所声明的代码,为原始代码添加新的功能,附加宏比较复杂,后面我们再详细介绍。
宏的声明、定义与实现
Swift语言和C语言的一大区别在于Swift一般无需做声明,如函数、变量、类等,直接定义即可使用。但宏却不同,宏必须进行声明,声明的主要作用是指定宏的名称、参数以及类型和使用场景。
与普通的Swift功能代码不同,每个宏都是一个单独的Swift包,在工程中我们可以创建一个新的Package,选择Swift Macro,如下图所示:
宏的实现依赖于swift-syntax包,Xcode会自动帮我们加载好依赖。创建好的的Package会自动生成模版文件,我们只需要关系Sources和Tests文件夹下的内容即可。自动生成的模板中的宏是使用了swift-syntax包的Swift源代码静态分析能力,略为复杂,增加了理解宏本身的难度。这里我们可以不理会这部分,专注于宏本身的逻辑。
首先,一个宏模块分为声明,实现,测试和使用4个部分。下面我们逐一来进行介绍。
宏的声明
独立宏声明
独立宏使用@freestanding来进行声明,在声明宏时,需要指定宏的角色。独立宏有两种角色:
expression:创建一段有返回值的代码。
declaration:声明类宏,用来创建声明类的代码。
例如我们声明一个角色为expression的宏,如下:
@freestanding(expression) public macro AppendHello(_ msg: String) -> String = #externalMacro(module: "MyMacroMacros", type: "AppendHelloMacro")
代码中,@freestanding(expression)指定了当前宏是一个表达式角色的独立宏,#externalMacro是Swift内置的一个宏,指定了当前宏所对应的模块名以及类型标识。
声明一个declaration角色的宏如下:
@freestanding(declaration, names: arbitrary) public macro MakeStatic(_ name: String) = #externalMacro(module: "MyMacroMacros", type: "MakeStaticMacro")
需要注意,宏在指定角色时,可以通过names参数来对要使用的符号进行定义,以上面的宏声明为例,MakeStatic的作用是会生成一个静态变量,因此会在原代码中新增符号,但是变量的名称是由参数决定的,因此需要将names参数设置为arbitrary,表示要生成的符号是不定的。
names参数可填为:
1 named(xxx) 具体的符号名称。
2 overloaded 对原符号的重载
3 prefixed(xxx) 增加前缀
4 suffixed(xxx) 增加后缀
5 arbitrary 动态符号名称
附加宏声明
附加宏使用@attached来进行声明,与独立宏类似,其也需要指定角色:
peer:对等角色,与所附加的原代码在相同的层级上增加代码,例如增加函数的重载。
member:成员角色,为所附加的原代码增加内部成员,如增加属性等。
memberAttribute:成员属性角色,为所附加的源代码的内部成员增加属性。
accessor:访问器角色,为所附加的源代码增加Getter,Setter方法等。
extension(之前为conformance,最新swift版本修改为extension):遵守着角色,为所附加的源代码增加协议和约束。
我们先来定义一个peer角色类型的宏,用来实现一个自动生成的重载函数,此重载函数会增强原函数的功能,添加函数的执行时间日志。如下:
@attached(peer, names: overloaded) public macro OverrideForAPM() = #externalMacro(module: "MyMacroMacros", type: "OverrideForAPMMacro")
这里我们将names指定为了overloaded,表示对原符号的重载操作。后面会有此宏的实现示例。
member角色的宏通常用来为类或结构增加成员变量或方法等,声明示例如下:
@attached(member, names: named(logSelf)) public macro MemberLog() = #externalMacro(module: "MyMacroMacros", type: "MemberLogMacro")
其中,我们声明时明确定义了要引入的符号logSelf,此符号将作为生成的函数名。
memberAttribute角色宏本质上是作用于类或结构的成员上,用来为成员增加修饰,例如可以定义一个宏为类的成员都默认加上@objc修饰:
@attached(memberAttribute) public macro Objc() = #externalMacro(module: "MyMacroMacros", type: "ObjcMacro")
accessor角色宏用在具体的成员上,用来增加访问器逻辑,例如下面的声明,此宏将为访问器自动生成计算属性逻辑:
@attached(accessor) public macro GetLog() = #externalMacro(module: "MyMacroMacros", type: "GetLogMacro")
extension宏用来为原结构增加一些协议或遵守一些规则,例如我们可以定义一个宏,来让所修饰的修改自动实现Equatable协议:
@attached(extension, conformances:Equatable ,names: named(==)) public macro EqualProtocol() = #externalMacro(module: "MyMacroMacros", type: "EqualProtocolMacro")
其中conformances参数指定要遵守的协议,因为我们同时要对协议进行实现,会引入新的符号,因此需要names参数中也指明。
宏的实现
宏的实现,即也是宏的定义。指定了宏具体要实现的逻辑。
独立宏实现
根据前面AppendHello宏的声明,在MyMacroMacros可以对其进行实现,代码如下:
public struct AppendHelloMacro: ExpressionMacro { public static func expansion(of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) throws -> ExprSyntax { // 解析宏的参数,此宏我们定义了一个msg参数 guard let argument = node.argumentList.first?.expression, let segment = argument.as(StringLiteralExprSyntax.self)?.segments.first else { fatalError("编译异常") } // 我们需要将静态代码内部的字符串数据解析出来 switch segment { case .stringSegment(let string): // 返回一个静态字符串表达式 return ExprSyntax(stringLiteral: "\"\(string.content.text + "Hello")\"") default: fatalError("编译异常") } } } @main struct MyMacroPlugin: CompilerPlugin { let providingMacros: [Macro.Type] = [ AppendHelloMacro.self, ] }
所有的表达式角色的独立宏,在定义时需要实现ExpressionMacro协议,此协议中的expansion函数将返回展开后的结果,我们可以根据逻辑来返回数据即可。需要注意,在编写宏时,我们所有做的操作都是元编程操作,因此需要对Swift元代码进行解析与处理,这也是swift-syntax主要提供的功能。代码中的解析逻辑你可以暂时无需关注。另外,在Plugin的定义中,我们要将此宏类实例进行返回,这里的类与我们前面声明时的类标识要一致。
MakeStatic宏的定义方法也类似,只是其需要实现DeclarationMacro协议,角色为声明类型的宏主要是为原代码增加一些声明,如增加属性,增加方法,增加协议等等。为了演示方便,MakeStatic的作用是根据传入的字符串生成一个静态变量,如下:
public struct MakeStaticMacro: DeclarationMacro { public static func expansion(of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) throws -> [DeclSyntax] { // 解析宏的参数,此宏我们定义了一个name参数 guard let argument = node.argumentList.first?.expression, let segment = argument.as(StringLiteralExprSyntax.self)?.segments.first else { fatalError("编译异常") } // 我们需要将静态代码内部的字符串数据解析出来 switch segment { case .stringSegment(let string): // 返回一个静态字符串表达式 return ["static var \(string): Any?"] default: fatalError("编译异常") } } }
附加宏实现
OverrideForAPM宏的实现如下:
public struct OverrideForAPMMacro: PeerMacro { public static func expansion(of node: AttributeSyntax, providingPeersOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) throws -> [SwiftSyntax.DeclSyntax] { // 函数的声明部分 guard let functionDecl = declaration.as(FunctionDeclSyntax.self) else { fatalError("编译异常") } // 函数的实现副本 if let body = functionDecl.body { return [ """ func \(functionDecl.name)(_ apm: Bool) { if apm {print(Date())} \(body.statements) if apm {print(Date())} } """ ] } return [] } }
MemberLog宏的实现如下:
public struct MemberLogMacro: MemberMacro { public static func expansion(of node: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext) throws -> [DeclSyntax] { return [ """ func logSelf(){ print(self) } """ ] } }
Objc宏的实现如下:
public struct ObjcMacro: MemberAttributeMacro { public static func expansion(of node: SwiftSyntax.AttributeSyntax, attachedTo declaration: some SwiftSyntax.DeclGroupSyntax, providingAttributesFor member: some SwiftSyntax.DeclSyntaxProtocol, in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.AttributeSyntax] { return ["@objc"] } }
GetLog宏的实现如下:
public struct GetLogMacro: AccessorMacro { public static func expansion(of node: AttributeSyntax, providingAccessorsOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) throws -> [AccessorDeclSyntax] { // 获取所修饰的符号 guard let property = declaration.as(VariableDeclSyntax.self), let binding = property.bindings.first, let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.trimmed else { return [] } return [ """ get { print("获取计算属性值", _\(identifier)) return _\(identifier) } """, """ set { _\(identifier) = newValue } """ ] } }
EqualProtocol 宏的实现如下:
public struct EqualProtocolMacro: ExtensionMacro { public static func expansion(of node: AttributeSyntax, attachedTo declaration: some DeclGroupSyntax, providingExtensionsOf type: some TypeSyntaxProtocol, conformingTo protocols: [TypeSyntax], in context: some MacroExpansionContext) throws -> [ExtensionDeclSyntax] { return [try ExtensionDeclSyntax("extension \(type.trimmed): Equatable { static func == (lhs: \(type.trimmed), rhs: \(type.trimmed)) -> Bool { lhs == rhs}}")] } }
对于附加宏来说,除了上述示例的场景外,我们也可以对某个宏指定多个角色,例如member角色宏和accessor角色宏,可以同时为所修饰的原结构增加内部属性和外部访问器方法。多个角色宏的实现也类似,只需要具体的实现多个协议即可了。
宏的使用
宏的使用非常简单,创建的宏Package中自动生成了一个main.swift文件,我们可以在其中进行使用测试,例如:
使用独立的表达式宏:
// newString将被赋值为 Xiao mingHello let newString = #AppendHello("Xiao ming ") print(newString)
使用独立的声明宏:
class MySingle { // 会被展开为 static var obj: Any? #MakeStatic("obj") }
使用peer宏:
// 此宏编译后会增加一个新的重载函数,如下: //func myFunc(_ apm: Bool) { // if apm { // print(Date()) // } // // print("MyFuncCall") // if apm { // print(Date()) // } //} @OverrideForAPM func myFunc() { print("MyFuncCall") } // 调用将打印: //2024-04-17 14:44:29 +0000 //MyFuncCall //2024-04-17 14:44:29 +0000 myFunc(true)
使用member宏:
@MemberLog class CustomClass { // 将像类中添加方法: // func logSelf() { // print(self) //} } let c = CustomClass() // MyMacroClient.CustomClass c.logSelf()
使用memberAttribute宏:
@Objc class SwiftObjcClass { // 宏展开有会增加 @objc func func1() {} // 宏展开有会增加 @objc var v1 = 0 // 宏展开有会增加 @objc static let s1 = 0 }
使用accessor宏:
class GetLogDemo { var _prop = 0 @GetLog var prop:Int // 下面将被展开 // { // get { // _prop // } // set { // _prop = newValue // } // } } let d = GetLogDemo() d.prop = 2 // 获取计算属性值 2 print(d.prop)
使用extension宏:
@EqualProtocol class MyNumber { } // 会在下面展开 //extension MyNumber: Equatable { // static func == (lhs: MyNumber, rhs: MyNumber) -> Bool { // lhs == rhs // } //}
宏的调试与测试
可以发现,宏的代码编写思路与常规的应用开发思路有很大不同,我们主要需要处理的是对Swift代码本身的语法树结构的解析与补充。当然,大部分工作swift-syntax包都帮我们处理好了。在开发宏时,我们可以直接在使用处右键将宏进行展开,可以直接看到宏编译后的结果,例如:
如果宏展开后的结果比较复杂,我们也可以在运行时进行断点,将宏展开,然后直接进行断点调试即可。
另外,如果想要对宏本身进行断点调试,则我们需要通过单元测试来运行宏,模板代码中已经默认生成了测试代码,例如对AppendHello宏进行单测,修改测试文件如下:
import SwiftSyntax import SwiftSyntaxBuilder import SwiftSyntaxMacros import SwiftSyntaxMacrosTestSupport import XCTest // Macro implementations build for the host, so the corresponding module is not available when cross-compiling. Cross-compiled tests may still make use of the macro itself in end-to-end tests. #if canImport(MyMacroMacros) import MyMacroMacros let testMacros: [String: Macro.Type] = [ "AppendHello": AppendHelloMacro.self, ] #endif final class MyMacroTests: XCTestCase { func testMacro() throws { #if canImport(MyMacroMacros) assertMacroExpansion( """ #AppendHello("Xiao Li ") """, expandedSource: """ "Xiao Li Hello" """, macros: testMacros ) #else throw XCTSkip("macros are only supported when running tests for the host platform") #endif } }
单测的逻辑也比较简单,即我们给一个输入宏,然后与预期的展开结果进行对比即可,因为宏是静态展开,因此非常容易也很适合进行单测。在单测执行时,我们是可以对宏的实现部分进行断点的,通过断点,可以对其输入参数的详细信息进行查看,方便我们宏逻辑的编写,以上述单测为例,断点可以后可查看语法节点数据,如下:
(lldb) po node MacroExpansionExprSyntax ├─pound: pound ├─macroName: identifier("AppendHello") ├─leftParen: leftParen ├─arguments: LabeledExprListSyntax │ ╰─[0]: LabeledExprSyntax │ ╰─expression: StringLiteralExprSyntax │ ├─openingQuote: stringQuote │ ├─segments: StringLiteralSegmentListSyntax │ │ ╰─[0]: StringSegmentSyntax │ │ ╰─content: stringSegment("Xiao Li ") │ ╰─closingQuote: stringQuote ├─rightParen: rightParen ╰─additionalTrailingClosures: MultipleTrailingClosureElementListSyntax
写在最后
本篇文章,单纯从Swift宏的角度介绍了各种宏的使用方法和应用场景,然而真正要写好宏,其实还是比较有难度的,首先在编写时展开结果并不直观,其次要考虑的边界情况也很多,因此单测试一个非常好的保证质量的工具。另外,能够熟练使用swift-syntax包也是写好宏的基础。有时间,后面在专门整理swift-syntax的用法吧,希望本篇文章可以为你带来一些帮助和启发,感谢你使用宝贵时间阅读。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
深入理解Transformer技术原理 | 得物技术
谷歌在2017年发布Transformer架构的论文时,论文的标题是:Attention Is All You Need。重点说明了这个架构是基于注意力机制的。 一、什么是注意力机制 在深入了解Transformer的架构原理之前,我们首先要了解下,什么是注意力机制。 人类的大脑对于信息的获取也存在注意力机制,下面我举几个简单的例子: 从上面的图片中,我们可能更容易关注,颜色更深的字、字号更大的字,另外像“震惊”这种吸引人眼球的文案也非常容易吸引人的关注。 我们知道在海量的互联网信息中,往往那些起着“标题党”的文章更能吸引人的注意,从而达到吸引流量的目的,这是一种简单粗暴的方式。另外在大量的同质化图片中,如果有一张图片它的色彩、构图等都别出一格,那你也会一眼就能注意到它,这也是一种简单的注意力机制。 假设有以下这两段文字,需要翻译成英文: 1、我在得物上买了最新款的苹果,体验非常好。 2、我在得物上买了阿克苏的苹果,口感非常好。 我们人类能很快注意到第一段文字中的苹果是指苹果手机,那么模型在翻译时就需要把他翻译成iPhone,而第二段文字中的苹果就是指的苹果这种水果,模型翻译时就需要将...
- 下一篇
管理者如何在团队里讨论那些不便讨论的话题
原文 How to Discuss the Undiscussables on Your Team 在团队中处理不便讨论的敏感话题可能会令人不适,但如果无视问题,它们会不知不觉地积聚起来,影响士气。本文介绍了如何识别这些问题,例如:会议上迅速形成的表面一致、缺乏有效的讨论、或是成员参与不均;并提出了一些方法,帮助揭示团队成员未表达的想法和情感,从而提升团队的工作效率。 对一个前景看好的新产品的质量问题讳莫如深;两位团队成员之间明显不和被忽视;团队的公开价值观与实际行为之间的脱节从未明说。 尽管大家都在努力创建一个心理安全的工作环境,但团队中依旧存在许多难以开口讨论的话题:这些话题有些隐晦,或者讨论起来颇具挑战。这些话题之所以不被讨论,一方面是因为可以避免短期内的不适和冲突;另一方面,有一个认知差距:领导者高估了团队成员的开放程度。领导者往往比团队成员有更高的心理安全感,并且常常由于错误共识效应,错误地认为其他人与自己有相同的看法和经历。 随着团队地理上越来越分散以及线上沟通增多,感知到团队间的不适或提出敏感话题变得更加困难。但是,忽略这些难以启齿的话题会导致工作关系紧张和无效会议。长此...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Linux系统CentOS6、CentOS7手动修改IP地址
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- Docker安装Oracle12C,快速搭建Oracle学习环境
- CentOS7安装Docker,走上虚拟化容器引擎之路
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- CentOS关闭SELinux安全模块
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- Hadoop3单机部署,实现最简伪集群
- CentOS6,7,8上安装Nginx,支持https2.0的开启