第 42 章 / 共 58 章 卷八 工程与原理

编码规范与工程实践

前 41 章讲完了 xlang 的全部语言表面和运行时机制;本章把视角从"怎么写一行代码"切换到"怎么组织一个项目"。从工程文件 .xlp 的目录布局,到命名约定、模块拆分、测试纪律、CI 守门、构建分发——这些不增加语言能力的"软"知识,决定了一个 xlang 项目是可持续维护的工程,还是一堆无法复用的脚本。

本章导读

编码规范不是审美偏好——它是团队协作的接口契约。xlang 编译器自身就是一个 130 个源码文件、695 个测试用例、843 条代码生成规则的中型工程,它的开发纪律(RC0 守门脚本、禁止回退扫描、诊断基线)本身就是本章最好的案例。本章的规范建议分两层:硬规则(违反会导致编译失败或运行时错误)和软约定(违反不会报错但会降低可维护性)。硬规则用编译器行为证明,软约定用编译器自身的代码风格示范。

本章学习目标

掌握 .xlp 工程文件的格式与目录布局约定;理解 xlang 命名约定(函数、类、变量、常量、文件);掌握 import/source import 的模块拆分策略与依赖方向原则;理解错误处理的最佳实践(try/catch 粒度、assert 防御、错误传播);掌握测试体系(正例运行、负例诊断断言、分类套件);学会用 xl check --json 集成 CI;理解 xl build 的构建分发工作流与资源嵌入;掌握 C 混编的工程组织(extern 声明文件、native-backed class 封装、ABI 头文件生成);了解文档注释语法;建立性能感知编码的检查清单。

42.1 工程文件与目录布局

单文件脚本用 xl run hello.xl 即可运行;一旦项目超过一个文件,就需要工程文件 .xlp(xlang project):

app.xlp
{
  "format": "xlang-project-1",
  "kind": "exe",
  "entry": "src/main.xl",
  "cSources": ["src/native.c"],
  "resources": ["app.res.json"]
}

字段规则:

字段必填说明
format是固定 "xlang-project-1",标识工程文件格式版本
kind是"exe"(可执行程序)或 "dll"(动态库)
entry是入口 .xl 文件,相对 .xlp 所在目录
cSources否参与编译的 C 源文件列表(.c/.cc/.cpp/.cxx)
resources否Windows 资源配置文件(.json/.xson),用于版本信息、图标、manifest

推荐目录布局(参照 xlang 编译器自身的工程结构):

myproject/
├── app.xlp              # 工程入口
├── src/
│   ├── main.xl          # 程序入口
│   ├── core/            # 核心逻辑模块
│   │   ├── parser.xl
│   │   └── engine.xl
│   ├── net/             # 网络模块
│   │   └── http.xl
│   └── native.c         # C 混编源文件
├── test/
│   ├── cases/           # 测试用例 .xl
│   └── run_tests.bat    # 测试入口脚本
├── docs/                # 设计文档
└── build.bat            # 构建脚本

所有相对路径都相对 .xlp 所在目录。xl run app.xlp 或 xl build app.xlp -o app.exe 即可运行或构建整个工程。

42.2 命名约定

xlang 编译器不强制命名风格(不会因命名报错),但一致的命名是团队协作的基础。以下是 xlang 标准库和编译器自身使用的约定:

实体风格示例
函数 / 方法camelCase 或 snake_case(标准库偏 snake_case)println, math.sqrt, string.split
class / union / typePascalCaseHttpClient, JsonNode, FileHandle
局部变量 / 参数camelCase 或 snake_caseitemCount, user_name
常量(文件作用域不可变)UPPER_SNAKE 或 PascalCaseMAX_RETRY, DefaultTimeout
文件名snake_case 或 kebab-casehttp_client.xl, json-parser.xl
包名 / 模块名小写单词或 snake_casemath, os, http, json_util
extern C 符号保持 C 侧原始命名xrtMalloc, file_open

核心原则:同一个项目内保持一致比选择哪种风格更重要。标准库使用 snake_case(println、to_string、array.append),你的项目可以选 camelCase,但不要混用。

避免的命名:以 __xl 或 xl6_ 开头的标识符是编译器内部保留前缀;以 xrt 开头的是 XRT 运行时保留前缀。用户代码不应使用这些前缀。

42.3 模块拆分与 import

第 35 章介绍了 import 和 source import 的语法;本节讲拆分的策略:

拆分原则:

一,单一职责。一个 .xl 文件做一件事。http_client.xl 只管 HTTP 请求,不混入 JSON 解析逻辑。当文件超过 300 行时,考虑拆分。

二,依赖方向单向。上层模块 import 下层模块,不反向。core/ 不 import net/,net/ 可以 import core/。循环依赖在 xlang 中会导致编译错误(import 展开阶段检测)。

三,公开接口最小化。只 export 需要被其他模块使用的函数和类型。内部辅助函数不加 export——它们不会进入 ABI 元数据,也不会被外部 import 看到。

模块拆分示例
// src/core/types.xl —— 基础类型定义
class Request
    string method
    string path
    dict<string> headers
end

// src/net/http.xl —— 网络层,依赖 core
import "../core/types"

export func send(Request req) as string
    // ...
end

// src/main.xl —— 入口,组装所有模块
import "core/types"
import "net/http"

func main(args: array<string>)
    // ...
end

source import vs import:source import 是编译期文本合并(类似 C 的 #include),适合紧密耦合的内部模块;import 是语义级引用(带 ABI 边界),适合松耦合的公共接口。不确定时优先用 import。

42.4 错误处理模式

第 10 章讲了 try/catch/throw 的语法;本节讲什么时候用哪种:

场景推荐方式理由
不变量被破坏(逻辑错误)assert 条件第一现场报警,不传播脏状态
可恢复的外部错误(文件不存在、网络超时)try/catch 具体异常类型调用方可以决定重试、降级或报告
不可恢复的致命错误throw 自定义 Error 或让 fatal 发生不要 catch 后吞掉——让程序快速失败
函数可能失败但调用方只关心成功/失败返回 bool + out 参数避免为简单的成功/失败创建异常对象

try/catch 粒度原则:try 块只包裹可能抛出异常的最小代码段。不要把整个函数体塞进一个 try——那样 catch 无法区分是哪一步失败了。

错误处理模式示例
// 模式 1:assert 守护不变量
func transfer(byref int from, byref int to, int amount)
    assert amount > 0
    assert from >= amount
    from = from - amount
    to = to + amount
end

// 模式 2:try/catch 处理外部错误
func loadConfig(path: string) as string
    try
        return file.read(path)
    catch e: IOError
        println(f"配置读取失败: {e.message}")
        return "{}"  // 降级为默认配置
    end
end

// 模式 3:bool + out 表示简单成功/失败
func parseInt(text: string, out int value) as bool
    // ...
end

42.5 测试体系

xlang 编译器自身拥有 695 个测试用例,分为正例和负例两大类。你的项目也应建立测试纪律:

正例测试(运行并验证输出):

test/cases/test_math.xl
import "../../src/core/math_ext"

func main()
    assert clamp(15, 0, 10) == 10
    assert clamp(-5, 0, 10) == 0
    assert clamp(7, 0, 10) == 7
    println("PASS: test_math")
end

负例测试(验证编译器报错):对于库的公开 API,应验证非法用法确实被编译器拒绝。xlang 编译器的做法是:运行 xl check bad_code.xl,断言退出码非零,并检查诊断文本包含预期关键词。

测试组织建议:

test/
├── cases/
│   ├── test_math.xl        # 按模块命名
│   ├── test_http.xl
│   └── test_json.xl
├── negatives/
│   ├── bad_type.xl         # 应编译失败的用例
│   └── bad_import.xl
└── run_tests.bat           # 一键运行全部

测试脚本模板:

test/run_tests.bat
@echo off
setlocal
set XL=..\release\xl.exe
set PASS=0
set FAIL=0

for %%f in (cases\test_*.xl) do (
    %XL% run %%f >nul 2>&1
    if !errorlevel! equ 0 (set /a PASS+=1) else (
        echo FAIL: %%f
        set /a FAIL+=1
    )
)
echo Passed: %PASS%  Failed: %FAIL%
if %FAIL% gtr 0 exit /b 1

42.6 CI 集成与守门

xl check 是为 CI 设计的快速验证命令——不生成代码,只跑词法、语法、语义检查:

$ xl check src/main.xl
# 成功:无输出,退出码 0
# 失败:输出诊断,退出码 1

$ xl check src/main.xl --json
# JSON 诊断输出,适合 CI 解析:
[{"file":"src/main.xl","line":5,"col":10,"phase":"sem","message":"..."}]

CI 管线推荐步骤:

步骤命令目的
① 静态检查xl check src/**/*.xl --json快速发现语法/语义错误
② 运行测试xl run test/cases/test_*.xl验证运行时行为
③ 构建产物xl build app.xlp -o app.exe验证完整构建链路
④ 负例验证xl check negatives/*.xl(期望失败)确保非法代码被拒绝

守门脚本思想:xlang 编译器的 RC0 守门(rc0_guard.bat)包含必需文件检查、禁止回退扫描、构建验证、测试全量运行。你的项目可以简化为:check + test + build 三步,任何一步失败就阻止合并。

42.7 构建与分发

xlang 提供两种执行模式(第 37 章):

模式命令适用场景产物
脚本运行xl run app.xl开发调试、嵌入式脚本无持久产物(内存执行)
原生构建xl build app.xlp -o app.exe发布分发独立 .exe 或 .dll

构建工作流建议:

一,开发期用 xl run 快速迭代——毫秒级编译,即时反馈。

二,发布时用 xl build 生成独立可执行文件——不依赖 xl.exe 运行。

三,Windows 发布可通过 --res app.res.json 嵌入版本信息、图标和 manifest:

app.res.json
{
  "version": "1.0.0",
  "description": "My xlang application",
  "icon": "app.ico"
}

四,构建 DLL 时 .xlp 的 kind 设为 "dll",配合 --emit-header 生成给 C 使用的头文件:

$ xl build lib.xlp -o mylib.dll --emit-header mylib.h

42.8 C 混编工程实践

第 32–34 章讲了 extern/export/native-backed class 的语法;本节讲工程组织:

一,extern 声明集中管理。把所有 C 函数的 extern 声明放在一个专门的绑定文件里(如 bindings/crt.xl),不散落在业务代码中。好处:C 库升级时只改一个文件。

bindings/xrt_binding.xl
// XRT 函数的 xlang 声明——集中管理
extern "xrtNow" func xrtNow() as int
extern "xrtRandomInt" func xrtRandomInt(min: int, max: int) as int
extern "xrtFileReadText" func xrtFileReadText(path: string) as string

二,native-backed class 封装资源。不要让用户直接操作 point 句柄——用 native-backed class 把 C 资源包装成有生命周期的对象(第 34 章)。destroy 映射确保资源释放不依赖用户记忆。

三,ABI 头文件自动生成。export 的函数和 class 可以通过 xl cgen --emit-header 自动生成 C 头文件——不要手写。生成的头文件包含正确的类型映射和调用约定。

四,C 源文件纳入 .xlp。混编的 .c 文件通过 cSources 字段参与构建,不需要单独的 Makefile 或编译脚本。

42.9 文档注释

xlang 支持文档注释语法,用于为公开 API 生成说明:

文档注释示例
/// 计算两个整数的最大公约数。
///
/// 参数:
///   a - 第一个整数(必须 > 0)
///   b - 第二个整数(必须 > 0)
///
/// 返回:a 和 b 的最大公约数
///
/// 示例:
///   assert gcd(12, 8) == 4
export func gcd(a: int, b: int) as int
    while b != 0
        int t = b
        b = a % b
        a = t
    end
    return a
end

注释原则(参照 xlang 编译器自身的代码风格):

一,注释说明为什么,代码说明做什么。不要写 // 加 1 这种重复代码的注释。

二,公开 API 必须有文档注释;内部辅助函数用简短行注释即可。

三,多写空行分隔功能段落——xlang 编译器的源码风格是"函数前用注释说明功能,函数内部按步骤分段"。

42.10 性能感知编码清单

第 41 章讲了内存模型的机制;本节把它浓缩为编码时的检查清单:

检查项做法避免
变量类型知道类型就声明类型(int, float, string)用 var 代替具体类型
容器选择同质数据用 auto 推断为静态泛型把同质数据存入 var 容器
字符串拼接多次拼接用 f-string 一次成形循环里 result = result + item
对象复制需要独立副本时显式 copy/clone误以为赋值或 byval 会复制对象
虚方法确定不被覆盖的方法标 final所有方法都标 virtual
闭包热路径用普通函数代替无捕获闭包在紧密循环里创建闭包
并发共享shared 容器并发写加 mutex假设 shared = 线程安全
资源释放用 native-backed class 管理 C 资源手动记住释放 point 句柄

性能调优工作流:先用 xl run 确认正确性 → 用 time 模块定位热点 → 用 xl cgen --emit 查看生成代码 → 确认是算法问题还是装箱问题 → 针对性优化 → 再次验证正确性。不要在没有 profiling 数据的情况下猜测瓶颈。

深入细节

深入 1:xlang 编译器的禁止回退纪律

xlang 编译器的 RC0 守门脚本维护一份"禁止回退清单":src 中不得重新出现直接 CRT malloc/free(必须走 XRT)、不得直接 printf(必须走 xl6Console)、不得直接 fopen(必须走 XRT 文件 API)。每次提交前自动扫描,违反即阻止合并。这种"只收紧不放松"的纪律保证了代码质量单调递增。你的项目可以建立类似的禁止清单:比如"不得在业务代码中直接 print 调试信息""不得 catch 后空处理"。

深入 2:诊断稳定性作为接口契约

xlang 编译器把诊断文本视为稳定接口:负例测试不仅断言退出码非零,还断言诊断文本包含预期关键词。这意味着编译器升级不能随意改变报错措辞——下游工具(IDE、CI 脚本)可能依赖诊断格式。你的库如果抛出自定义异常,异常消息也应视为公开接口的一部分,升级时保持向后兼容。

深入 3:.xlp 与多文件工程的编译模型

.xlp 的 entry 指定入口文件,但入口文件通过 import 拉入的其他 .xl 文件会自动参与编译——不需要在 .xlp 里列出所有源文件。cSources 只列 C 文件。这与 C 项目的 Makefile 需要列出所有 .c 文件不同:xlang 的依赖发现是语义级的(import 链),不是文件级的。

⚠️ 常见错误

❌ 错误 1:把所有代码塞进一个文件

超过 500 行的单文件难以导航、难以复用、难以并行开发。当文件开始"什么都有"时,就是拆分的信号。按职责拆分:类型定义、核心逻辑、I/O、入口各一个文件。

❌ 错误 2:catch 后空处理(吞异常)

try ... catch e ... end 里什么都不做,等于把错误藏起来。至少打印日志或重新 throw。如果确实可以忽略,写注释说明为什么可以忽略。

❌ 错误 3:测试只测正例

只验证"正确输入产生正确输出"是不够的。边界值(空数组、零、null)、非法输入(类型错误、越界)和并发场景都应覆盖。xlang 编译器的 695 个测试里,负例占了相当比例。

❌ 错误 4:extern 声明散落在业务代码中

每个用到 C 函数的文件都写一遍 extern 声明,C 库升级时要改 N 个文件。集中到一个绑定文件,其他文件 import 它。

本章小结

主题核心要点
工程文件.xlp 定义 kind/entry/cSources/resources,相对路径基于 .xlp 目录
命名项目内一致即可;避免 __xl/xl6_/xrt 保留前缀
模块单一职责、依赖单向、接口最小化;优先 import,紧耦合用 source import
错误处理assert 守不变量、try/catch 处理外部错误、bool+out 表示简单成败
测试正例验证行为、负例验证拒绝、分类套件组织、一键运行脚本
CIxl check --json 快速守门 + 测试运行 + 构建验证
构建开发用 run、发布用 build、DLL 用 --emit-header、资源用 --res
C 混编extern 集中管理、native-backed class 封装资源、ABI 头文件自动生成
文档/// 文档注释用于公开 API;注释说"为什么"不说"做什么"
性能声明具体类型、同质用静态容器、f-string 代替循环拼接、final 降级虚派发

复习题

  1. .xlp 文件的 kind 字段有哪些合法值?entry 字段必须指向什么类型的文件?
  2. 解释"依赖方向单向"原则。如果 core/ 和 net/ 互相 import 会发生什么?
  3. assert 和 try/catch 的使用场景有什么不同?为什么不应该用 try/catch 代替 assert?
  4. xl check --json 的输出格式是什么?它适合集成到什么场景?
  5. 负例测试验证什么?为什么只检查退出码非零是不够的?
  6. xl build 和 xl run 的产物和适用场景有什么不同?
  7. 为什么 extern 声明应该集中管理而不是散落在业务代码中?
  8. 文档注释(///)和普通注释(//)的区别是什么?哪些实体应该有文档注释?
  9. 列出性能感知编码清单中你最容易犯的三个错误,并说明正确做法。
  10. xlang 编译器的"禁止回退"纪律是什么?你的项目可以建立哪些类似的禁止规则?

编程练习

练习 1:搭建工程骨架

创建一个 .xlp 工程,包含:src/main.xl(入口)、src/core/math_ext.xl(至少 3 个数学函数)、src/core/string_ext.xl(至少 2 个字符串工具)、test/cases/test_math.xl 和 test/cases/test_string.xl。验证 xl run app.xlp 能运行,xl check 通过,测试全部 PASS。

参考思路:main.xl import 两个模块并调用;测试文件也 import 对应模块并用 assert 验证。

练习 2:CI 守门脚本

为你的练习 1 工程编写一个 ci.bat(或 ci.sh),依次执行:① xl check 所有 .xl 文件;② xl run 所有测试用例;③ xl build 生成 exe。任何一步失败则退出码非零并打印失败步骤。再故意引入一个类型错误,验证 CI 脚本能正确拦截。

参考思路:用 for 循环遍历文件,检查 errorlevel,累计失败计数。

练习 3:C 混编工程

创建一个 .xlp 工程,包含一个 native.c(实现一个 C 函数 int add_c(int a, int b))和一个 main.xl(通过 extern 声明调用 add_c)。验证 xl run 和 xl build 都能正确执行。然后添加 --emit-header 生成头文件,检查生成的 .h 内容。

参考思路:cSources 字段加入 native.c;main.xl 里 extern "add_c" func addC(a: int, b: int) as int。