Swift宏的实现

    上篇介绍了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.mfbz.cn/a/759016.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Redis基础教程(三):redis命令

💝💝💝首先,欢迎各位来到我的博客,很高兴能够在这里和您见面!希望您在这里不仅可以有所收获,同时也能感受到一份轻松欢乐的氛围,祝你生活愉快! 💝&#x1f49…

仓库管理系统11--物资设置

1、添加用户控件 <UserControl x:Class"West.StoreMgr.View.GoodsTypeView"xmlns"http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x"http://schemas.microsoft.com/winfx/2006/xaml"xmlns:mc"http://schemas.openxm…

sqlserver开启CDC

1、背景 由于需要学习flink cdc&#xff0c;并且数据选择sqlserver&#xff0c;所以这里记录sqlserver的cdc开启操作步骤。 2、基础前提 官方介绍地址&#xff1a;https://learn.microsoft.com/zh-cn/sql/relational-databases/track-changes/enable-and-disable-change-dat…

Ubuntu22.04 源码安装 PCL13+VTK-9.3+Qt6,踩坑记录

Ubuntu 22.04LTS;cmake-3.25.0;VTK-9.3;PCL-1.13;Qt6.6 PCL可以通过 apt 命令直接安装(sudo apt install libpcl-dev),apt 命令安装的 VTK 是简略版,没有对 Qt 支持的包,所以笔者使用源码安装 PCL 和 VTK。 1. 安装 VTK 1) 安装 ccmake 和 VTK 依赖项: sudo apt-g…

DataWhale-吃瓜教程学习笔记 (五)

学习视频&#xff1a;第4章-决策树_哔哩哔哩_bilibili 西瓜书对应章节&#xff1a; 第四章 4.1&#xff1b;4.2 文章目录 决策树算法原理- 逻辑角度- 几何角度 ID3 决策树- 自信息- 信息熵 &#xff08;自信息的期望&#xff09;- 条件熵 &#xff08; Y 的信息熵关于概率分布 …

从万里长城防御体系看软件安全体系建设@安全历史03

长城&#xff0c;是中华民族的一张重要名片&#xff0c;是中华民族坚韧不屈、自强不息的精神象征&#xff0c;被联合国教科文组织列入世界文化遗产名录。那么在古代&#xff0c;长城是如何以其复杂的防御体系&#xff0c;一次次抵御外族入侵&#xff0c;而这些防御体系又能给软…

HarmonyOS Next开发学习手册——创建轮播 (Swiper)

Swiper 组件提供滑动轮播显示的能力。Swiper本身是一个容器组件&#xff0c;当设置了多个子组件后&#xff0c;可以对这些子组件进行轮播显示。通常&#xff0c;在一些应用首页显示推荐的内容时&#xff0c;需要用到轮播显示的能力。 针对复杂页面场景&#xff0c;可以使用 Sw…

C++ sizeof的各种

C sizeof的各种 1. 含有虚函数的类对象的空间大小2. 虚拟继承的类对象的空间大小3. 普通变量所占空间大小4. 复合数据类型&#xff08;结构体和类&#xff09;5. 数组6. 类型别名7. 动态分配内存8. 指针9. 静态变量10. 联合体11. 结构体使用#program pack 1. 含有虚函数的类对象…

firewalld防火墙转发流量到其他端口forward port rules

假设云主机eth0: 47.93.27.106 tun0: inet 10.8.0.1 netmask 255.255.255.0 Show rules for a specific zone (public) sudo firewall-cmd --zonepublic --list-all Add the tun0 interface to the public zone: sudo firewall-cmd --zonepublic --add-interfacetun0 --…

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

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

机械设计简单介绍

机械设计简单介绍 1 介绍1.1 概述1.2 机械机构设计基本步骤1.3 关键1.3.1 静力学1.3.2 动力学1.3.3 运动学1.3.4 刚度学 1.4 示例【机械臂】 2 资料2.1 知识体系2.2 博客类汇总2.3 免费CAD模型获取2.4 3D打印2.5 SolidWorks 3 具备能力3.1 熟练翻阅 机械设计手册3.2 知道 N 家常…

【01-02】Mybatis的配置文件与基于XML的使用

1、引入日志 在这里我们引入SLF4J的日志门面&#xff0c;使用logback的具体日志实现&#xff1b;引入相关依赖&#xff1a; <!--日志的依赖--><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId><version&g…

Part 8.3.3 最近公共祖先

两个点的最近公共祖先&#xff0c;即两个点的所有公共祖先中&#xff0c;离根节点最远的一个节点。 【模板】最近公共祖先&#xff08;LCA&#xff09; 题目描述 如题&#xff0c;给定一棵有根多叉树&#xff0c;请求出指定两个点直接最近的公共祖先。 输入格式 第一行包含…

VMware虚拟机安装CentOS7.9 Oracle 11.2.0.4 RAC+单节点RAC ADG

目录 一、参考资料 二、RAC环境配置清单 1.主机环境 2.共享存储 3.IP地址 4.虚拟机 三、系统参数配置 1. 配置网卡 1.1 配置NAT网卡 1.2 配置HostOnly网卡 2. 修改主机名 3. 配置/etc/hosts 4. 关闭防火墙 5. 关闭Selinux 6. 配置内核参数 7. 配置grid、oracle…

vue3:星星评分组件

一、效果 二、代码 子组件stars.vue&#xff1a; <template><div class"stars"><div class"star" v-for"star in stars" :key"star" click"setScore(star)"><svgt"1719659437525"class&qu…

贪心算法题目总结

1. 整数替换 看到这道题目&#xff0c;我们首先能想到的方法就应该是递归解法&#xff0c;我们来画个图 此时我们出现了重复的子问题&#xff0c;就可以使用递归&#xff0c;只要我们遇到偶数&#xff0c;直接将n除以2递归下去&#xff0c;如果是奇数&#xff0c;选出加1和减1中…

面试框架一些小结

springcloud的⼯作原理 springcloud由以下⼏个核⼼组件构成&#xff1a; Eureka&#xff1a;各个服务启动时&#xff0c;Eureka Client都会将服务注册到Eureka Server&#xff0c;并且Eureka Client还可以反过来从Eureka Server拉取注册表&#xff0c; 从⽽知道其他服务在哪⾥ …

Java+JSP+Mysql+Tomcat实现Web图书管理系统

简介&#xff1a; 本项目是基于springspringmvcJdbcTemplate实现的图书馆管理系统&#xff0c;包含基本的增删改查功能&#xff0c;可作为JavaWeb初学者的入门学习案例。 环境要求&#xff1a; java8 mysql5.7及以下 eclipse最新版 项目目录 模块设计 页面设计 1. 登录页…

【Spring Boot】认识 JPA 的接口

认识 JPA 的接口 1.JPA 接口 JpaRepository2.分页排序接口 PagingAndSortingRepository3.数据操作接口 CrudRepository4.分页接口 Pageable 和 Page5.排序类 Sort JPA 提供了操作数据库的接口。在开发过程中继承和使用这些接口&#xff0c;可简化现有的持久化开发工作。可以使 …

汽车尾灯(转向灯)电路设计

即当汽车进行转弯时,司机打开转向灯,尾灯会根据转向依次被点亮,经过一定的间隔后,再全部被消灭。不停地重复,直到司机关闭转向灯。 该效果可由以下电路实现: 完整电路图: 02—电路设计要点 延时电路的要点主要有两个: 一、当转向开关被按下时,LED需要逐个亮起; 二、LED被逐…
最新文章