资讯动态

【仓颉语言入门 · 第二篇 第 18 课】

发布时间:2026/10/30 5:03:08 来源:尧图企业网站定制
【仓颉语言入门 · 第二篇 第 18 课】deriving 宏实战自动生成 toString、equals 与 JSON上一课我们打下了宏的地基宏包怎么建、Tokens怎么拼、quote怎么插值、parseDecl怎么读语法树。这一课把这些零件组装成一件真正有用的工具——deriving 宏。写个class User标上一行DeriveToStringtoString就自动长出来了再标DeriveEquals、DeriveJson比较运算符和 JSON 序列化也有了。全程在编译期完成运行时零开销。学完这一课你就拥有了一个能给自己类批量发方法的小框架。本文所有代码、输出与报错文案均在仓颉 SDK 1.2.0 下逐行实测编译运行。目录第二篇导航第一篇《仓颉语言入门》共 30 课已完结环境搭建 → 语法 → 函数 → 类型系统 → 工程化 → 并发 → 两场实战。第二篇在更高的起点上继续共26 课、4 个模块模块课次内容一、补遗篇0106正则、时间日期、命令行参数、随机数、加密哈希、二进制处理二、数据库篇0715入门与连接、建表、查询、插入、更新删除、预编译防注入、事务、手写 DAO、综合小实战三、进阶硬核篇1622反射、宏基础、deriving 实战、FFI 基础、FFI 进阶、并发容器、对象池四、实战项目篇2326HTTP 基础、HTTP 进阶、多线程日志分析器、简易爬虫正则表达式std.regex时间日期处理std.time命令行参数解析std.argopt随机数与概率std.random加密与哈希std.crypto二进制数据处理std.binary数据库入门认识 std.database.sql连上 MySQL建表用代码定义表结构查询SELECT 与结果集遍历插入INSERT、占位符与自增主键更新与删除UPDATE / DELETE预编译与防 SQL 注入事务批量操作的成败一致手写 DAO 数据访问层数据库综合小实战反射在运行时打量类型宏基础std.ast 与 quotederiving 宏实战本文与 C 互操作基础FFI 调用 C 函数FFI 进阶结构体、指针与编译配置并发容器并发安全的集合对象池与资源复用实战三HTTP 服务端基础实战四HTTP 进阶与 JSON 接口实战五多线程日志分析器实战六简易爬虫一、deriving一次写宏处处生成给类写toString是件枯燥活。每个类的写法都一样区别只在字段列表class User { let id: Int64 var name: String init(id: Int64, name: String) { this.id id this.name name } public func toString(): String { return User(id ${id}, name ${name}) } }字段加几个这串模板就重抄一遍equals、toJson同理。写一两个类能忍写二十个就是体力活——和第 16 课开头的问题一模一样。别的语言早有答案Rust 用#[derive(Debug)]Java 生态用 Lombok 的Data套路都是在类上打一个标注编译器照着字段列表自动生成样板方法。这类标注有个专门的名字deriving派生。仓颉没有内置 derive但有宏。上一课的属性宏正好能干这事把DeriveToString标在类上宏拿到的input就是整个类声明的 Tokens用parseDecl拆开它、读出字段名和类型再把原类声明 生成的方法一起还给编译器。一次写好所有类都能用。二、工程准备沿用上一课的宏工程模式宏必须放在独立的宏包里宏定义与调用不能同包。新建工程cjpm init --name cjderive目录结构macros是src的子目录对应宏包cjderive.macroscjderive/ ├── cjpm.toml └── src/ ├── main.cj └── macros/ └── derive.cjsrc/macros/derive.cj宏包第一行写macro package cjderive.macros里面放三个 deriving 宏src/main.cj普通包package cjderiveimport cjderive.macros.*后用宏名标注类三、第一步读出类名和字段写宏之前先在普通程序里把拆类声明这件事练熟parseDecl在普通main里也能跑。把下面代码贴进src/main.cjpackage cjderive import std.ast.* main() { let decl parseDecl(quote( class User { let id: Int64 var name: String } )) if (let Some(c) - (decl as ClassDecl)) { println(类名: ${c.identifier.value}) println(字段数: ${c.body.decls.size}) for (d in c.body.decls) { if (let Some(v) - (d as VarDecl)) { println(${v.identifier.value}: ${v.declType.toTokens()}) } } } }运行输出类名: User 字段数: 2 id: Int64 name: String这条链路上的四个节点就是 deriving 宏的全部原料节点拿到什么parseDecl(tokens)把类声明的 Tokens 解析成Decldecl as ClassDecl转成类声明节点as外面要加括号c.body.decls类体内的成员声明列表字段、构造器、方法都在里面d as VarDecl筛出字段identifier.value是字段名declType.toTokens()是字段类型init、成员函数也是body.decls里的元素只是它们不是VarDeclas转换得到None自然被过滤掉——所以遍历出来的恰好全是字段。四、DeriveToString自动生成 toString思路宏返回input quote(...)。input是原类声明原样保留quote(...)里生成一段extend给类补上toString。仓颉的类不能在声明外直接加成员函数但extend可以——扩展写在类外效果和写在类里一样。extend User : ToString还能顺带实现ToString接口这样println(u)就能直接用。打开src/macros/derive.cj// src/macros/derive.cj macro package cjderive.macros import std.ast.* public macro DeriveToString(input: Tokens): Tokens { let c (parseDecl(input) as ClassDecl).getOrThrow() let className c.identifier.value var parts var first true for (d in c.body.decls) { if (let Some(v) - (d as VarDecl)) { let f v.identifier.value if (!first) { parts , } parts ${f} \${${f}} first false } } let template ${className}(${parts}) return input quote( extend $(c.identifier) : ToString { public func toString(): String { return $(template) } } ) }src/main.cj// src/main.cj package cjderive import cjderive.macros.* DeriveToString class User { let id: Int64 var name: String init(id: Int64, name: String) { this.id id this.name name } } main() { let u User(1, zhangsan) println(u) }运行User(id 1, name zhangsan)宏体里最绕的一行是parts ${f} \${${f}}。它在宏里拼一个字符串这个字符串将来是生成代码里的字符串模板所以有两层转义\是字符串里普通的引号转义本例没用到\${输出字面量${不触发宏里的插值${f}正常插值取宏里变量f的值字段是id时拼出来的是id ${id}这串文字。攒齐所有字段后template的值是User(id ${id}, name ${name})。再经quote的$(template)插进模板String类型的插值会变成带引号的字符串字面量于是生成代码里的return User(id ${id}, name ${name})——${id}在生成代码运行时才求值正好是我们想要的。五、DeriveEquals自动生成 和 !toString拼的是字符串equals拼的是代码this.id rhs.id this.name rhs.name。这段条件表达式不能再用 String 插值——$(字符串)插进quote只会变成字符串字面量不会变成可执行代码。解决办法是上一课见过的cangjieLex先把字符串词法分析回 Tokens再插进quote就是真正的代码了。在derive.cj末尾追加public macro DeriveEquals(input: Tokens): Tokens { let c (parseDecl(input) as ClassDecl).getOrThrow() var conds var first true for (d in c.body.decls) { if (let Some(v) - (d as VarDecl)) { let f v.identifier.value if (!first) { conds } conds this.${f} rhs.${f} first false } } return input quote( extend $(c.identifier) { public operator func (rhs: $(c.identifier)): Bool { return $(cangjieLex(conds)) } public operator func !(rhs: $(c.identifier)): Bool { return !(this rhs) } } ) }注意!必须自己写。仓颉不会从自动推导!漏掉它用u1 ! u3时编译器直接报error: invalid binary operator ! on type Class-User and Class-User # note: you may want to implement operator func !(right: Class-User) for type Class-User给类标上DeriveEquals堆在DeriveToString下面即可属性宏可以叠多个。第五节把main换成三个对象、四次打印main() { let u1 User(1, zhangsan) let u2 User(1, zhangsan) let u3 User(2, lisi) println(u1) println(u1 u2) println(u1 u3) println(u1 ! u3) }实测输出User(id 1, name zhangsan) true false true对应关系println(u1)走toStringu1 u2字段全同u1 u3字段不同u1 ! u3走!。如果照搬第四节的main、只留println(u1)编译时会提示note: unused function:!!生成了却没有被调用。这是编译期警告不是错误程序照常运行补上一行u1 ! u3就会消失。六、DeriveJson按类型生成 JSON再进一步生成toJson(): String输出{id: 1, name: zhangsan}。难点只有一个——JSON 里字符串值要带引号数字不带。所以宏要读字段类型String字段的值两侧补\其余直接插值。继续追加public macro DeriveJson(input: Tokens): Tokens { let c (parseDecl(input) as ClassDecl).getOrThrow() var parts var first true for (d in c.body.decls) { if (let Some(v) - (d as VarDecl)) { let f v.identifier.value let t v.declType.toTokens().toString() if (!first) { parts , } if (t String) { parts \${f}\: \\${${f}}\ } else { parts \${f}\: \${${f}} } first false } } let template {${parts}} return input quote( extend $(c.identifier) { public func toJson(): String { return $(template) } } ) }v.declType.toTokens().toString()把字段类型变成字符串Int64、String宏在编译期按它分流。给User再叠一个DeriveJson调用println(u1.toJson()){id: 1, name: zhangsan}这是教学版的最小实现只管字段是字符串还是其他不处理嵌套对象、数组和转义字符。真上生产要么在宏里继续完善这些分支要么换成成熟的 JSON 库——但读字段、按类型拼模板的骨架思路完全相同。七、让报错更友好diagReport上面的宏有个粗糙处第一行(parseDecl(input) as ClassDecl).getOrThrow()假定使用者只会标在类上。万一有人标到函数上DeriveToString func add(a: Int64, b: Int64): Int64 { a b }编译期宏直接炸出异常堆栈NoneValueException at cjderive.macros::DeriveToString(std.ast::Tokens)(...) error: macro evaluation has failed for macro call DeriveToString使用者根本看不懂自己错在哪。std.ast提供了diagReport让宏主动报告一条规范的编译错误。把开头的转型抽成一个公共函数放进derive.cjfunc requireClass(input: Tokens): OptionClassDecl { match (parseDecl(input) as ClassDecl) { case Some(c) Some(c) case None diagReport(DiagReportLevel.ERROR, input, this macro can only be applied to class, apply it to a class declaration) None } }每个宏开头改成public macro DeriveToString(input: Tokens): Tokens { if (let Some(c) - requireClass(input)) { // ……原来的生成逻辑…… return input quote(...) } return input }注意if里的c已经由requireClass绑定原来那行let c (parseDecl(input) as ClassDecl).getOrThrow()必须删掉。保留它会报error: redefinition of declaration c note: c is previously declared here三个宏都要按这个方式改删掉开头那行let c ...直接用条件里绑定出来的c末尾补return input。diagReport四个参数级别DiagReportLevel.ERROR、出错的 Tokens、错误消息、修复建议。报完 ERROR 编译本就失败宏把input原样返回即可。再编译那段错误用法error: this macro can only be applied to class src\main.cj:7:1: 7 | func add(a: Int64, b: Int64): Int64 { | _^ 8 | | a b 9 | | } | |_^ apply it to a class declaration错误位置精确指到函数声明消息和建议都是人话。三个宏共用一个requireClass这就是完整版derive.cj的最终形态。八、常见错误与避坑错误写法现象宏标在函数 / 结构体上无校验版宏内getOrThrow()抛NoneValueException报macro evaluation has failed只生成没生成!invalid binary operator ! on type ...!不会自动推导用$(字符串)往 quote 里插代码得到的是字符串字面量不是可执行代码代码要先cangjieLex成 Tokens空字段类标DeriveEqualsconds为空生成return后面没表达式报mismatched types: expected Bool, found Unit生成代码本身不合法报错落在宏展开处编译器会附注the error originates in the macro并提示可用--debug-macro查看更多最后一条值得展开宏生成的代码仍然要过类型检查。报错位置显示在宏名那一行指向的是展开后的代码第一眼可能陌生按消息找生成逻辑里的问题即可。九、常用 API 速查需求API解析类声明parseDecl(tokens) as ClassDecl类名classDecl.identifier.value类成员列表classDecl.body.decls筛字段decl as VarDecl字段名varDecl.identifier.value字段类型varDecl.declType.toTokens().toString()字符串转代码 TokenscangjieLex(str)生成扩展quote(extend $(c.identifier) { ... })扩展中实现接口extend X : ToString { ... }宏内报编译错误diagReport(DiagReportLevel.ERROR, tokens, 消息, 建议)模板里输出字面量${\${十、课后练习规矩不变前 4 题必做第 5 题选做。每题只考本课 12 个知识点题面给全函数签名和 API 提示先看懂题再动笔。练习 24 依赖本课写好的三个宏请先在cjderive工程里把derive.cj写完。第 1 题必做拆出类声明在普通main里用parseDecl解析下面的类打印类名和每个字段的名字 类型。提示parseDecl(quote(...))得到Declif (let Some(c) - (decl as ClassDecl))转换字段是VarDeclv.identifier.value是名v.declType.toTokens()是类型。package cjderive import std.ast.* main() { let decl parseDecl(quote( class Point { let x: Int64 let y: Int64 } )) // 补全转成 ClassDecl打印类名、逐字段打印 }期望输出类名 Point x: Int64 y: Int64打印格式按类名 ${...}和${名}: ${类型}自行对齐即可。第 2 题必做给 Book 生成 toString定义Book类字段title: String、price: Int64构造器两参数标上DeriveToString创建Book(仓颉语言入门, 59)并直接println。提示import cjderive.macros.*extend已帮你实现ToString所以不需要手写toString。期望输出Book(title 仓颉语言入门, price 59)第 3 题必做比较两个 Book给第 2 题的Book再叠DeriveEquals创建三本书b1与b2内容相同b3不同。打印b1 b2和b1 ! b3。提示属性宏直接往下叠一行即可!由宏一并生成不用自己写。期望输出true true第 4 题必做Book 转 JSON给第 2 题的Book再叠DeriveJson打印b1.toJson()。提示String字段的值会自动带引号Int64不会这就是宏里按declType分流的意义。期望输出{title: 仓颉语言入门, price: 59}第 5 题选做自己动手写一个 DeriveIntro在derive.cj末尾追加宏DeriveIntro标在类上后生成public func intro(): String返回固定格式的自我介绍。然后在Userid: Int64、name: String上使用并打印u.intro()。提示骨架照抄DeriveToString——用requireClass或(parseDecl(input) as ClassDecl).getOrThrow()拿到ClassDecl遍历body.decls筛出VarDecl边遍历边拼字段名串、边计数最后quote里extend出intro方法。public macro DeriveIntro(input: Tokens): Tokens { // 补全 }期望输出I am User, I have 2 fields: id, name下节预告这一课我们把宏用到了实处三个 deriving 宏让任何类标一行就拥有toString、/!和toJson。回头看第 16 课的反射——同样是按字段列表批量处理反射在运行时查找、有开销、错误暴露晚宏在编译期一次生成、运行时零开销、字段写错直接编译报错。两者各有适用场景但样板代码这件事宏是更优解。下一课进入FFI 基础与 C 互操作。仓颉可以用foreign func直接调用 C 函数我们会从环境配置讲到第一个跨语言调用——这也是 CIDE 的csrc目录约定在背后支撑的能力。系列说明本系列基于 Windows 平台 CIDE 仓颉 SDK1.2.0编写。本文全部代码、运行输出与报错文案均已在 SDK 1.2.0 下逐块编译运行、逐字实测如遇 SDK 版本差异导致的细节出入以你本地版本为准欢迎评论区交流。 遇到问题扫码联系作者跟着课程练习时如果在 SDK 安装、环境变量配置、编译报错或调试上卡住欢迎扫码加作者企业微信直接咨询请备注仓颉课程离线环境下图片可能加载不出来也可以在 CIDE 菜单Help ▸ 联系作者 / Contact中查看同一张二维码应用内置兜底图无需联网。 工具下载本系列全程使用的仓颉 IDE ——CIDE免费开源、社区版GitCode 仓库 / 安装包下载https://gitcode.com/wp_upala/cide打开页面后进入发行版Releases两种包任选其一安装版下载CIDE-版本-x64-Setup.exe双击安装适合日常长期使用免安装版Portable下载CIDE-版本-x64-Portable.zip解压到任意目录即用不写注册表、不留安装痕迹拷到 U 盘也能在别的电脑直接运行包内附《使用说明.txt》。适合先试用、或在受限电脑上学习本系列课程。仓颉 SDK 请前往仓颉编程语言官网下载https://cangjie-lang.cn

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑