第三章:函数命名规范——动作与对象的完美结合

函数命名这事儿,说实话,我见过太多团队在这上面栽跟头了。记得我刚入行那会儿,接手过一个项目,函数名全是 func1do_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 总结

函数命名规范说白了就三句话:

  1. 动作在前,对象在后——让读者第一眼知道你要干嘛
  2. 参数用前缀标明方向——别让调用者猜哪个是输入、哪个是输出
  3. 函数名暗示返回值——看到函数名就知道它会返回什么

做到这三点,你的代码至少能打 80 分。剩下的 20 分,靠团队统一和代码审查来补。好了,这一章就聊到这儿,下一章咱们聊聊变量命名——别看它简单,坑也不少。