feat: release Dever 0.1.4 with enforceable project guidance
This commit is contained in:
1 parent
201bdee372
commit
89629c2197
86 files changed
+2798
-338
No files matched your search
+47
-41
@@ -72,7 +72,7 @@ Linux 开发环境运行构建产物:
|
||||
```
|
||||
|
||||
- `new`:从随版本验证的模板原子创建新项目,不覆盖已有目录;`--markdown` 生成等价 Markdown 源码。
|
||||
- 机器版 `update`:从官方仓库的 Releases 显式下载并校验,统一更新活动核心与对应 skill;不修改业务配置。`skill path [project-root]` 返回经验证的配套指引,`skill install <new-directory>` 安装稳定的 AI 加载入口。项目可锁定 dever.version,普通项目命令不会触发更新。
|
||||
- 机器版 `update`:从官方仓库的 Releases 下载并校验,更新核心与配套 skill,不改业务项目文件。`skill path [project-root]` 返回对应版本的指引。在项目根目录运行 `skill install`,自动向 `AGENTS.md` 写入或更新 Dever 规则块,保留其他内容,无需指定技能目录。`new` 自动生成同一份规则。项目可用 `dever.version` 锁定版本;普通项目命令不会触发更新。
|
||||
- `check`:加载生产源码,完成语法、名称、类型、分句及应用入口可达性和冗余硬检查;不执行应用或准备运行资源。
|
||||
- `fmt --check`:只检查格式,不修改文件。
|
||||
- `fmt`:统一格式化项目 `module/` 和存在的 `test/`;需要修复格式时单独执行 `dever fmt my_app`。
|
||||
@@ -105,7 +105,7 @@ greet(name: Text) (response: Greeting) {
|
||||
- App 能力的输入、输出和向外传播的错误都属于公开契约,不能引用 Domain、Adapter 等私有类型。业务错误类型应声明在 App,Domain 可通过 `app.ErrorType.Variant` 使用它。
|
||||
- `domain`、`model`、`port`、`adapter`、`api` 和 `job` 都是领域私有实现。跨领域、跨组件只能调用目标 App;API 和 Job 入口不是普通源码调用目标。
|
||||
- App 可调用本领域 Domain、Port 及其他领域 App;API 只能调用同领域 App。Domain 只能调用同领域 Domain。Adapter 只能调用同文件 helper 与标准能力,不能反调 App、Domain、Model、Port 或其他 Adapter。Model 操作仍只能由所属领域 App 使用静态 DSL 执行。
|
||||
- 同一 role 的单文件与同名目录二选一。非 API 的 topic 目录保持一层;只有 `api/` 可递归。role 目录只有一个源码文件时必须合回 role 文件,避免无意义拆分。
|
||||
- App、Domain、Port、Adapter、Job 的单文件与同名目录二选一,目录至少包含两个内聚主题文件。Model、API 可以同时使用角色主文件和主题目录,不受最少文件数限制。非 API 的主题目录保持一层,只有 `api/` 可递归。
|
||||
- 一个文件可放一组内聚类型和函数,不按函数机械拆文件。禁止用 `common`、`shared`、`utils`、`helper`、`base`、`service` 作为业务源码桶。
|
||||
- `port` 只声明类型和无函数体的操作合同,必须写 `fails app.ErrorChoice`;错误 choice 至少包含一个显式 error 分支,也可引用公开标准错误 choice。输入必须是无约束的明确值类型,输出同样不加范围约束。当前 Port 不接受 handler 参数。App/Domain 的外部文件、网络 I/O 必须经过 Port;日志、时间、随机数等已有能力保持原约束。
|
||||
- 没有 `import`、`use`、通配导入或别名。
|
||||
@@ -315,7 +315,11 @@ register() () {
|
||||
|
||||
`assert` 只接受 Bool;`assert_eq` 要求两侧类型完全相同且可比较,不做隐式数值转换。失败报告测试身份、原始源码位置和实际/期望值。业务失败继续用 `result(call)` 与 choice 分句验证,不增加异常断言语法。
|
||||
|
||||
`dever test <project-root>` 在执行任何用例前检查全部生产和测试源码,并把所有测试入口编译成一个可缓存的原生测试程序;每个测试仍在独立原生进程与临时项目中运行。不使用 Model 的测试不读取数据库配置或初始化数据库;使用 Model 的测试走正式迁移、Seed、事务和关闭流程,但配置由运行器在临时目录生成且只使用 SQLite。测试命令不会读取项目 `config/setting.json` 或项目 `data/`,也不从环境变量接收数据库覆盖;成功和失败都会清理临时文件。首版不提供共享 setup、筛选、并行、重试或自动超时。
|
||||
`dever test <project-root>` 在执行任何用例前检查全部生产和测试源码,并把所有测试入口编译成一个可缓存的原生测试程序;每个测试仍在独立原生进程与临时项目中运行。不使用 Model 的测试不初始化数据库;使用 Model 的测试走正式迁移、Seed、事务和关闭流程,但数据库配置由运行器在临时目录生成且只使用 SQLite。CLI 仍读取 `config/setting.json` 与锁中的依赖元数据,测试执行不会连接其中的部署数据库或使用项目 `data/`,也不从环境变量接收数据库覆盖;成功和失败都会清理临时文件。
|
||||
|
||||
定向运行使用 `--filter <component[.domain[.topic]]>`,例如 `--filter user.account` 或 `--filter user.account.register`。它按完整身份或点分段前缀选择用例,不使用子串或通配符;无匹配时明确失败。筛选不跳过其他源码的检查,不改变原生 suite 的索引;未选择用例计入 `not run`。报告仍显示原有的 `component/domain/topic` 路径身份。
|
||||
|
||||
每例默认最多运行 60 秒,`--timeout-ms <正整数毫秒>` 可覆盖;限时不包含测试程序的编译。超时用例失败,运行器终止并回收本例拥有的进程组和后代,然后继续后续选中用例。任一用例失败或超时,命令返回非零。测试仍按顺序执行,不提供共享 setup、并行或重试。
|
||||
|
||||
## 3. 类型与值
|
||||
|
||||
@@ -840,7 +844,7 @@ dever.json.element(dever.json.value.Document, array_id: Int, index: Int) -> Int?
|
||||
|
||||
`Document` 用整数 ID 保存一棵 JSON 树:`root` 是根节点 ID;`nodes` 的 ID 从 0 连续递增;子节点 ID 小于父节点 ID。Object 将属性名映射到子节点 ID,Array 保存有序子节点 ID。`property` 和 `element` 返回节点 ID,再通过 `node` 取得 Value;缺失、类型不符或越界时返回 `null`。JSON 的 `null` 则是实际存在的 `Value.Null`,两者不同。
|
||||
|
||||
数字保留原始 JSON 文本,避免先丢失精度。解析失败位置是从 0 开始的 Unicode codepoint 偏移,包括输入末尾位置。重复键、非法转义、未配对代理项、多余输入和超过 256 层的嵌套会传播 `ParseResult.Failed`。手工构造 Document 时,`stringify` 会检查根节点、连续 ID、子节点引用和输出规模,失败时传播 `WriteResult.Failed`;两者都可用 `result` 显式捕获。完整的解析和写回分句见 [标准库示例](examples/old/dever/library/module/main.dever)。
|
||||
数字保留原始 JSON 文本,避免先丢失精度。解析失败位置是从 0 开始的 Unicode codepoint 偏移,包括输入末尾位置。重复键、非法转义、未配对代理项、多余输入和超过 256 层的嵌套会传播 `ParseResult.Failed`。手工构造 Document 时,`stringify` 会检查根节点、连续 ID、子节点引用和输出规模,失败时传播 `WriteResult.Failed`;两者都可用 `result` 显式捕获。当前函数和失败类型见 [JSON 标准包](library/dever/json.dever)及 [JSON 值类型](library/dever/json/value.dever)。
|
||||
|
||||
### HTTP
|
||||
|
||||
@@ -903,7 +907,7 @@ start(listener: dever.system.Listener) () {
|
||||
}
|
||||
```
|
||||
|
||||
先用 `dever.net.listen(...)` 创建 Listener,再调用 serve;编译器会自动等待网络结果。完整启动分句见 [HTTP 示例](examples/old/dever/http/module/main.dever)。serve 持续接收连接,可作为 `server = run(dever.http.serve(...))` 启动,再用 `stop(server)` 取消并等待子任务清理。调用者持有的 Listener 别名仍由调用者关闭。
|
||||
在 Adapter 中先用 `dever.net.listen(...)` 创建 Listener,再调用 serve;编译器会自动等待网络结果。serve 持续接收连接,可作为 `server = run(dever.http.serve(...))` 启动,再用 `stop(server)` 取消并等待子任务清理。调用者持有的 Listener 别名仍由调用者关闭。普通应用的 HTTP 入口用 [模块 API](#模块-api) 声明,无需自行编写服务启动函数。
|
||||
|
||||
- 服务端先预留任务容量,再接受连接;没有无界应用队列或每连接独占线程。每条 HTTP/1 连接按顺序执行 handler。
|
||||
- 握手超时或底层 I/O 错误关闭该连接;正文超时返回 408,handler 超时返回 504,正文超限返回 413。HTTP/1 随后关闭连接,HTTP/2 结束对应流。写入停滞超过期限关闭连接。
|
||||
@@ -961,7 +965,7 @@ WebSocket Limits 默认 `message_bytes = 1048576`、`idle_ms = 60000`、`write_m
|
||||
|
||||
应用需持续调用 receive/messages 才能读取协议帧;收到 Ping 时引擎先发送 Pong,再交给应用。库不建立额外的定时 Ping 任务,可通过 send(Ping(...)) 实现应用保活。close 校验合法状态码和最多 123 UTF-8 字节的原因,等待对端关闭握手;关闭会唤醒等待中的读写。当前不提供关闭帧元数据、子协议协商或压缩扩展。
|
||||
|
||||
完整示例见 [live_http](examples/old/dever/live_http/module/main.dever):`/stream` 分段输出,`/events` 推送三条 SSE 事件,`/ws` 回显文本和二进制消息。
|
||||
这些底层操作用于 Adapter,类型和函数定义见 [HTTP](library/dever/http.dever)、[SSE](library/dever/sse.dever)和 [WebSocket](library/dever/websocket.dever)标准包。
|
||||
|
||||
### HTTP 连接池与流式客户端
|
||||
|
||||
@@ -1026,7 +1030,7 @@ route(request: dever.http.Request, context: Context) (response: dever.http.Respo
|
||||
|
||||
http_serve_live、http_serve_tls 和 http_serve_live_tls 也接受这个末尾参数;live handler 的输入顺序为 request、reply、context,TLS 配置位于 context 之前。这里直接绑定静态 handler,不引入闭包或动态路由解释器。
|
||||
|
||||
优雅停机先调用 `dever.net.close_listener(listener)` 并处理结果,再等待 server Task。HTTP 停止接收新连接,HTTP/2 同时发送 GOAWAY,已开始的响应继续完成;WebSocket/SSE 会话仍由现有 handler 拥有。客户端收到 GOAWAY 后不再向该连接派发新请求,也不自动重放已发送请求。使用 `timeout(server, grace_ms, expired)` 给排空设置上限,expired 是零输入、零输出的静态 handler;到期先停止剩余任务并等待清理。需要立即停止时使用 `stop(server)`。完整可运行用法见 [backend 示例](examples/old/dever/backend/module/main.dever)。
|
||||
优雅停机先调用 `dever.net.close_listener(listener)` 并处理结果,再等待 server Task。HTTP 停止接收新连接,HTTP/2 同时发送 GOAWAY,已开始的响应继续完成;WebSocket/SSE 会话仍由现有 handler 拥有。客户端收到 GOAWAY 后不再向该连接派发新请求,也不自动重放已发送请求。使用 `timeout(server, grace_ms, expired)` 给排空设置上限,expired 是零输入、零输出的静态 handler;到期先停止剩余任务并等待清理。需要立即停止时使用 `stop(server)`。自动 API 服务的启动与停机由核心负责,配置见 [模块 API](#模块-api)。
|
||||
|
||||
### 模块 API
|
||||
|
||||
@@ -1390,61 +1394,63 @@ TCP 的 `connect`、`connect_timeout`、`listen`、`accept`、`read`、`write`
|
||||
|
||||
## 15. 完整可运行示例
|
||||
|
||||
[示例索引](examples/README.md) 列出可运行示例及编译拒绝反例,提供每个项目的入口、预期结果和运行边界。每个示例目录都是独立项目根,源码位于其 `module/`。
|
||||
完整业务示例是 [CMS](examples/README.md),同时维护普通源码和 Markdown 两套实现。它展示账号、站点、权限、租户隔离、文章编辑与发布、文件上传和定时任务。每套都是独立项目,源码在 `module/`,测试在 `test/`。
|
||||
|
||||
当前完整示例位于 [examples/old/dever/library/module/main.dever](examples/old/dever/library/module/main.dever)。它把以下能力放在同一个程序中:
|
||||
在仓库根目录检查两套实现:
|
||||
|
||||
- `package` 与公开零输入入口;
|
||||
- 命名输出;
|
||||
- `List<Int>` 和 `reduce`;
|
||||
- JSON 解析、选择型失败分句与重新编码;
|
||||
- Text、Int 和 Unicode。
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
dever check examples/old/dever/library
|
||||
dever fmt examples/old/dever/library --check
|
||||
dever run examples/old/dever/library
|
||||
```sh
|
||||
dever check examples/cms/dever
|
||||
dever check examples/cms/md
|
||||
dever fmt examples/cms/dever --check
|
||||
dever fmt examples/cms/md --check
|
||||
dever test examples/cms/dever --filter news.article.publish
|
||||
dever test examples/cms/md --filter news.article.publish
|
||||
```
|
||||
|
||||
输出:
|
||||
这些测试使用临时 SQLite,不连接部署数据库。实际运行 CMS 会使用部署配置和数据,先按示例说明完成密钥、账号、租户数据库和 Owner 配置。
|
||||
|
||||
```text
|
||||
json = {"message":"你好 Dever","total":6}
|
||||
```
|
||||
只学习某项能力时,用随 skill 提供的独立小项目:
|
||||
|
||||
其他针对性示例:
|
||||
| 场景 | 项目 |
|
||||
| --- | --- |
|
||||
| HTTP、自动 REST、身份核验 | [http-rest](skills/assets/examples/http-rest/) |
|
||||
| 持久 Job、事务入队、虚拟时钟测试 | [jobs](skills/assets/examples/jobs/) |
|
||||
| Python、JavaScript、Go 接入与 Port fake | [external-lib](skills/assets/examples/external-lib/) |
|
||||
|
||||
- [examples/old/dever/core/module/main.dever](examples/old/dever/core/module/main.dever):用户 type、选择型、可空值、数值范围分句、List、Map、值语义和数值规则。
|
||||
- [examples/old/dever/stream/module/main.dever](examples/old/dever/stream/module/main.dever):文件分块读取、Stream 和 `reduce`。
|
||||
- [examples/old/dever/contracts/module/main.dever](examples/old/dever/contracts/module/main.dever):受约束的端口、明确失败、主动恢复、纯函数和包依赖。
|
||||
- [examples/old/dever/http/module/main.dever](examples/old/dever/http/module/main.dever):Hyper HTTP/1 服务及静态 route。
|
||||
- [examples/old/dever/async/module/main.dever](examples/old/dever/async/module/main.dever):Task、Group、Channel、非阻塞定时、`parallel` 和 `blocking`。
|
||||
- [examples/old/dever/backend/module/main.dever](examples/old/dever/backend/module/main.dever):源码运行参数、共享 Context、HTTP 连接池和优雅停机;请求一次后自动退出。
|
||||
- [examples/cms/dever/module/news/article/api/admin/manage.dever](examples/cms/dever/module/news/article/api/admin/manage.dever):生成的 admin REST/业务入口、站点认证和租户文章事务。
|
||||
各项目的命令和验证范围见 [示例指引](skills/references/examples.md)。新建最小项目使用 `dever new`,不从历史代码猜语法。
|
||||
|
||||
## 16. 当前未实现
|
||||
`examples/old/` 只保存历史语言回归材料,不是当前应用模板,不能直接按应用执行 `check/run/build`。其中的 `main.dever`、`package` 和旧可见性写法不用于新项目。
|
||||
|
||||
以下能力不能写进当前 Dever 程序:
|
||||
## 16. 语言边界与平台支持
|
||||
|
||||
### 不提供的语法
|
||||
|
||||
Dever 用分句、类型化失败和集合操作表达控制流程。以下写法不属于当前语言,检查时会明确报错:
|
||||
|
||||
- `if`/`else`、`switch`、`match` 和条件表达式;条件永久使用 function 分句。
|
||||
- `for`、`while`、`break`、`continue` 和用户递归;重复使用集合/Stream 组合器。
|
||||
- class、继承、interface、method、annotation、macro、unsafe 和 FFI。
|
||||
- lambda、闭包、一般化一等 function、用户泛型 type/function。
|
||||
- exception、`throw` 和 `catch`;可恢复错误使用推导式默认传播、`result(call)` 和 `fail(...)`。
|
||||
|
||||
### 当前能力边界
|
||||
|
||||
Linux x86_64 已提供签名安装包、机器共享编译服务和配套 skill,也能生成 Linux ARM64 应用。API/CMD、Model、权限、数据库租户隔离、Job、应用测试和外部 Lib 都使用同一套语言合同;运行、构建不隐式下载依赖。
|
||||
|
||||
当前限制如下:
|
||||
|
||||
- 通用动态 Json 值、Set 和 BigInt;持久化 JSON 使用 Model 的 `Json` 字段,普通 JSON 处理使用 `dever.json.Document`。
|
||||
- CONNECT、非空 trailer、服务器推送、WebSocket-over-HTTP/2,以及 WebSocket 子协议、压缩扩展和关闭帧元数据。
|
||||
- 正式签名 runtime/build packs、共享 daemon 编译及跨平台发行验收。现有 LLVM 内核覆盖受管值、静态 handler、Result、文件/流、真实协程与并发组合、TCP/HTTP/TLS/SSE/WebSocket 及系统能力;独立应用入口已接通 CMD→App→Dever/external Port/Adapter、SQLite/PostgreSQL Model/事务,以及 HTTP/REST、可信身份、自动权限、database-only 租户与组件状态、Cookie/Header、Upload 存储和日志配置。持久 Job 和应用 Test 复用现有语义,Test 保持一次编译及每用例进程/fake/数据库/时钟隔离。external 复用统一 Worker 协议、类型化数据与声明业务错误,以及校验后提取的内嵌资源。Worker 与其他资源归属每次入口:退出先关闭 Worker,再排空子任务并关闭数据库等资源。私有 `dever run/build/test` 默认使用 LLVM 与显式 runtime pack,不读取后端环境变量或调用宿主 Rust/Cargo/cc;managed `dever-core` 仍等待可信 daemon 编译入口,不能把本机定向验收当作正式或全平台发行。
|
||||
- table/field 多租户隔离模式,以及按租户业务配置动态选择已编译 Adapter;当前只支持 database 隔离和进程级 Adapter 选择。
|
||||
- GPU capability、macOS/Windows 外部程序沙箱和完整宿主发行;当前也不提供 ARM64 宿主编译器安装包。
|
||||
- 编译器自举。
|
||||
|
||||
编译器会拒绝尚未支持的语法,不会把它们静默转换成默认行为。
|
||||
未支持的能力明确失败,不使用默认值或关闭隔离来继续运行。具体安装与沙箱条件见 [安装指引](skills/references/install.md)。
|
||||
|
||||
## 17. 开发建议
|
||||
|
||||
1. 先把领域状态建模为字段型和选择型 type。
|
||||
2. 用小型具名 function 组合业务逻辑;把每个分支写成穷尽、互斥的分句。
|
||||
2. 函数按完整业务职责命名,分支写成穷尽、互斥的分句。需要表达独立规则或复用时再抽取,不机械拆成碎片函数。
|
||||
3. 需要重复时选择 `each`、`filter`、`find`、`sum`、`reduce` 或 `reduce_until`;只有副作用可并行且顺序不重要时使用 `parallel_each`。
|
||||
4. 用可空值表达简单缺失;需要区分多种失败原因时定义选择型结果。
|
||||
5. 在系统边界保留 Bytes、资源和显式失败,不隐式吞掉 I/O、JSON 或 HTTP 错误。
|
||||
@@ -1501,7 +1507,7 @@ configured_or_default(result: domain.PortResult.Failed(message)) (value: domain.
|
||||
}
|
||||
```
|
||||
|
||||
完整的 domain 定义见 [contracts 示例](examples/old/dever/contracts/module/domain.dever)。`recover` 允许这个函数结束时消费失败义务,因此应该放在实际决定恢复策略的小函数上,不能为通过编译给全部函数加同一个声明。
|
||||
例中的 `domain.Port` 是包含 `value: Int >= 1 and <= 65535` 的 record;`domain.PortResult` 有 `Ready(port: Port)` 和 `error Failed(message: Text)` 两个分支。`recover` 允许这个函数结束时消费失败义务,应放在实际决定恢复策略的函数上,不能为通过编译给全部函数加同一个声明。
|
||||
|
||||
失败义务会跟随 record、choice 载荷和集合。未知集合的局部查找不能证明剩余元素已处理;可能覆盖旧值的 Map 更新也不能静默丢掉旧失败。可用完整遍历处理元素、继续返回原集合,或在真实恢复边界声明策略。空遍历不能消费没有传给 handler 的错误上下文,短路跳过的调用也不能算处理。分析是保守的,未实现任意程序的语义等价证明。
|
||||
|
||||
@@ -1514,9 +1520,9 @@ dever check my_app
|
||||
|
||||
首次生成需要新路径;已存在的文件不会覆盖。`check`、`run`、`build` 自动检查项目根已有的 `dever.api`,变化时打印增删项并失败。快照记录公开名称、类型、可见字段、构造边界、约束、效果和恢复策略。正常修改私有实现不扩大公开接口。更新基线需要单独生成新文件并审查差异,不会自动“修复”基线。基线必须由项目审查或权限保护;如果允许 AI 同时任意修改源码和基线,它就不是授权边界。未提供基线时不会限制接口变化。
|
||||
|
||||
`check`、`run`、`build` 共用应用硬检查 C016:拒绝从生产入口不可达的 App/Domain 函数,以及符合现有判定条件的私有空转发和 Model CRUD 包装,不能识别全部包装形态。检查先于 Worker 和运行库准备,`check` 保持静态、离线。`test` 按自己的测试入口检查,不套用生产可达性,测试调用不能使生产死代码通过 `check`。
|
||||
`check`、`run`、`build` 共用应用硬检查 C016:拒绝从生产入口不可达的 App/Domain 函数,以及符合现有判定条件的私有空转发和 Model CRUD 包装。对于生产入口可达的 App/Domain,还拒绝同一源码文件内契约和实现完全相同、没有副作用(包括内部调用)的私有函数,以及参数未变且前一次结果仍可复用的重复纯调用。有副作用、契约不同、参数或前一次结果已被修改的情况不按这两条重复规则拒绝。检查先于 Worker 和运行库准备,`check` 保持静态、离线。`test` 按自己的测试入口检查,不套用生产应用硬检查,测试调用不能使生产死代码通过 `check`。
|
||||
|
||||
编译器还会建议清理有证据的重复范围判断(W002)、相同私有实现(W003)、可复用的纯调用(W004)和没有增加契约的私有转发(W005)。未使用赋值沿用 W001,不叠加第二套规则。指定应用入口时,W007 提醒没有外部使用需求的公开函数;普通库检查不推测未知调用方。这些警告本身不改变执行、不作为硬错误;上述应用检查会另外拒绝其中符合 C016 判定条件的转发。检查不按行数或函数个数强迫抽象。官方源码执行同样的契约检查,CLI 默认只展示当前用户源码的建议;官方硬错误不会隐藏。
|
||||
编译器还会建议清理有证据的重复范围判断(W002)、相同私有实现(W003)、可复用的纯调用(W004)和没有增加契约的私有转发(W005)。未使用赋值沿用 W001,不叠加第二套规则。指定应用入口时,W007 提醒没有外部使用需求的公开函数;普通库检查不推测未知调用方。这些警告本身不改变执行、不作为硬错误;上述应用检查会另外拒绝符合 C016 判定条件的重复和转发,普通库和测试保留警告。CLI 已用相同主位置和关联源码证据报 C016 时,不再重复显示对应 W003/W004/W005;其他警告保留。检查不按行数或函数个数强迫抽象。官方源码执行同样的契约检查,CLI 默认只展示当前用户源码的建议;官方硬错误不会隐藏。
|
||||
|
||||
契约分析有明确的工作上限(当前每轮相应证明最多 16,384 个工作单元,名义类型嵌套最多 128 层)。超大嵌套失败结构或事实证明超过上限时报 C015,不会跳过检查后放行。共享结构使用缓存与写时复制,普通重复引用不会直接展开成指数数量的副本。
|
||||
|
||||
|
||||
Reference in new issue
Block a user