作为软件工程师,出产物就应该具备工程的健壮性和美观性。因此代码规范是作为软件工程师的职业素养。但总所周知,程序员的工作基本就是在维护一座屎山,这确实是现实中种种客观条件约束下导致的。
记录本篇博客,也是希望本人在打码过程,能够保持负责任的初心,以及追求完美的极客精神吧。
诸君共勉。愿人间没有ugly的代码。
一. 变量声明与命名风格
(1) 代码中的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束。但也常以双下划綫标识类内部变量。
反例:_name / __name / $name / name_ / name$ / name__
(2) 代码中的命名严禁使用拼音与英文混合的方式,更不允许直接使用中文的方式。 说明:正确的英文拼写和语法可以让阅读者易于理解,避免歧义。注意,即使纯拼音命名方式 也要避免采用。
正例:alibaba / taobao / youku / hangzhou 等国际通用的名称,可视同英文。
反例:DaZhePromotion [打折] / getPingfenByName() [评分] / int 某变量 = 3
(3) 变量名和函数名以小写字母开头,开头之后的部分每个单词以大写字母开头。
正例:short counter; / char itemDelimiter = ' ';
(4) 类名以大写字母开头。
正例:XmlService / User
(5) 常量命名全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长。 正例:MAX_STOCK_COUNT
(6) 抽象类命名使用 Abstract 或 Base 开头;异常类命名使用 Exception 结尾;测试类 命名以它要测试的类的名称开始,以 Test 结尾。
(7) 避免短的或无意义的命名。使用尽量完整的单词 组合来表达其意。
正例::AtomicReferenceFieldUpdater。
反例:变量 int a 的随意命名方式。
(8) 枚举成员名称需要全大写,单词间用下划线隔开。
(9) 单个字符的变量名只适用于用来计数的临时变量,因为此时该变量的用途十分明显
(10) 声明每一个变量都要用独立的一行
正例:int height; / int width;
(11) 当一个变量被用到时再声明它。
(12) 对于指针或引用,在类型名和*或&之间用一个空格,但是在*或&和变量名之间没有空格
正例:char *x; / const QString &myString; / const char * const y = "hello";
(13) Qt的ui相关的,在ui里的变量也尽量要进行命名,特别是在代码中使用到的,反例:ui->frame,命名时注意添加该界面的标识性字段,尽量使得ui里所有变量名字在整个程序域中不重名,便于在QSS中控制。
正例:如在SE_ReportDialog.ui中的命名,m_reportFrame;在SE_CalculateWindow.ui中的命名:m_calculateFrame等。
(14) 如上个例子,在特定地方声明的变量应添加适当的前缀,如类的成员变量添加m_前缀;全局变量添加g_;局部变量添加l_。
(15) 接上一点,在前缀下划线后第一个字母,常为变量类型的缩写,如int为i,double为d。
正例:int m_iMonth; double l_dMoney;
二.代码格式
(1) 大括号的使用约定。如果是大括号内为空,则简洁地写成{}即可,不需要换行;如果 是非空代码块统一如下例:
if ( ... )
{
...
}
else
{
...
}
(2) 左小括号和字符之间不出现空格;同样,右小括号和字符之间也不出现空格
反例:if (空格 a == b 空格)
(3) if/for/while/switch/do 等保留字与括号之间都必须加空格。
正例:if (foo)
(4) 任何二目、三目运算符的左右两边都需要加一个空格。
正例:lineF == lineN
(5) 注释的双斜线与注释内容之间有且仅有一个空格。
正例: // 这是示例注释,请注意在双斜线之后有一个空格
(6) 方法参数在定义和传入时,多个参数逗号后边必须加空格。即逗号左边没有空格,逗号右边有一个空格。
正例: QT_REQUIRE_VERSION(argc, argv, "4.0.2")
(7) 左引号的左边和右引号的右边都有一个空格,左引号的右边和右引号的左边都没有空格。如果右引号右边是右括号的话,它们之间没有空格。
正例: qDebug() << Q_FUNC_INFO << "was called with value1:" << value1 << "value2:" << value2;
QT_REQUIRE_VERSION(argc, argv, "4.0.2")
(8) 不同逻辑、不同语义、不同业务的代码之间插入一个空行分隔开来以提升可读性。
(9) 当条件语句的执行部分多于一句的时候才使用花括号。然而在if-else语句块中,如果if或else中的一个包含了多行,另一个为了对称性原则,也要用花括号。
正例:if (address.isEmpty())
return false;
(10) 每行代码不多于100个字符,超出需要换行。换行时:第二行相对第一行缩进 4 个空格,从第三行开始,不再继续缩进。运算符与下文一起换行。方法调用的点符号与下文一起换行。方法调用中的多个参数需要换行时,在逗号后进行。
正例:if (longExpression
+ otherLongExpression
+ otherOtherLongExpression) { }
三.类相关
(1)避免通过一个类的对象引用访问此类的静态变量或静态方法,无谓增加编译器解析成 本,直接用类名来访问即可。
(2)构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,请放在 init 方法中。
(3)类内方法定义的顺序依次是:公有方法->公有槽->信号->保护方法->私有方法->私有槽。
(4)当一个类有多个构造方法,或者多个同名方法,这些方法应该按顺序放置在一起, 便于阅读。
(5)成员变量一般为名词。函数成员一般为动词/动词+名词,当返回值为Bool型变量时,函数名一般以前缀’is’开头。
正例:void setDirty(bool b); / bool isDirty() const;
(6)每一个对象,只要它不是基础类型(int, float, bool, enum, or pointers),都应该以常量引用的形式传递。这条使得代码运行得更快。
正例:QString myMethod(const QString &foo, const QPixmap &bar, int number);
(7)为了使构造函数被错误使用的可能性降到最小,每一个构造函数(除了拷贝构函数)都应该检查自己是否需要加上explicit 符号。
(8)尽量减少在头文件中包含其他头文件的数量,可以用前置声明法。
正例:class KBar;
class KFoo : public KFooBase
{ … };
(9)假如你有一个Foo类,有Foo.h文件和Foo.cpp文件,在你的Foo.cpp文件中,要先包含Foo.h文件再包含其他头文件。
正例:// .h文件
class Foo
{ …};
// .cpp文件
#include "foo.h"
#include "bar.h"
四. 控制相关
(1) case标签和switch在同一列。每一个case语句的末尾都要有一个break语句或return语句。在一个 switch 块内,都必须包含一个 default 语句并且 放在最后,即使空代码。
(2) 除常用方法(如 getXxx/isXxx)等外,不要在条件判断中执行其它复杂的语句,将 复杂逻辑判断的结果赋值给一个有意义的布尔变量名,以提高可读性
五. 注释相关
(1) 方法内部单行注释,在被注释语句上方另起一行,使用//注释。方法内部多行注释 使用/* */注释,注意与代码对齐。
(2) 对于注释的要求:第一、能够准确反应设计思想和代码逻辑;第二、能够描述业务含 义,使别的程序员能够迅速了解到代码背后的信息。完全没有注释的大段代码对于阅读者形同 天书,注释是给自己看的,即使隔很长时间,也能清晰理解当时的思路;注释也是给继任者看 的,使其能够快速接替自己的工作。
(3) 好的命名、代码结构是自解释的,注释力求精简准确、表达到位。避免出现注释的 一个极端:过多过滥的注释,代码的逻辑一旦修改,修改注释是相当大的负担。
(4) 建议每个文件、每个类、每个函数,尤其是接口API部分,使用doxygen语法标注。
1. 代码规范
Unity中的C#代码,变量命名和函数命名均采用小驼峰命名法,即名称首字母小写,其余单词的首字母大写。(由于Unity内置接口以及C#的系统接口都是使用大驼峰命名法(即首字母也是大写),我们使用不同命名规则就能轻易辨认出某个函数是内置的还是我们定义的)类名和常量命名使用大驼峰命名法。
Python代码,变量命名使用下划线命名法,常量命名使用大写的下划线命名法,静态变量使用大驼峰命名法,函数使用小驼峰命名法,类使用大驼峰命名法。(由于Python的变量声明不需要任何关键字,只能使用多种命名法加以区分)
其他地方的命名规则均使用大驼峰(包括Unity编辑器中的对象命名、文件命名等)
. 注释要求
每个类、函数前都要写注释,在Python中直接在类/函数前一行使用“#”来注释。在C#中使用以下格式的注释:
///
/// 函数解释
///
类中的成员变量也需要注释,可以使用上面的格式,也可以直接在变量后用“//”注释,如果变量作用清晰,变量名能表达清楚则可以不写注释。如果需要声明多个成员变量,可以写在一起,然后在第一个成员变量前面使用上述注释方法进行分区,如以下代码所示:
注:VS中,在声明的类、函数或者变量前输入“///”就会自动补全以上格式的注释。
另外,在定义了很多函数的情况下,要将功能相似的函数放在一起,并使用以下语句进行代码分区:
#region 区域名
.... 代码段 ....
#endregion
这样可以在编辑器中收起一部分代码段,方便查看其他部分的代码,也能作为一个注释告诉其他人这段代码是做什么的。效果如下:
最后,在代码逻辑中,也需要尽量写注释,特别是用到了其他技术(比如Unity提供的一些接口)时要注明一下。
命名除了必须符合编码规范外,同时应尽量使其让其具备良好的可读性。关于命名(实在想不到应该叫啥,或者单词都不会翻译 ),可参考CodeLf。
在代码中,我们经常会碰到硬编码,比如存取一个本地文件的名字(如:Setting.file),比如预设的一些组件的名字(如:控件1、控件2)等等。硬编码其实无法避免,举个极端的说法,在UI上每个按钮的名字都是硬编码,这个叫做“设置”,那个叫做“关闭”。
但硬编码还是存在很大的问题,以上述读取一个文件的名字为例,如果代码中有多个地方访问该文件,直接使用硬编码时,每个地方都将使用一次"Setting.file",这是让人担忧的事情!因为当这个"Setting.file"发生变更时,我们将不得不修改每一处硬编码的地方。(当然了,我们也是可以通过全局crtl+F轻松替换)。
那为什么不采取一个更加漂亮的处理方式呢?个人建议,所有硬编码的地方,只要有第二处引用到同一个硬编码,则应该将该硬编码提取成一个常量变量储存,每个地方引用该变量。
举例:
/*test.h*/
class ST_Scen_Instance
{
public:
// 这里就有个违反规范的地方是没有全大写,主要是全大写个人觉得阅读起来比较困难
static const QString LatitudeName;
static const QString LongitudeName;
static const QString HeightName;
inline void changeLLA(double latitude, double longitude, double height);
inline double longitude() const;
inline double latitude() const;
inline double height() const;
};
void ST_Scen_Instance::changeLLA(double latitude, double longitude, double height)
{
for (auto ¶ : m_lParameterValue)
if (para.m_sName == QString(LongitudeName))
para.m_sInitialValue = QString::number(longitude);
else if (para.m_sName == QString(LatitudeName))
para.m_sInitialValue = QString::number(latitude);
else if (para.m_sName == QString(HeightName))
para.m_sInitialValue = QString::number(height);
}
inline double ST_Scen_Instance::longitude() const
{
for (auto ¶ : qAsConst(m_lParameterValue))
if (para.m_sName == QString(LongitudeName))
return para.m_sInitialValue.toDouble();
return 116;
}
inline double ST_Scen_Instance::latitude() const
{
for (auto ¶ : qAsConst(m_lParameterValue))
if (para.m_sName == QString(LatitudeName))
return para.m_sInitialValue.toDouble();
return 40;
}
inline double ST_Scen_Instance::height() const
{
for (auto ¶ : qAsConst(m_lParameterValue))
if (para.m_sName == QString(HeightName))
return para.m_sInitialValue.toDouble();
return 0;
}
/*test.cpp*/
const QString ST_Scen_Instance::LatitudeName = "m_Latitude";
const QString ST_Scen_Instance::LongitudeName = "m_Longitude";
const QString ST_Scen_Instance::HeightName = "m_Altitude";
单例类可谓非常常用,但使用时容易忘记多线程访问的问题。参考:C++ 线程安全的单例模式总结。
建议使用最简单的写法为:
/// 内部静态变量的懒汉实现 //
class Single
{
public:
// 获取单实例对象
static Single &GetInstance();
// 打印实例地址
void Print();
private:
// 禁止外部构造
Single();
// 禁止外部析构
~Single();
// 禁止外部复制构造
Single(const Single &signal);
// 禁止外部赋值操作
const Single &operator=(const Single &signal);
};
Single &Single::GetInstance()
{
// 局部静态特性的方式实现单实例
static Single signal;
return signal;
}
void Single::Print()
{
std::cout << "我的实例内存地址是:" << this << std::endl;
}
Single::Single()
{
std::cout << "构造函数" << std::endl;
}
Single::~Single()
{
std::cout << "析构函数" << std::endl;
}
/// 内部静态变量的懒汉实现 //
由于Qt内建的一套隐式内存共享策略,即新对象引用共享数据只单纯让引用数+1,而不需要拷贝数据(这样使用值传递和const &传递效果基本一样了),只有在写时才会发生深拷贝操作。参考:Implicit Sharing。
隐式操作在绝大多数时候只会发生在后台,与程序员无关,但在少数情况下,这将会带来一定影响,甚至是致命影响。
首先是官方提到的案例,迭代器:
QVector<int> a, b;
a.resize(100000); // make a big vector filled with 0.
QVector<int>::iterator i = a.begin();
// WRONG way of using the iterator i:
b = a;
/*
Now we should be careful with iterator i since it will point to shared data
If we do *i = 4 then we would change the shared instance (both vectors)
The behavior differs from STL containers. Avoid doing such things in Qt.
*/
a[0] = 5;
/*
Container a is now detached from the shared data,
and even though i was an iterator from the container a, it now works as an iterator in b.
Here the situation is that (*i) == 0.
*/
b.clear(); // Now the iterator i is completely invalid.
int j = *i; // Undefined behavior!
/*
The data from b (which i pointed to) is gone.
This would be well-defined with STL containers (and (*i) == 5),
but with QVector this is likely to crash.
*/
然后同样是迭代的问题,详见官方API:
QString s = ...;
for (QChar ch : s) // detaches 's' (performs a deep-copy if 's' was shared)
process(ch);
for (QChar ch : qAsConst(s)) // ok, no detach attempt
process(ch);