Swift宏的实现

news2024/11/24 23:07:36

    上篇介绍了Swift宏的定义与生声明,本篇主要看看是Swift宏的具体实现。结合Swift中Codable协议,封装一个工具让类或者结构体自动实现Codable协议,并且添加一些协议中没有的功能。

关于Codable协议

    Codable很好,但是有一些缺陷:比如严格要求数据源,定义为String给了Int就抛异常、支持自定义CodingKey但是写法十分麻烦、缺字段的情况下不使用Optional会抛异常而不是使用缺省值等等。

基于以上情况,之前也写了一些Codable协议的补充,比如之前使用属性包装器增加了协议的默认值的提供具体地址https://github.com/duzhaoquan/DQTool.git

Swift Macro 的参考链接

  1. 【WWDC23】一文看懂 Swift Macro
  2. swift-macro-examples
  3. Swift AST Explorer
  4. CodableWrapper

实现目标:

Swift5.9之后新出了宏,通过宏可以更加优雅的封装Codable协议,增加新功能

  1. 支持缺省值,JSON缺少字段容错
  2. 支持 String Bool Number 等基本类型互转
  3. 驼峰大小写自动互转
  4. 自定义解析key
  5. 自定义解析规则 (Transformer)
  6. 方便的 Codable Class 子类

具体的实现

定义几个宏

  • @Codable
  • @CodableSubclass
  • @CodableKey(..)
  • @CodableNestedKey(..)
  • @CodableTransformer(..)

先简单的声明与实现

声明Codable和CodableKey宏。

// CodableWrapperMacros/CodableWrapper.swift

@attached(member, names: named(init(from:)), named(encode(to:)))
@attached(conformance)
public macro Codable() = #externalMacro(module: "CodableWrapperMacros", type: "Codable")

@attached(member)
public macro CodableKey(_ key: String ...) = #externalMacro(module: "CodableWrapperMacros", type: "CodableKey")

实现Codable和CodableKey宏。

// CodableWrapperMacros/Codable.swift
import SwiftSyntax
import SwiftSyntaxMacros

public struct Codable: MemberMacro {
    public static func expansion(of _: AttributeSyntax,
                                 providingConformancesOf declaration: some DeclGroupSyntax,
                                 in _: some MacroExpansionContext) throws -> [(TypeSyntax, GenericWhereClauseSyntax?)]
    {
        return []
    }

    public static func expansion(of node: SwiftSyntax.AttributeSyntax,
                                 providingMembersOf declaration: some SwiftSyntax.DeclGroupSyntax,
                                 in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.DeclSyntax]
    {
        return []
    }
}
// CodableWrapperMacros/CodableKey.swift
import SwiftSyntax
import SwiftSyntaxMacros

public struct CodableKey: ConformanceMacro, MemberMacro {
    public static func expansion(of node: SwiftSyntax.AttributeSyntax,
                                 providingMembersOf declaration: some SwiftSyntax.DeclGroupSyntax,
                                 in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.DeclSyntax]
    {
        return []
    }
}


添加宏定义

​

​// CodableWrapperMacros/Plugin.swift
import SwiftCompilerPlugin
import SwiftSyntaxMacros

@main
struct CodableWrapperPlugin: CompilerPlugin {
    let providingMacros: [Macro.Type] = [
        Codable.self,
        CodableKey.self,
    ]
}

 在这里,@Codable实现了两种宏,一种是一致性宏(Conformance Macro),另一种是成员宏(Member Macro)。

一些关于这些宏的说明:

  • @CodableCodable协议的宏名不会冲突,这样的命名一致性可以降低认知负担。
  • Conformance Macro用于自动让数据模型遵循Codable协议(如果尚未遵循)。
  • Member Macro用于添加init(from decoder: Decoder)func encode(to encoder: Encoder)这两个方法。在@attached(member, named(init(from:)), named(encode(to:)))中,必须声明新增方法的名称才是合法的。

实现自动遵循Codable协议

// CodableWrapperMacros/Codable.swift

public struct Codable: ConformanceMacro, MemberMacro {
    public static func expansion(of node: AttributeSyntax,
                                 providingConformancesOf declaration: some DeclGroupSyntax,
                                 in context: some MacroExpansionContext) throws -> [(TypeSyntax, GenericWhereClauseSyntax?)] {
        return [("Codable", nil)]
    }

        public static func expansion(of node: SwiftSyntax.AttributeSyntax,
                                 providingMembersOf declaration: some SwiftSyntax.DeclGroupSyntax,
                                 in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.DeclSyntax]
    {
        return []
    }
}

编译一下。右键@Codable -> Expand Macro查看扩写的代码,看起来还可以。

但如果BasicModel本身就遵循了Codable,编译就报错了。所以希望先检查数据模型是否遵循Codable协议,如果没有的话再遵循它,怎么办呢? 打开Swift AST Explorer 编写一个简单StructClass,可以看到整个AST,declaration: some DeclGroupSyntax对象根据模型是struct还是class分别对应了StructDeclClassDecl。补充上检查代码之后如下,增加了检查时否时class或者struct,否则抛出错误。代码如下

public static func expansion(of node: AttributeSyntax,
                                providingConformancesOf declaration: some DeclGroupSyntax,
                                in context: some MacroExpansionContext) throws -> [(TypeSyntax, GenericWhereClauseSyntax?)] {
    var inheritedTypes: InheritedTypeListSyntax?
    if let declaration = declaration.as(StructDeclSyntax.self) {
        inheritedTypes = declaration.inheritanceClause?.inheritedTypeCollection
    } else if let declaration = declaration.as(ClassDeclSyntax.self) {
        inheritedTypes = declaration.inheritanceClause?.inheritedTypeCollection
    } else {
        throw ASTError("use @Codable in `struct` or `class`")
    }
    if let inheritedTypes = inheritedTypes,
        inheritedTypes.contains(where: { inherited in inherited.typeName.trimmedDescription == "Codable" })
    {
        return []
    }
    return [("Codable" as TypeSyntax, nil)]
}

实现 @Codable 功能

先定义个 ModelMemberPropertyContainerinit(from decoder: Decoder) 和 func encode(to encoder: Encoder) 的扩展都在里面实现。

public static func expansion(of node: SwiftSyntax.AttributeSyntax,
                                providingMembersOf declaration: some SwiftSyntax.DeclGroupSyntax,
                                in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.DeclSyntax]
{
    let propertyContainer = try ModelMemberPropertyContainer(decl: declaration, context: context)
    let decoder = try propertyContainer.genDecoderInitializer(config: .init(isOverride: false))
    let encoder = try propertyContainer.genEncodeFunction(config: .init(isOverride: false))
    return [decoder, encoder]
}
// CodableWrapperMacros/ModelMemberPropertyContainer.swift

import SwiftSyntax
import SwiftSyntaxMacros

struct GenConfig {
    let isOverride: Bool
}

struct ModelMemberPropertyContainer {
    let context: MacroExpansionContext
    fileprivate let decl: DeclGroupSyntax

    init(decl: DeclGroupSyntax, context: some MacroExpansionContext) throws {
        self.decl = decl
        self.context = context
    }

    func genDecoderInitializer(config: GenConfig) throws -> DeclSyntax {
        return """
        init(from decoder: Decoder) throws {
            fatalError()
        }
        """ as DeclSyntax
    }

    func genEncodeFunction(config: GenConfig) throws -> DeclSyntax {
        return """
        func encode(to encoder: Encoder) throws {
            fatalError()
        }
        """ as DeclSyntax
    }
}

填充init(from decoder: Decoder) 

 需要得知属性名、@CodableKey的参数、@CodableNestedKey的参数、@CodableTransformer的参数、初始化表达式。获取memberProperties列表:

struct ModelMemberPropertyContainer {
    let context: MacroExpansionContext
    fileprivate let decl: DeclGroupSyntax
    fileprivate var memberProperties: [ModelMemberProperty] = []

    init(decl: DeclGroupSyntax, context: some MacroExpansionContext) throws {
        self.decl = decl
        self.context = context
        memberProperties = try fetchModelMemberProperties()
    }

    func fetchModelMemberProperties() throws -> [ModelMemberProperty] {
        let memberList = decl.memberBlock.members
        let memberProperties = try memberList.compactMap { member -> ModelMemberProperty? in
            guard let variable = member.decl.as(VariableDeclSyntax.self),
                  variable.isStoredProperty
            else {
                return nil
            }
            // name
            guard let name = variable.bindings.map(\.pattern).first(where: { $0.is(IdentifierPatternSyntax.self) })?.as(IdentifierPatternSyntax.self)?.identifier.text else {
                return nil
            }

            guard let type = variable.inferType else {
                throw ASTError("please declare property type: \(name)")
            }

            var mp = ModelMemberProperty(name: name, type: type)
            let attributes = variable.attributes

            // isOptional
            mp.isOptional = variable.isOptionalType

            // CodableKey
            if let customKeyMacro = attributes?.first(where: { element in
                element.as(AttributeSyntax.self)?.attributeName.as(SimpleTypeIdentifierSyntax.self)?.description == "CodableKey"
            }) {
                mp.normalKeys = customKeyMacro.as(AttributeSyntax.self)?.argument?.as(TupleExprElementListSyntax.self)?.compactMap { $0.expression.description } ?? []
            }

            // CodableNestedKey
            if let customKeyMacro = attributes?.first(where: { element in
                element.as(AttributeSyntax.self)?.attributeName.as(SimpleTypeIdentifierSyntax.self)?.description == "CodableNestedKey"
            }) {
                mp.nestedKeys = customKeyMacro.as(AttributeSyntax.self)?.argument?.as(TupleExprElementListSyntax.self)?.compactMap { $0.expression.description } ?? []
            }

            // CodableTransform
            if let customKeyMacro = attributes?.first(where: { element in
                element.as(AttributeSyntax.self)?.attributeName.as(SimpleTypeIdentifierSyntax.self)?.description == "CodableTransformer"
            }) {
                mp.transformerExpr = customKeyMacro.as(AttributeSyntax.self)?.argument?.as(TupleExprElementListSyntax.self)?.first?.expression.description
            }

            // initializerExpr
            if let initializer = variable.bindings.compactMap(\.initializer).first {
                mp.initializerExpr = initializer.value.description
            }
            return mp
        }
        return memberProperties
    }
}

 完善genDecoderInitializer

    func genDecoderInitializer(config: GenConfig) throws -> DeclSyntax {
        // memberProperties: [ModelMemberProperty]
        let body = memberProperties.enumerated().map { idx, member in

            if let transformerExpr = member.transformerExpr {
                let transformerVar = context.makeUniqueName(String(idx))
                let tempJsonVar = member.name

                var text = """
                let \(transformerVar) = \(transformerExpr)
                let \(tempJsonVar) = try? container.decode(type: type(of: \(transformerVar)).JSON.self, keys: [\(member.codingKeys.joined(separator: ", "))], nestedKeys: [\(member.nestedKeys.joined(separator: ", "))])
                """

                if let initializerExpr = member.initializerExpr {
                    text.append("""
                    self.\(member.name) = \(transformerVar).transformFromJSON(\(tempJsonVar), fallback: \(initializerExpr))
                    """)
                } else {
                    text.append("""
                    self.\(member.name) = \(transformerVar).transformFromJSON(\(tempJsonVar))
                    """)
                }

                return text
            } else {
                let body = "container.decode(type: type(of: self.\(member.name)), keys: [\(member.codingKeys.joined(separator: ", "))], nestedKeys: [\(member.nestedKeys.joined(separator: ", "))])"

                if let initializerExpr = member.initializerExpr {
                    return "self.\(member.name) = (try? \(body)) ?? (\(initializerExpr))"
                } else {
                    return "self.\(member.name) = try \(body)"
                }
            }
        }
        .joined(separator: "\n")

        let decoder: DeclSyntax = """
        \(raw: attributesPrefix(option: [.public, .required]))init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: AnyCodingKey.self)
            \(raw: body)
        }
        """

        return decoder
    }
  • let transformerVar = context.makeUniqueName(String(idx)) 需要生成一个局部transformer变量,为了防止变量名冲突使用了makeUniqueName生成唯一变量名

  • attributesPrefix(option: [.public, .required]) 根据 struct/class 是 open/public 生成正确的修饰。所有情况展开如下:

    open class Model: Codable {
        public required init(from decoder: Decoder) throws {}
    }
    
    public class Model: Codable {
        public required init(from decoder: Decoder) throws {}
    }
    
    class Model: Codable {
        required init(from decoder: Decoder) throws {}
    }
    
    public struct Model: Codable {
        public init(from decoder: Decoder) throws {}
    }
    
    struct Model: Codable {
        init(from decoder: Decoder) throws {}
    }
    

    填充func encode(to encoder: Encoder)

    func genEncodeFunction(config: GenConfig) throws -> DeclSyntax {
        let body = memberProperties.enumerated().map { idx, member in
            if let transformerExpr = member.transformerExpr {
                let transformerVar = context.makeUniqueName(String(idx))
    
                if member.isOptional {
                    return """
                    let \(transformerVar) = \(transformerExpr)
                    if let \(member.name) = self.\(member.name), let value = \(transformerVar).transformToJSON(\(member.name)) {
                        try container.encode(value: value, keys: [\(member.codingKeys.joined(separator: ", "))], nestedKeys: [\(member.nestedKeys.joined(separator: ", "))])
                    }
                    """
                } else {
                    return """
                    let \(transformerVar) = \(transformerExpr)
                    if let value = \(transformerVar).transformToJSON(self.\(member.name)) {
                        try container.encode(value: value, keys: [\(member.codingKeys.joined(separator: ", "))], nestedKeys: [\(member.nestedKeys.joined(separator: ", "))])
                    }
                    """
                }
    
            } else {
                return "try container.encode(value: self.\(member.name), keys: [\(member.codingKeys.joined(separator: ", "))], nestedKeys: [\(member.nestedKeys.joined(separator: ", "))])"
            }
        }
        .joined(separator: "\n")
    
        let encoder: DeclSyntax = """
        \(raw: attributesPrefix(option: [.open, .public]))func encode(to encoder: Encoder) throws {
            let container = encoder.container(keyedBy: AnyCodingKey.self)
            \(raw: body)
        }
        """
    
        return encoder
    }
    

    @CodableKey @CodableNestedKey @CodableTransformer增加Diagnostics

这些宏是用作占位标记的,不需要实际扩展。但为了增加一些严谨性,比如在以下情况下希望增加错误提示:

@CodableKey("a")
struct StructWraning1 {}

实现也很简单抛出异常即可

public struct CodableKey: MemberMacro {
    public static func expansion(of node: AttributeSyntax, providingMembersOf _: some DeclGroupSyntax, in context: some MacroExpansionContext) throws -> [DeclSyntax] {
        throw ASTError("`\(self.self)` only use for `Property`")
    }
}

 这里也就印证了 @CodableKey 为什么不用 @attached(memberAttribute)(Member Attribute Macro) 而使用 @attached(member)(Member Macro) 的原因。如果不声明使用@attached(member),就不会执行MemberMacro协议的实现,在MemberMacro位置写上@CodableKey("a")也就不会报错。

实现@CodableSubclass,方便的Codable Class子类

先举例展示Codable Class子类的缺陷。编写一个简单的测试用例:是不是出乎意料,原因是编译器只给ClassModel添加了init(from decoder: Decoder)ClassSubmodel则没有。要解决问题还需要手动实现子类的Codable协议,十分不便:

@CodableSubclass就是解决这个问题,实现也很简单,在适时的位置super call,方法标记成override就可以了。

func genDecoderInitializer(config: GenConfig) throws -> DeclSyntax {
    ...
    let decoder: DeclSyntax = """
    \(raw: attributesPrefix(option: [.public, .required]))init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: AnyCodingKey.self)
        \(raw: body)\(raw: config.isOverride ? "\ntry super.init(from: decoder)" : "")
    }
    """
}

func genEncodeFunction(config: GenConfig) throws -> DeclSyntax {
    ...
    let encoder: DeclSyntax = """
    \(raw: attributesPrefix(option: [.open, .public]))\(raw: config.isOverride ? "override " : "")func encode(to encoder: Encoder) throws {
        \(raw: config.isOverride ? "try super.encode(to: encoder)\n" : "")let container = encoder.container(keyedBy: AnyCodingKey.self)
        \(raw: body)
    }
    """
}

具体代码实现地址:GitHub - duzhaoquan/CodableTool

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

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

相关文章

Cherno 游戏引擎笔记 (45~60)

有几个部分的笔记以图片形式呈现(如果没找到文本可以查看是否遗漏了图片笔记) My Github REPO(GitHub - JJJJJJJustin/Nut: The game_engine which learned from Cherno) 源码笔记,希望帮到你 :-} ---Shader Library(着色器库&…

x-file-storage一行代码进行文件上传,摆脱阿里云,腾讯云,华为云等不同云的学习,简单高效

问题: 不使用x-file-storage时如果使用某个云首先需要学习他的sdk,这样很麻烦,而x-file-storage集成了各种云的上传,只需要进行配置即可一行代码进行上传 使用 官方地址:X File Storage 一行代码将文件存储到本地、FTP、SFTP、…

线性表与顺序存储结构(下)

前言 接上文(线性表与顺序存储结构(上))。 这些顺序存储结构的方法在顺序表上下卷中已经提到过,但是有些许不同,可以为理解顺序表提供更丰富的视角。(不过最主要的区别在于顺序表上下卷中的顺…

一个合理的前端应用文件结构

在大型应用中,最关键且最具挑战性的方面之一就是拥有一个良好且合理的文件结构。在考虑通过微前端将代码库拆分成多个应用之前,可以遵循一些步骤来改善项目级别的架构,并在您考虑这一路径时使过渡更容易。 我们的目标是应用某种模块化方法&am…

关于图片大小问题造成的QPixmap或QImage读取图片失败的解决办法

今天碰到一个奇怪又离谱的问题 : 图片加载失败。明明路径是正确的,图片也实实在在存在。。。 经过比对,发现如下问题: 我就齐了怪了 这大小怎么差这么多?会不会是这里除了问题。秉持着怀疑的态度,我试着用GIMP重新导出…

java笔记(30)——反射的 API 及其 使用

文章目录 反射1. 什么是反射2. 获取class字段(字节码文件对象)方式1方式2方式3应用 3. 获取构造方法和权限修饰符前期准备获取所有的公共构造方法获取所有的构造方法获取无参构造方法获取一个参数的构造方法获取一个参数的构造方法获取两个参数的构造方法…

查看Linux系统中用过什么命令 history

提示:以下所有命令都在Ubuntu-24.04-live-server-amd64系统中运行 文章目录 前言一、基本用法:二、查看其他用户历史三、增加时间戳结语 前言 history 命令用于显示和操作当前用户在当前会话中执行过的命令历史记录。它是 Bash shell 的内置命令&#x…

架构设计上中的master三种架构,单节点,主从节点,多节点分析

文章目录 背景单节点优点缺点 主从节点优点缺点 多节点优点缺点 多节点,多backup设计优点缺点 总结 背景 在很多分布式系统里会有master,work这种结构。 master 节点负责管理资源,分发任务。下面着重讨论下master 数量不同带来的影响 单节点 优点 1.设…

PTA:7-194 循环结构 —— 中国古代著名算题。趣味题目:物不知其数。

作者 苑丽红 单位 长春理工大学 中国古代著名算题。原载《孙子算经》:“今有物不知其数,三三数之剩二;五五数之剩三;七七数之剩二。问物几何?”。本题要求:设某物数量是 N,且三三数剩 x,五五…

如何在Ubuntu20上离线安装joern(包括sbt和scala)

在Ubuntu 20上离线安装Joern,由于Joern通常需要通过互联网从其官方源或GitHub等地方下载,但在离线环境中,我们需要通过一些额外的步骤来准备和安装。(本人水平有限,希望得到大家的指正) 我们首先要做的就是…

Excel 快速查询工具 2023.7.1 更新

Excel 快速查询工具作者表示这个软件是因为有时候需要在 Excel 和网站之间进行切换非常的麻烦,这款软件可以以半透明的方框位于桌面上。 特点 软件窗口半透明并至于顶部,无需来回切换界面。 实时查询,不用点击查询或者按回车之类的&#x…

什么是Cookie?有什么用?如何清除浏览器中的Cookie?

互联网上的每一次点击和每一个选择都可能被一种名为Cookie的技术记录下来。但Cookie是什么?我们在网站上登录时,为什么经常会被问及是否接受Cookie?接受Cookie登录会不会影响我们的在线隐私? Cookie是什么? Cookie是一…

vue3 全局引入 onMounted, reactive, ref 的插件全局引入

webpack 的引入 npm install -D unplugin-auto-import const AutoImport require(unplugin-auto-import/webpack).default;configureWebpack: {devtool: source-map,module: {rules: [{test: /\.mjs$/,include: /node_modules/,type: javascript/auto}],}, plugins: [Aut…

LLM——10个大型语言模型(LLM)常见面试题以及答案解析

今天我们来总结以下大型语言模型面试中常问的问题 1、哪种技术有助于减轻基于提示的学习中的偏见? A.微调 Fine-tuning B.数据增强 Data augmentation C.提示校准 Prompt calibration D.梯度裁剪 Gradient clipping 答案:C 提示校准包括调整提示,尽量减少产生…

基于大津阈值法和二值化与联通区域标记进行图像目标计数

1 建模 A.读取图像: 1.使用imread函数读取图像文件。 2.如果图像是彩色的,通常需要先转换为灰度图像,因为大津阈值法通常应用于灰度图像。 B.灰度化: 1.如果图像是彩色的,使用rgb2gray函数将其转换为灰度…

Data-Driven Reinforcement Learning for Robotic Manipulation

意思是 不同的任务以及机器人都有单独的数据和模型 未来需要整合 一个大的数据集包含所有的 然后训练一个大模型 以后具体的任务只需要针对这个模型进行微调 challenge bootstrapping with large data 2 3 4 高清图补充

uniapp, ‍[⁠TypeError⁠]‍ “Failed to fetch dynamically imported module“ 报错解决思路

文章目录 1. 背景2. 报错3. 解决思路4. 思考参考1. 背景 最近基于uniapp开发一款设备参数调试的APP软件,在使用第三方插件的过程中,出现下面的报错。 2. 报错 [plugin:vite:import-analysis] Cannot find module ‘D:/leaning/uniapp/demo/jk-uts-udp示例/uni_modules/uts-…

安装maven与nexus

安装maven与nexus Maven官网下载地址:http://maven.apache.org cd /data/software/wget https://mirrors.tuna.tsinghua.edu.cn/apache/maven/maven-3/3.8.1/binaries/apache-maven-3.8.8-bin.tar.gz# 解压 tar xf apache-maven-3.8.1-bin.tar.gz -C /opt/[rooth…

在Clion使用CubeMX Stm32的步骤

Step1 准备软件,安装环境: 1. cubemx v6.5.0(可以兼容以前版本的project) https://www.st.com.cn/zh/development-tools/stm32cubemx.html STM32CubeMX 默认安装目录, 6.5版本可以兼容老版本 C:\Program Files\STMicroelectroni…

关于响应式编程的理解与SpringCloudGateway的理解

关于响应式编程的理解与SpringCloudGateway的理解 一. 响应式编程与函数式编程的区别二. 响应式编程中常用的组件2.1 RxJava定义2.2 Rxjava基本概念2.3 RxJava 用法 三 SpringcloudGateway四 常见的四种限流规则 一. 响应式编程与函数式编程的区别 总的来说,响应式编…