第三章:函数命名规范——动作与对象的完美结合
函数命名这事儿,说实话,我见过太多团队在这上面栽跟头了。记得我刚入行那会儿,接手过一个项目,函数名全是 func1、do_thing 这种,读代码简直像在破译密码。后来我慢慢总结出一套规律,今天就跟大家聊聊。
3.1 函数命名的核心原则:动作 + 对象
一个函数,本质上就是「对某个对象执行某个操作」。所以命名时,我习惯把「动作」放在前面,「对象」放在后面。这样读起来就像在说一句完整的话。
标准格式:动词_名词 或 动词_形容词_名词
举个例子:
// 好的命名
uint32_t timer_get_count(void);
void uart_send_byte(uint8_t data);
int adc_read_channel(uint8_t ch);
// 不好的命名
uint32_t get_timer_count(void); // 动作在后,读起来别扭
void send_uart_byte(uint8_t data); // 对象不明确
int read_adc_channel(uint8_t ch); // 勉强可以,但不够统一
我个人习惯把动词统一放在最前面。为什么?因为你在看代码时,第一眼就想知道「这个函数要干嘛」,而不是「它要对谁下手」。你想想看,是不是这个理?
3.2 常用动词表
我在项目中遇到过不少命名混乱的情况,后来整理了一份常用动词表,团队统一使用后,代码可读性提升了一大截。
| 动词 | 适用场景 | 示例 |
|---|---|---|
| init | 初始化硬件或模块 | timer_init() |
| deinit | 反初始化,释放资源 | spi_deinit() |
| open / close | 打开/关闭设备或通道 | uart_open(), gpio_close() |
| read / write | 读写数据 | i2c_read(), flash_write() |
| get / set | 获取/设置参数或状态 | adc_get_value(), pwm_set_duty() |
| enable / disable | 使能/禁能功能 | irq_enable(), wdt_disable() |
| start / stop | 启动/停止操作 | dma_start(), timer_stop() |
| is_ / has_ | 判断状态或属性(返回布尔值) | is_ready(), has_data() |
小技巧:我建议团队内部把这张表贴在墙上。新成员写代码时,先查表选动词,再拼对象名。这样出来的函数名,八九不离十。
3.3 返回值与参数命名约定
函数命名不仅要体现动作和对象,还得把返回值和参数的含义「说清楚」。嗯,这里要注意,参数命名其实是在告诉调用者「你要给我什么」,返回值命名则是在说「我会还你什么」。
3.3.1 参数命名:体现输入输出方向
我习惯用以下前缀来区分参数方向:
- 无前缀:输入参数(只读)
- p_:输出参数(指针,函数会修改其指向的内容)
- io_:输入输出参数(既读又写)
// 输入参数:直接传值
void uart_send(uint8_t data);
// 输出参数:通过指针返回结果
int adc_read(uint8_t channel, uint16_t *p_value);
// 输入输出参数:缓冲区既传入数据,又传出结果
int crypto_encrypt(uint8_t *io_buffer, uint32_t length);
我曾经在一个项目中,看到有人把所有指针参数都叫 buf,结果调试时根本分不清哪个是输入、哪个是输出。后来加了 p_ 和 io_ 前缀,问题迎刃而解。
3.3.2 返回值命名:用函数名暗示返回内容
返回值本身没有名字,但函数名可以暗示它。我总结了几种常见模式:
| 返回类型 | 函数名模式 | 示例 |
|---|---|---|
| 状态码(成功/失败) | 动词 + 对象 + _status | flash_write_status() |
| 布尔值(是/否) | is_ / has_ 开头 | is_buffer_full() |
| 数值(计数、大小等) | get_ + 名词 + _count/size | get_queue_count() |
| 句柄或指针 | get_ + 名词 + _handle/ptr | get_timer_handle() |
// 状态码返回
int spi_transfer_status(uint8_t *tx, uint8_t *rx, uint32_t len);
// 布尔值返回
bool is_adc_busy(void);
// 数值返回
uint32_t get_fifo_available(void);
// 句柄返回
void *get_mutex_handle(void);
避坑指南:我曾经见过有人写 int check_status(void),返回 0 表示成功,1 表示失败。结果后来维护的人以为返回 1 是成功,直接写反了逻辑。所以,要么用枚举明确状态,要么在函数名里说清楚返回值的含义。
3.4 实际案例:一个完整的函数命名规范
说了这么多,咱们看个完整的例子。假设我们要写一个 I2C 驱动:
// 初始化
int i2c_init(uint32_t frequency);
// 写数据
int i2c_write(uint8_t slave_addr, uint8_t *p_data, uint32_t length);
// 读数据
int i2c_read(uint8_t slave_addr, uint8_t *p_buffer, uint32_t length);
// 检查是否忙
bool i2c_is_busy(void);
// 获取错误码
uint32_t i2c_get_error(void);
// 使能中断
void i2c_irq_enable(bool enable);
你看,每个函数名都遵循了「动词_对象」的格式。参数也清晰标注了输入输出方向。读代码的人一眼就能明白:i2c_write 是往 I2C 总线上写数据,p_data 是输入缓冲区,length 是长度。
我的习惯:写函数名时,我会先在心里默念一遍:「这个函数要做什么?」然后直接把答案写成函数名。比如「我要初始化定时器」,那就是 timer_init()。简单粗暴,但有效。
3.5 总结
函数命名规范说白了就三句话:
- 动作在前,对象在后——让读者第一眼知道你要干嘛
- 参数用前缀标明方向——别让调用者猜哪个是输入、哪个是输出
- 函数名暗示返回值——看到函数名就知道它会返回什么
做到这三点,你的代码至少能打 80 分。剩下的 20 分,靠团队统一和代码审查来补。好了,这一章就聊到这儿,下一章咱们聊聊变量命名——别看它简单,坑也不少。