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 图展示了接口定义的核心逻辑:
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 精确处理每种错误。这就是规范的力量——不用看实现代码,光看接口声明就知道怎么用。
好了,接口定义规范就讲这么多。说白了,命名、参数、返回值这三件事,是接口的「门面」。门面做好了,后面不管是自己维护还是交接给别人,都会轻松很多。我这些年吃过的亏告诉我:接口规范不是束缚,是保护。