目录
1 简短
函数应该保持简短,以提高代码的可读性和可维护性。简短的函数通常在20行以内,并且每个函数只做一件事,并清晰地表达其目的。函数应该保持单一职责,只处理一个抽象层级上的任务。混合不同层级的操作会导致代码难以理解。为了提高代码的清晰度,我们应该按照自顶向下的顺序组织函数,每个函数都应该调用下一个抽象层级的函数,形成清晰的阅读路径。
#include <stdio.h>
#include <string.h>
// 高层抽象:渲染页面
void renderPage(char* buffer, int bufferSize) {
setupPage(buffer, bufferSize);
addContent(buffer, bufferSize);
teardownPage(buffer, bufferSize);
}
// 中层抽象:设置页面
void setupPage(char* buffer, int bufferSize) {
// ...
}
// 低层抽象:添加页面内容
void addContent(char* buffer, int bufferSize) {
// ...
}
// 低层抽象:拆除页面
void teardownPage(char* buffer, int bufferSize) {
// ...
}
int main() {
char page[1024];
renderPage(page, sizeof(page));
printf("%s\n", page);
return 0;
}
2 switch语句
Switch语句通常很难保持短小和单一职责,因为它们往往需要处理多种情况。为了解决这个问题,可以将Switch语句隐藏在较低的抽象层级中。这样,Switch语句只出现一次,且不会被系统其他部分直接访问。
例如,在一个工资支付系统中,我们可能会根据不同的员工类型(如小时工、月薪员工、佣金员工)来计算工资。为了保持代码的清晰性和单一职责,我们可以将Switch语句隐藏在一个工厂方法中,该方法负责创建不同类型的员工对象。
#include <stdio.h>
#include <stdlib.h>
typedef enum {
HOURLY,
SALARIED,
COMMISSIONED
} EmployeeType;
typedef struct {
EmployeeType type;
double hoursWorked;
double salary;
double commissionRate;
double salesAmount;
} Employee;
typedef struct {
double hourlyWage;
double salaryPerMonth;
double commission;
} PayDetails;
// 计算工资的函数声明
PayDetails calculateHourlyPay(double hourlyWage, double hoursWorked);
PayDetails calculateSalariedPay(double salaryPerMonth);
PayDetails calculateCommissionedPay(double commissionRate, double salesAmount);
// 工厂方法,根据员工类型计算工资
PayDetails calculatePay(Employee employee) {
switch (employee.type) {
case HOURLY:
return calculateHourlyPay(employee.hourlyWage, employee.hoursWorked);
case SALARIED:
return calculateSalariedPay(employee.salaryPerMonth);
case COMMISSIONED:
return calculateCommissionedPay(employee.commissionRate, employee.salesAmount);
default:
return (PayDetails){0, 0, 0}; // 默认返回零工资
}
}
// 实现计算小时工资的函数
PayDetails calculateHourlyPay(double hourlyWage, double hoursWorked) {
return (PayDetails){hourlyWage * hoursWorked, 0, 0};
}
// 实现计算月薪的函数
PayDetails calculateSalariedPay(double salaryPerMonth) {
return (PayDetails){0, salaryPerMonth, 0};
}
// 实现计算佣金工资的函数
PayDetails calculateCommissionedPay(double commissionRate, double salesAmount) {
return (PayDetails){0, 0, commissionRate * salesAmount};
}
int main() {
Employee employee = {COMMISSIONED, 0, 0, 0.1, 10000};
PayDetails pay = calculatePay(employee);
printf("Hourly Pay: %.2f\n", pay.hourlyWage);
printf("Salary: %.2f\n", pay.salaryPerMonth);
printf("Commission: %.2f\n", pay.commission);
return 0;
}
3 函数参数
一元函数的普遍形式:一元函数应该清晰地表达其意图,要么询问关于参数的问题,要么对参数进行操作并返回结果。应该避免使用输出参数进行转换,而应该使用返回值。
#include <stdio.h>
#include <stdbool.h>
// 检查文件是否存在
bool fileExists(const char* filename) {
// 假设的文件存在检查逻辑
return true;
}
// 打开文件
FILE* fileOpen(const char* filename) {
// 假设的文件打开逻辑
return fopen(filename, "r");
}
int main() {
bool exists = fileExists("MyFile.txt");
printf("File exists: %s\n", exists ? "Yes" : "No");
FILE* file = fileOpen("MyFile.txt");
if (file) {
printf("File opened successfully.\n");
fclose(file);
}
return 0;
}
标识参数:避免使用布尔值作为标识参数,因为这会增加函数的复杂性,如果必须使用,应该考虑将函数拆分为两个或更多函数。
#include <stdio.h>
// 重构前:使用布尔标识参数
void render(bool isSuite) {
if (isSuite) {
// 渲染套件
} else {
// 渲染单个测试
}
}
// 重构后:拆分为两个函数
void renderForSuite() {
// 渲染套件
}
void renderForSingleTest() {
// 渲染单个测试
}
int main() {
renderForSuite();
renderForSingleTest();
return 0;
}
多元函数:多元函数需要更多的上下文来理解参数之间的关系,应该尽量避免或者通过将参数组合成对象来简化。
#include <stdio.h>
#include <stdlib.h>
// 比较两个值
void assertEquals(const char* message, int expected, int actual) {
if (expected != actual) {
printf("%s: expected %d but got %d\n", message, expected, actual);
}
}
int main() {
assertEquals("Test failed", 10, 20);
return 0;
}
参数对象:如果函数看起来需要多个参数,这通常是一个信号,表明应该将这些参数封装成一个对象。这样可以减少参数的数量,提高代码的可读性。
#include <stdio.h>
// 定义一个结构体来封装多个参数
typedef struct {
int year;
int month;
int day;
} Date;
// 函数声明,现在只接受一个参数
void printDate(const Date* date);
int main() {
// 创建一个日期对象
Date today = {2024, 5, 19};
// 调用函数,只传递一个参数
printDate(&today);
return 0;
}
// 函数定义,接受一个日期结构体作为参数
void printDate(const Date* date) {
printf("Today's date: %d-%d-%d\n", date->year, date->month, date->day);
}
动词与关键字:函数名和参数应该形成清晰的动词/名词对,这有助于解释函数的意图和参数的顺序,关键字形式的函数名可以减少记忆参数顺序的负担。
#include <stdio.h>
// 使用动词和关键字
void assertExpectedEqualsActual(int expected, int actual) {
if (expected != actual) {
printf("Assertion failed: expected %d, actual %d\n", expected, actual);
}
}
int main() {
assertExpectedEqualsActual(5, 3);
return 0;
}
4 无副作用
函数应该避免副作用,即在执行过程中对外部状态进行修改,如改变全局变量或输入参数的值。副作用会破坏函数的纯粹性,导致代码难以理解和维护。
在下面示例中,checkPassword 函数只负责验证用户名和密码是否匹配,它不改变任何外部状态。如果需要初始化会话,应该在 main 函数中单独处理,而不是在密码检查函数中。这样的设计避免了副作用,使得 checkPassword 函数更加清晰和可靠。
#include <stdio.h>
#include <string.h>
// 定义一个用户结构体
typedef struct {
char username[50];
char hashedPassword[50]; // 存储加密后的密码
} User;
// 定义一个密码验证器结构体
typedef struct {
// 加密器的实现细节
} Cryptographer;
// 密码验证器实例
Cryptographer cryptographer;
// 检查密码是否正确,不产生副作用
int checkPassword(const char* username, const char* password) {
User user;
// 假设从数据库或其他来源获取用户信息
strcpy(user.username, username);
strcpy(user.hashedPassword, "hashed_password"); // 假设的加密密码
// 使用加密器验证密码
if (strcmp(cryptographer.decrypt(user.hashedPassword), password) == 0) {
return 1; // 密码正确
}
return 0; // 密码错误
}
// 解密密码的函数(假设实现)
char* decrypt(const char* hashedPassword) {
// 返回明文密码
return (char*)"password";
}
int main() {
char username[50] = "user1";
char password[50] = "password";
// 调用checkPassword,不会产生副作用
if (checkPassword(username, password)) {
printf("Login successful.\n");
// 可以在这里初始化会话,而不是在checkPassword中
} else {
printf("Login failed.\n");
}
return 0;
}
5 结构化编程
结构化编程强调每个函数只应有一个入口和一个出口,避免使用break、continue和goto语句。虽然这种规范对于大型函数有益,有助于提高代码的清晰性和可维护性,但在小型函数中可能过于严格。因此,在保持函数简短的情况下,偶尔使用return、break或continue语句是可以接受的,它们可以提高代码的表达力。然而,goto语句由于其可能导致代码结构混乱,即使在大型函数中也应尽量避免使用。