4. 接口定义规范:统一接口命名规则、参数传递与错误码

好,咱们进入第四章节。说实话,接口定义这块,是重构里最容易「翻车」的地方。我见过太多项目,前期图省事,每个人按自己习惯写接口,结果到了联调阶段,光对齐函数名就花了一周。嗯,咱们今天就把这事彻底理清楚。

4.1 统一接口命名规则:让函数名自己会说话

我个人的习惯是,接口命名必须做到「见名知意」。你想想看,一个团队里五个人,如果每个人对「初始化」的理解都不一样,有人用 Init,有人用 Setup,还有人用 Begin,那代码就没法维护了。

咱们定一个硬规矩:所有 GUI 接口统一采用 GUI_模块_动作 的三段式结构。

动作类型 命名模板 示例 说明
初始化 GUI_XXX_Init GUI_Button_Init 分配资源、设置默认状态
反初始化 GUI_XXX_Deinit GUI_Button_Deinit 释放资源、恢复默认
控制/配置 GUI_XXX_Control GUI_Button_Control 修改属性、触发行为
获取状态 GUI_XXX_Get GUI_Button_GetState 只读查询,不修改内部数据
设置属性 GUI_XXX_Set GUI_Button_SetText 写操作,可能触发重绘

核心原则:前缀 GUI_ 保证全局唯一性,中间模块名避免缩写歧义,后缀动作动词统一。不要出现 GUI_Btn_Setup 这种混搭写法。

我在项目中遇到过一件事:有个同事把「销毁对话框」写成了 GUI_Dlg_Kill,结果另一个模块里有个 GUI_Task_Kill 是杀任务的。你看,命名不统一,光看名字根本分不清是销毁界面还是干掉进程。后来我们强制用 Deinit 统一所有「反初始化」场景,再没出过这种误会。

4.2 参数传递规范:结构体指针优先

参数怎么传?我直接说结论:能用结构体指针,就别用散装参数。

为什么?我给你算笔账。假设一个按钮控件需要 8 个配置参数:坐标 x、y、宽、高、颜色、字体、回调函数、使能状态。如果你写成 GUI_Button_Init(x, y, w, h, color, font, callback, enable),调用方得记住每个参数的位置,稍微调换顺序就出 bug。而且接口签名一旦定下来,后面想加一个「圆角半径」参数,所有调用处都得改。

用结构体指针就灵活多了:

/* 推荐方式:结构体指针传递 */
typedef struct {
    int16_t      x;
    int16_t      y;
    uint16_t     width;
    uint16_t     height;
    uint32_t     color;
    GUI_Font*    font;
    GUI_Callback callback;
    bool         enable;
} GUI_Button_Config;

GUI_Error GUI_Button_Init(GUI_Button_Handle handle, 
                          const GUI_Button_Config* config);

你看,调用方只需要填充结构体成员,顺序错了也没关系。以后想加 radius 字段,直接在结构体末尾追加,接口签名不用变,不影响已有代码。

我的经验:结构体指针加 const 修饰,明确告诉调用者「这个函数不会改你的配置数据」。另外,结构体里尽量用 int16_t、uint32_t 这种明确宽度的类型,别用 int,不同编译器下 int 长度不一样,容易踩坑。

那什么时候用散装参数?我个人觉得只有一种情况:参数不超过 3 个,而且语义极其简单。比如 GUI_Button_GetState(handle),就一个句柄参数,没必要套结构体。

4.3 返回值与错误码定义:别让调用方猜

嗯,这里我要重点说说。很多嵌入式代码喜欢用 int 返回值,0 表示成功,-1 表示失败。但失败的原因是什么?调用方完全不知道。是参数错了?内存不够?还是硬件没准备好?

我建议的做法是:定义统一的枚举类型作为返回值。

/* 统一错误码定义 */
typedef enum {
    GUI_OK              = 0,    /* 成功 */
    GUI_ERR_PARAM       = -1,   /* 参数错误 */
    GUI_ERR_MEMORY      = -2,   /* 内存不足 */
    GUI_ERR_TIMEOUT     = -3,   /* 操作超时 */
    GUI_ERR_BUSY        = -4,   /* 资源忙 */
    GUI_ERR_NOT_SUPPORT = -5,   /* 不支持的操作 */
    GUI_ERR_HARDWARE    = -6,   /* 硬件错误 */
    GUI_ERR_UNKNOWN     = -7    /* 未知错误 */
} GUI_Error;

每个函数都返回 GUI_Error,调用方拿到错误码后,可以精确知道问题出在哪。我曾经在一个项目里调试触摸屏驱动,发现 GUI_Touch_Read 总是返回 -1,但就是不知道具体原因。后来改成枚举返回值,才发现是 GUI_ERR_TIMEOUT——I2C 总线被别的任务占着没释放。你看,一个错误码省了我半天调试时间。

避坑指南:千万不要把错误码和有效返回值混在一起。比如有的函数返回 int,正数表示有效数据长度,负数表示错误码。这种设计在 C 语言里很常见,但可读性极差。我建议把「数据」和「状态」分开:数据通过输出参数传,状态通过返回值传。

4.4 整体接口规范框架

说了这么多,咱们用一张图把整个规范串起来。下面这张 SVG 图展示了接口定义的核心逻辑:

接口定义规范核心框架 命名规则 参数传递 返回值与错误码 GUI_模块_动作 三段式 Init / Deinit / Control / Get / Set 结构体指针优先 const 修饰 + 明确宽度类型 统一枚举 GUI_Error 数据与状态分离 可维护 · 可移植 · 可扩展 命名、参数、返回值三者统一 才能让接口像「协议」一样稳定

4.5 实际代码示例:一个完整的接口实现

光说不练假把式。咱们拿一个简单的「进度条」控件,把上面三条规范串起来看看:

/* progress_bar.h */
#ifndef GUI_PROGRESS_BAR_H
#define GUI_PROGRESS_BAR_H

#include "gui_types.h"  /* 包含 GUI_Error 定义 */

/* 进度条配置结构体 */
typedef struct {
    int16_t      x;           /* 左上角 x 坐标 */
    int16_t      y;           /* 左上角 y 坐标 */
    uint16_t     width;       /* 宽度 */
    uint16_t     height;      /* 高度 */
    uint32_t     bg_color;    /* 背景色 */
    uint32_t     fg_color;    /* 前景色 */
    uint8_t      min_value;   /* 最小值 */
    uint8_t      max_value;   /* 最大值 */
} GUI_ProgressBar_Config;

/* 初始化进度条 */
GUI_Error GUI_ProgressBar_Init(GUI_Handle handle, 
                               const GUI_ProgressBar_Config* config);

/* 反初始化进度条 */
GUI_Error GUI_ProgressBar_Deinit(GUI_Handle handle);

/* 设置当前进度值 */
GUI_Error GUI_ProgressBar_SetValue(GUI_Handle handle, uint8_t value);

/* 获取当前进度值 */
GUI_Error GUI_ProgressBar_GetValue(GUI_Handle handle, uint8_t* value);

#endif /* GUI_PROGRESS_BAR_H */

注意看:这个接口里,Init 用结构体指针传配置,GetValue 用输出参数传数据,所有函数都返回 GUI_Error。调用方拿到返回值后,可以用 switch 精确处理每种错误。这就是规范的力量——不用看实现代码,光看接口声明就知道怎么用。

好了,接口定义规范就讲这么多。说白了,命名、参数、返回值这三件事,是接口的「门面」。门面做好了,后面不管是自己维护还是交接给别人,都会轻松很多。我这些年吃过的亏告诉我:接口规范不是束缚,是保护。