playbook/docs/tsl/code_style.md

14 KiB
Raw Blame History

TSL 代码风格Code Style

文档类型:规范页 是否可直接用于生成代码:仅部分 是否含已验证可执行示例:否 是否含已验证反例:否 遇到不确定时跳转到:naming.mdsyntax/index.mdtoolchain.md

本章节规定 TSL 代码的结构与格式约定。

以下出现的 <TOKEN><...>、模板占位和单独标成 反例 / 不可照写 的块,只用于说明风格或反例,不等于可直接复制的源码字面量。

本页里的 反例 / 不可照写 仅表示风格上的不推荐写法,不等于语法主线里“已验证会编译失败/运行失败”的反向边界;因此页头里的 是否含已验证反例 仍保持为

相关文档:

  • 命名规范:docs/tsl/naming.md
  • 工具链与验证命令(模板):docs/tsl/toolchain.md

1. 文件与组织

1.1 单一职责

  • 一个文件只做一件事;职责明确。
  • .tsl 建议作为“入口/编排层”:聚合参数/配置、串起流程;可复用逻辑下沉到 .tsfunit/class/function)中。
  • 当一个顶层声明同时承担“协议适配 + 业务计算 + I/O/环境依赖 + 临时代码”时,优先拆分边界:核心纯逻辑 → 工具函数 → 边界适配I/O

1.2 文件名与顶层声明(硬约束)

  • TSL 语法要求(仅 .tsf):每个 .tsf 文件只能有一个顶层声明,且文件基名必须与顶层声明同名。
  • .tsl 允许直接写语句,不要求顶层声明;文件名不强制,但建议清晰可检索。
  • 推荐文件名使用 PascalCase 以提升检索与协作一致性;扩展名按类型使用 .tsl/.tsf(两者都属于 TSL 源文件,风格规则一致)。
  • 详细约束与命名细则见 docs/tsl/naming.md

1.3 依赖与分层

  • 避免循环依赖;依赖方向应从“入口/业务层”指向“通用/底层模块”。
  • 公共能力(类型/常量/纯函数)下沉到可复用模块;不要在多个文件里复制粘贴同一段工具逻辑。
  • 发现环依赖时的默认处理顺序:
    1. 提取共享部分到更底层的 *Common/*Shared unit
    2. 通过参数/回调注入反转依赖(让底层不再直接引用上层)。
    3. 必要时引入更明确的边界文件(例如 adapter 层),把依赖集中在边界处。

1.4 推荐布局(读者视角)

  • 同类代码按“对外 API → 核心实现 → 辅助工具 → 测试/示例”的顺序组织。
  • 对外 API尽量靠前读者先看到“怎么用”实现细节与 helper 放到后面(例如 unitinterfaceclasspublic)。
  • 对外声明尽量“收口”:
    • unitinterface 只放对外 const/type/function 声明与必要注释;实现细节与 helper 放在后部。
    • class 的对外 API 放在 public;内部状态与实现细节放在 private
  • 测试/示例:优先独立文件/目录,避免夹在核心实现中间(减少无关 diff 干扰 review

2. 格式Formatting

2.1 缩进与空白

  • 使用空格缩进,禁止 Tab。
  • 默认缩进 4 个空格;继续缩进保持与上层语义一致。
  • 行尾不留空格;文件以换行符结尾。
  • 逻辑块之间用空行分隔,不要用空行堆砌。

2.2 行宽与换行

  • 单行建议不超过 100 字符;超过时应换行以保持可读性。
  • 换行遵循“断在运算符后、对齐到语义层级”的原则。
  • 长字符串/URL 可适当超出,但避免影响阅读。

2.3 begin/end 与代码块

  • 代码块使用统一的块结构(示例按常见 TSL 写法;若项目语法/约定有差异,以项目现有代码为准):

代码块身份:配置片段 / 概念骨架

if cond then
begin
    DoSomething()
end
else
begin
    DoOther()
end
  • 多语句分支使用 begin/end 包裹:在 then/else 后换行写 beginend 单独成行。
  • else/elseif 等分支关键字另起一行,与上一块的 end 对齐。
  • 单语句分支可省略 begin/end(保持清晰优先;一旦分支变复杂就回退到块结构):

代码块身份:配置片段 / 概念骨架

if cond then DoSomething()
else DoOther()

2.4 运算符与分隔符

  • 二元运算符两侧加空格:a + bx == y
  • 一元运算符不加空格:!flag-value
  • 逗号后加空格:f(a, b, c)
  • 不要为了对齐而插入多余空格;让格式由缩进表达结构。

2.5 控制流

  • 多语句分支必须使用 begin/end;单语句分支可省略 begin/end,写成单行(如 if cond then stmt)。
  • 复杂条件拆分为具名布尔变量或小函数。
  • 早返回优于深层嵌套:

代码块身份:配置片段 / 概念骨架

if !ok then return err;
// main path

3. 注释Comments

注释用于解释为什么以及必要的背景,而不是重复代码。

3.0 注释形式与语言

  • 支持的注释形式:
    • 行注释:// ...(默认优先使用)
    • 块注释:{ ... }
    • 块注释:/* ... */
  • 注释语言:跟随文件,可中英混写;同一文件内尽量保持一致的表达风格。
  • 注释中不要写入明文密钥/Token/密码等敏感信息;示例使用占位符(如 <TOKEN>)。

3.1 文件级注释

  • 文件开头说明用途、主要职责、关键依赖/约束。
  • 若文件实现某个对外 API写明入口与预期行为。

3.2 函数/接口注释

  • “对外可见”的定义:
    • unitinterface 区域中的声明(对外 API
    • classpublic 区域的方法/property对外 API
    • 顶层 function:该函数本身(对外入口)
  • 对外可见的函数必须写注释,包含:
    • 做什么(行为)
    • 入参/返回值含义(必要时含单位、范围)
    • 关键副作用与异常情况
  • 注释使用完整句子,末尾带标点。
  • 推荐模板(按需裁剪;语言可中英混写):

代码块身份:配置片段 / 概念骨架

// Summary: 一句话说明做什么(以及关键约束/边界)。
// Args:
// - foo: 含义(单位/范围/约束)。
// Returns: 返回值语义(以及错误/空值含义,如适用)。
// Side effects: 关键副作用I/O/全局状态/缓存/日志等)。
// Errors: 失败条件与处理方式(返回/抛出/降级)。

3.3 行内注释

  • 用于解释复杂逻辑、非直观边界条件、性能/安全考量。
  • 避免“显而易见注释”:
  • 尾随注释(写在代码行末)只用于非常短的补充;超过一行时改为写在语句上方,或重构代码提醒意图。

不推荐这样写:

代码块身份:反例 / 不可照写 代码块说明:风格反例,不是已验证语法反例。

count := count + 1;  // obvious

推荐直接删掉这类显而易见注释:

代码块身份:配置片段 / 概念骨架

count := count + 1;

3.4 TODO/FIXME

  • 统一格式:TODO(name): ... / FIXME(name): ...
  • 写清原因和期望修复方向,而非“留个坑”。
  • name 使用 Git 用户名;不强制附 issue/ticket如有可追加在描述中

4. 代码实践Best Practices

本节偏“实践建议”should用于提升可读性/可测试性;若目标项目有更严格的约束与检查命令,以项目落地的工具链为准(参考 docs/tsl/toolchain.md)。如果项目对自动化或 AI 代理有额外要求应把约束直接写进仓库内可见的检查脚本、CI 配置或项目文档,而不是依赖隐藏规范。

4.1 变量与常量

  • 默认使用不可变/只读:能用 const 就用 const;可变状态尽量压到最小作用域,并让“更新点”集中且明显。
  • 对外 API 优先只读:对外暴露用只读 property只有 read,不写 write),内部用私有成员保存。

代码块身份:配置片段 / 概念骨架

type User = class
public
    property UserId read user_id_;  // readonly
private
    user_id_;
end;
  • 变量声明与第一次使用尽量靠近。
  • 避免隐式类型转换TSL 为动态类型,但运行时仍有类型与单位;外部输入(参数/配置/文件/接口)应在边界处显式解析与校验,再进入核心逻辑。
  • 避免隐式全局:函数尽量只依赖显式入参;若必须使用顶层全局/静态可变变量,必须在声明处写注释说明:它是什么、用于什么、以及(如不明显)为什么需要是全局/静态。

4.2 函数设计

  • 签名尽量自解释:对外 API 的参数/返回值建议显式写类型注解;并用注释写清契约(可复用 3.2 的模板)。
  • 类型注解不支持 xxx.xxx 形式;使用单一类型名。
  • 若需标注类型来源,允许在类型名前用块注释写 {Unit.} 前缀,例如 style_: {DocxML.}Style;。该前缀仅为注释,不参与语义或类型检查,工具可能忽略;类型名仍是 Style(建议紧贴类型名书写)。
  • 示例({Unit.} 前缀仅用于阅读,不改变类型名):

代码块身份:配置片段 / 概念骨架

type DocxContext = class
public
    property Style read style_ write style_;
private
    style_: {DocxML.}Style;
end;

function RenderParagraph(para_: {DocxML.}Paragraph): void;
  • 无返回值函数显式标注返回类型为 voidcreate/destroy 作为构造/析构函数不写返回类型。

代码块身份:配置片段 / 概念骨架

function Func(a: string; b: ClassName): void;
  • 参数默认可读写(引用语义);输入参数如果不应被修改,优先使用 const 修饰符让意图与约束更明确。
  • 单一职责:函数过长说明拆分点已出现(建议 ≤ 4060 行把“纯计算”与“I/O/环境依赖(文件/网络/数据库/全局状态)”分离,降低耦合、便于测试。
  • 参数组织与顺序:
    • 输入参数在前;可选配置/选项(如 *Options/*Config)居中;输出/回调在后。
    • 避免堆叠多个布尔开关参数;优先收敛到 *Options/*Config(按需在 classunit 中定义)。
  • 示例:避免多个开关参数直接堆在签名里(调用点难以理解各参数含义),改为 *Options/*Config

不推荐这样写:

代码块身份:反例 / 不可照写 代码块说明:风格反例,不是已验证语法反例。

function ExportReport(
    path: string;
    rows: array;
    include_header;
    compress;
    dry_run
): void;

推荐把可选开关收敛到 Options / Config

代码块身份:配置片段 / 概念骨架

type ExportOptions = class
public
    property IncludeHeader read include_header_ write include_header_;
    property Compress read compress_ write compress_;
    property DryRun read dry_run_ write dry_run_;
private
    include_header_;
    compress_;
    dry_run_;
end;

function ExportReport(path: string; rows: array; options: ExportOptions): void;
  • 尽量避免超过 5 个参数;必要时封装为对象(class/unit)。

4.3 错误处理

  • 错误必须显式处理:返回失败/错误、抛出异常、或记录并降级best-effort。禁止“看起来成功了但其实失败了”的隐式路径。
  • 不设默认策略:按场景选择返回/抛出/降级,并在对外注释里写清契约(参考 3.2 模板的 Errors)。
    • 返回失败/错误:调用方有能力恢复/重试/改参数时(参数不合法、外部输入解析失败、依赖不可用等)。
    • 抛出异常:不应发生的内部错误/不变量被破坏,继续执行风险更大时。
    • 记录并降级:功能可选、失败不影响主流程时(例如缓存读取失败 → 当作 cache miss必须在代码旁注释说明“为什么允许”。
  • 不要吞掉异常/错误:try/except 之后如果继续执行,必须有明确替代行为(返回/重试/降级)以及理由;否则应将错误继续向上抛出或返回。
  • 错误信息与日志(允许在库里打日志,但要克制):
    • 错误/日志至少包含:做什么失败 + 关键上下文(脱敏)便于定位避免只有“failed”。
    • 禁止把 Token、密码、个人数据等敏感信息写入日志、注释或错误信息如需记录标识优先记录脱敏后的业务 ID 或可复现的上下文键。
    • 避免重复记录:同一个错误链路尽量只在边界层记录一次(库里记录后,上层通常不再重复打一遍同等级日志)。
  • 示例:try/except/end + 降级best-effort
    • LoadCacheValue / RecordCacheWarning 代表项目自有函数,重点在结构而不是函数名本身。

代码块身份:配置片段 / 概念骨架

// 读取可选缓存:失败允许降级为 cache miss必须可观测并说明原因。
function ReadOptionalCache(path: string);
begin
    try
        return LoadCacheValue(path)
    except
        // best-effort: cache 仅用于提速,失败不应影响主流程
        RecordCacheWarning(path)
        return nil
    end
end;

4.4 性能与可测试性

  • 避免过早优化:先写清晰正确的代码,再用项目实际可用的 profile、trace、日志或基准数据定位瓶颈并做最小化改动。
  • 复杂逻辑要可测试:把“纯计算/解析/规则”与“I/O/环境依赖(文件/网络/DB/全局状态”分离I/O 层做薄封装,核心逻辑保持可单测。
  • 避免在热路径里做隐式昂贵操作:循环内重复 I/O、重复解析/格式化、无界缓存、隐式复制等;缓存如必须引入,明确生命周期与上限(大小/TTL/清理点)。
  • 示例:薄 I/O + 厚纯逻辑(便于测试与复用):
    • LoadConfigText 代表项目自己的 I/O 边界函数;ParseConfig 只负责解析与校验。

代码块身份:配置片段 / 概念骨架

// pure: 只做解析/校验,不做 I/O便于单元测试
function ParseConfig(text: string): array;

// I/O: 只负责读文件与兜底处理,把逻辑交给 ParseConfig
function LoadConfig(path: string): array;
begin
    text := LoadConfigText(path);
    return ParseConfig(text)
end;