工程代码码风与规范手册

· · 科技·工程

分享一点工程代码常用的码风与规范,供有兴趣的 oier 进行参考。

请注意,在不同编程语言的社区和不同的团队中,码风会有细微的区别,但大体是一致的。本文以 C++ 语言为例,主要参考 Linux 开源社区的工程码风(也融合了一些其他的社区的码风和我自己的一些习惯)。除了 C++ 代码之外,会有少量其他编程语言的代码用于辅助说明。

花括号格式

一般采用 Java 格式的花括号,即所有开花括号在上一行末尾,不换行。

namespace solution {
    void solve(int num) {
        for (int i = 1; i <= num; i++) {
            if (i % 10 == 0) {const
                std::cout << i << std::endl;
            }
        }
    }
}

也有采用 K&R 格式的,即函数、结构、类、枚举、命名空间等采用开花括号前换行,其他的不换行。

namespace solution
{
    void solve(int num)
    {
        for (int i = 1; i <= num; i++) {
            if (i % 10 == 0) {
                std::cout << i << std::endl;
            }
        }
    }
}

最少见的应该是 Allman 格式,即所有开花括号前都换行。

namespace solution
{
    void solve(int num)
    {
        for (int i = 1; i <= num; i++)
        {
            if (i % 10 == 0)
            {
                std::cout << i << std::endl;
            }
        }
    }
}

在传统 C# 语言中,习惯性采用 Allman 格式。但现在也有很多 C# 语言开发者习惯使用 Java 格式的花括号。这里是一段示例 C# 代码,采用 Java 格式:

namespace Solution;

public static class Solution {
    public static void Solve(int num) {
        for (int i = 1; i <= num; i++) {
            if (i % 10 == 0) {
                Console.WriteLine(i);
            }
        }
    }
}

Go 语言强制采用 Java 格式,且不允许省略花括号(哪怕语句块只有一条语句),否则会导致编译错误。

package solution

import (  // 其他括号也强制使用 Java 格式,比如这里的圆括号
    "fmt"
)

func Solve(num int) {
    for i := 1; i <= num; i++ {
        if i % 10 == 0 {
            fmt.Println(i);
        }
    }
}

在 Java 格式与 K&R 格式中,附加语句(即必须依赖某条语句而存在的语句,不允许单独存在)的关键字通常在主语句的语句块闭花括号之后。例如:

if (/* ... */) {
    // ...
} else if (/* ... */) {
    // ...
} else {
    // ...
}

do {
    // ...
} while (/* ... */);

try {
    // ...
} catch (/* ... */) {
    // ...
}

如果是 Allman 格式,则附加语句的花括号在单独的一行。

if (/* ... */)
{
    // ...
}
else
{
    // ...
}

通常不建议省略语句块的花括号(哪怕只有一条语句),添加花括号在大多数情况下可以增强可读性、使控制流明显、方便后续更改代码。尤其是减少 if-else 配对歧义。Go 语言强制使用花括号。

if (condition) {
    std::cout << "Yes" << std::endl;
}

:::info[拓展内容]{open} Python 语言不存在花括号,因此也不必纠结于这些花括号格式。Python 采用缩进来标记语句块。

def solve(num: int):
    for i in range(1, num + 1):
        if i % 10 == 0:
            print(i)

:::

空格

二元或三元运算符与两边运算数之间添加空格。

int res = a + b;
char level = res >= 90 ? 'A' : 'B';

成员操作符等用于访问成员或连接标识符的操作符两侧不需要添加空格。

error.print();
std::exit(1);

对于小型计算或在特殊位置的计算,若添加空格会导致可读性反而下降,则可以省略空格。

int res = arr[i+1];
int res = arr[i + 1];

Rust 中有一个地方也满足这个条件:

for i in 0..=n+1 {
    // ...
}

// 但通常人们会添加括号
for i in 0..=(n + 1) {
    // ...
}

关键字后需要圆括号的,圆括号之前添加空格;函数调用与括号之间不添加空格。

if (condition) {
    solve(condition);
}

使用 Java 或 K&R 格式的花括号时,开花括号前添加空格。

void solve() {
    // ...
}

行尾注释起始标志与行尾之间添加两个空格;注释起始标志后添加一个空格。

std::array<int, 8> arr;  // A new method to create an array.
//                     ^^  ^

逗号之后添加空格。

solve(1, n, target);

以花括号为界限的列表中,在两侧花括号内部添加空格。方括号、圆括号则不需要。

std::vector<int> list{ 1, 2, 3, 4, 5 };

列表形式代码

以下所说的列表为广义列表,包括数组、参数列表等形式与列表相同或相似的语法。

多行列表的开括号采用 Java 格式,置于行尾。几乎没有采用 Allman 格式(置于新行)的,这和部分语言的语法特性有关。Go 语言强制采用 Java 格式。

edges[i] = Edge {
    .from = u,
    .to = v,
    .weight = w,
};

多行列表的最后一项末尾的逗号保留。

std::vector<int> list {
    1,
    2,
    3,  // here
};

如果多行列表的开括号后立即换行(即第一项在新行),则所有项提升一个缩进,闭括号保持与开花括号所在行行首一致的缩进;若开括号和没有立即换行(即第一项在开括号后),则其余项保持与第一项对齐的缩进,闭括号保持与项一致的缩进或会退到与开括号所在列一致的缩进,或者保持在最后一项之后。

solve_another(1, n, target, visited);

solve_another(
    1,
    n,
    target,
    visited,
);

solve_another(1,
              n,
              target,
              visited,
             );

与 C++ 有关的更具体的码风

标签习惯上不缩进。

class Sample {
private:
    // ...

public:
    // ...

protected:
    // ...
};

switch (something) {
case 0:  // ...
case 1:  // ...
case 2:  // ...
default:  // ...
}

对于 switch-case 语句,如果标签后的语句比较复杂或出现变量声明,且不需要贯穿标签(即“fall through”),则添加语句块(用于分割作用域)。

switch (something) {
case 0: {
    // ...
    break;  
}
case 1: {
    // ...
    break;
}
default: {
    // ...
    break;  // 可选,推荐加上
}
}

对于需要贯穿的场景,若上一个标签后有额外的语句,则添加 // fallthrough 注释;若没有额外语句,则与下一个标签紧贴。若编译器给出贯穿警告,则根据编译器文档在特定位置进行标记或抑制特定位置的警告。

新版的 C++ 中,可以使用 [[fallthrough]] 标签进行标记,以避免某些编译器的警告。

switch (something) {
case 0:
case 1:
    // ...
    break;
case 2:
    // ...
    [[fallthrough]]
case 3:
    // ...
    break;
}

除内联函数和无函数体的构造函数外,所有函数在主函数之前进行原型声明,在主函数之后进行定义。若不在主程序,则内链函数和无函数体的构造函数的定义放在头文件中,所有其他函数的原型声明放在头文件中,函数定义放在对应源文件中。

#include <cmath>
#include <format>
#include <iostream>

class Node {
    double x = 0;
    double y = 0;

public:
    Node() = default;
    Node(double x, double y) : x(x), y(y) {}
    Node(double distance, double angle, bool tag);
    void print() const;
};

Node get_node(double a, double b, bool rightangle, bool condition);

inline bool get_tag(bool a, bool b) {
    return a ^ b;
}

int main() {
    // ...

    return 0;
}

Node::Node(double distance, double angle, bool tag) {
    if (tag) {
        this->x = distance;
        this->y = angle;
    } else {
        this->x = std::cos(angle) * distance;
        this->y = std::sin(angle) * distance;
    }
}

void Node::print() const {
    std::cout << std::format("({}, {})", x, y);
}

Node get_node(double a, double b, bool rightangle, bool condition) {
    if (rightangle) {
        return Node(a, b);
    } else {
        return Node(a, b, get_tag(rightangle, condition));
    }
}

指针与引用推荐在合适的位置添加 const,尤其作为函数参数时。

void print(const std::vector<int>& vec);
void print(const int* const array, int length);

不修改对象属性的成员函数标记为 const

class Sample {
    // ...
public:
    // ...
    void print() const;
    // ...
};

包含成员函数的声明为 class;不包含成员函数且仅作为数据包装的,声明为 struct

C 式类型转换的括号与值之间不添加空格。

int a = (int)something;

C++ 语言中,推荐使用带类型安全检查的类型转换方式。

int a = static_cast<int>(something);

纯虚函数的参数中如果涉及到自身类型,则推荐使用 CRTP(奇异模板递归模式)。使用 CRTP 时,添加断言以保证代码安全。可以采用 CRTP 混合纯虚函数的形式实现接口。对于不需要再作为基类被继承的类,添加 final

template <typename Derived>
class Comparable {
public:
    int compare_to(const Derived& other) const;

protected:
    static_assert(!std::is_same_v<Derived, Comparable>, "Error");
    static_assert(sizeof(Derived) > 0, "Error");

private:
    virtual int compare_to_impl(const Derived& other) const = 0;
};

class Integer final : public Comparable<Integer>{
private:
    int value;

    int compare_to_impl(const Integer& other) const override;

public:
    Integer(int num) : value(num) {}
    void set_value(int num);
    int get_value() const;
};

template <typename Derived>
int Comparable<Derived>::compare_to(const Derived& other) const {
    return static_cast<const Derived&>(*this).compare_to_impl(other);
}

int Integer::compare_to_impl(const Integer& other) const override {
    // 避免溢出,不直接相减
    if (value < other.value) {
        return -1;
    } else if (value > other.value) {
        return 1;
    } else {
        return 0;
    }
}

void Integer::set_value(int num) {
    value = num;
}

int Integer::get_vlaue() const {
    return value;
}

更多

若还有我没有提到的,读者可以在评论区指出。