编码之道
2005-12-30 李欣蔚 翻译
引入
这篇文章记述了我多年开发所使用的编码风格.我的风格并不具有广泛性.事实上,我不知道还有谁和我编码的怪异风格相近.但是我喜欢它并且想和你们分享(你们真是幸运的家伙!).我在所有语言都使用它,包括:C,C++,JAVA,C#,Python,…
如果你想马上快速地浏览一下此种风格,翻到本页底部,看看源代码到底是如何用我的deWitters风格书写的,你就能马上明白它们是如何好了.
为什么需要编码风格?
每一位程序员都使用某种编码风格--好的或者糟糕的.一种编码风格能够给予代码一致的外观.它能够让算法更加清晰或者更加复杂.下面是使用可靠的编码风格的2个主要原因:
- 开发清晰和可读的代码,这能够让阅读它的其它人能够迅速理解它的意思.更加重要的是当你回头看一些一年前写的代码的时候,你不会开始想:”我记不清楚了,这简直是在浪费时间…”
- 当团队协作时,最好是所有人都使用统一的编码风格,这样代码都具有统一的外观.
因为大多数代码都是我自己开发的,我不需要考虑第二个原因.另外还因为我非常固执,我不会采用其它人的风格.这是为什么我的风格是完全能充分产生清晰可读代码的原因.
基本规则
我在下面的规则中列举了编码风格的重要的几个方面:
- 所有代码都应该尽可能的可理解
- 所有代码应该尽可能的可读,除非它和上面的规则相冲突
- 所有代码应该尽可能简单,除非它和上面的规则相冲突
对待上面的规则的最好方式是让所有事情尽可能的简单,除非具有足够的可理解性和可读性.引用爱因斯坦的话:
让所有的事情尽可能的简单,但不要过分简单.
由于现代编程语言的诞生,编写可理解和阅读的代码变得有可能.完全使用汇编语言的时代已经离我们远去.因此我的编码风格试图尽可能的接近自然语言.你会说读我的代码就像读书一样.这也可能是为什么我很少为我的代码写文档的原因.我几乎从不写文档!我甚至认为写文档是”有害的”的(并且写的也不酷)只有当我写些古怪东西的时候我才用注释解释为什么.在下认为,注释应该从不解释你的代码做什么;而应该让你的代码说它自己是做什么的.
任何傻瓜都能写计算机能够理解的代码.优秀的程序员才能写人能够理解的代码.
马丁 弗诺尔
标识符
让我们以编码风格中最重要的主题--标识符—作为开始.所有标识符和你的其它代码以及注释都应该用英语来书写.软件项目从一个人传给另一个人,从一家公司传给世界另一端的另一家公司是非常寻常的.正因为你并不知道你的代码会传给谁,所以将它们全部用英语书写.
变量
变量命名应该全部用小写字母并用下划线分隔单词.它更符合书写习惯并因为它最具有可读性.下划线恰好代替了在书写习惯中的空格.相信我,一个叫做”RedPushButton”的并不能像”red_push_button”一样具有容易并快速的阅读.
如果你想让变量具有可理解性,你必须给它们取明显的名字.非常清楚的是变量都是表示某些”对象”或者”值”的,因此为它们命名.但不要在诸如kuidsPrefixing vndskaVariables ncqWith ksldjfTheir nmdsadType上面浪费时间,因为它们不可读也不清晰.如果你有一个变量”age”,它很明显是int或者unsigned short.如果它是”filename”,很明显它必须是字符串.简单!某些时候对于某些变量,如果你在它的命名中包含类型将具有更好的可读性.比如对于GUI按钮:”play_button”或者”cancel_button”.
下面是某些能够增加变量可读性的前缀和后缀.下面列出了非常常用的一些:
is_,has_
对于所有boolean值使用这些前缀,这样就不会在类型上出错.它同样对
于if语句非常适合.
the_
对于所有的全局变量使用”the_”,它能够非常清楚的表示这里只有一
个.
_count
所以_count表示元素的个数.不要使用复数形式”bullets”代替”bullet_count”,因为复数将表示数组.
数组或者表示列表的其它变量必须写成复数形式,比如enemies, walls 和 weapons.尽管如此,你不需要对所有数组类型使用复数,因为某些数组并不真正表示项目序列.比如”char filename[64]”后者”byte buffer[128]”.
Const或者Final
Const或者final必须用大写形式表示,并使它们更加具有可读性.比如MAX_CHILDREN,X_PADDING或者PI.这种用法很广泛并且应该被用来避免和普通变量相混淆.
你能够在常量名字中使用MAX和MIN表示极限值.
类型
类型定义了变量的分类.它是有点抽象,所以我不能够使用英语作为如何书写它们的参考.但是我们应该明确区分它们和其它标识符之间的差别.所以对于类型,我使用UpperCamelCase.对于每一个class,struct,enum或者其它在你的变量声明之前的事物都使用UpperCamelCase.
以此种方式命名你的类型能够让你对普通变量使用同样的名字,比如
HelpWindow help_window;
FileHeader file_header;
Weapon weapons[ MAX_WEAPONS ];
程序流程
if, else if, else
书写if语句有多种方式.让我们从圆括号开始.这里有3种主要的放置你的圆括号的方式:
if(condition)
if (condition)
if( condition )
我从来没有在英语正文中看到圆括号像例1一样放置的,所以为什么我要像那样编码呢?哪些单词恰好被不适当的分隔了.第二个例子将条件和圆括号放在一起代替了if语句,同时圆括号居然是if语句的一部分而不是条件语句的一部分.只有最后一个例子有优点,它具有更好的圆括号结构的一个概貌.
if (!((age > 12) && (age < 18)))
if( !((age > 12) && (age < 18)) )
就个人而言,我本应该以不同方式写这段代码,但它只是作为一个示例.
现在对于花括号应该怎么办呢?不要使用它们!不幸的是C,C++,JAVA或者C#不允许这样做,只有Python允许.所以我们不能丢掉它们,但我们能做什么能够让它看起来像Python程序一样简洁呢?
if( condition ) {
statements;
}
else if( condition ) {
statements;
}
else {
statements;
}
当条件过长,你必须将它们断行.试着在操作符之前将它们断开,并且条件保持最低的关联.在下一行与前面对齐并使用缩排来展示嵌套结构.不要把花括号正好写在条件的后面,在这种情况下将它们紧挨下一行使子块清晰:
if( (current_mayor_version < MIN_MAYOR_VERSION)
|| (current_mayor_version == MIN_MAYOR_VERSION
&& current_minor_version < MIN_MINOR_VERSION) )
{
update();
}
当if条件只有一条语句的时候,你可以不使用花括号,但是要确保你将语句写在下一行,除了它是一条return语句或者break语句.
if( bullet_count == 0 )
reload();
if( a < 0 ) return;
while
While循环与if结构书写一样.我为每一个缩进使用4个空格
while( condition ) {
statements;
}
对于do-while循环,将while与紧邻的花括号放在相同一行.如果在子块的开始或者结尾的while就不会有混淆.
do {
statements;
} while( condition )
for
for循环一生有且仅有的意图就是迭代.这就是它要做的!for循环常常能够被while循环代替,但是请不要这样做.当你对某些元素进行迭代的使用,试着使用’for’,只有当它不能解决问题的时候,才使用’while’.’for’结构非常直观:
for( int i = 0; i < MAX_ELEMENTS; i++ ) {
statements;
}
使用I,j,k,l,m作为迭代数字,’it’作为对对象的迭代.
switch
Switch语句有与if和while结构相似的结构.唯一需要考虑的就是额外的标识符.同样在break后面留出多余的空格.
switch( variable ) {
case 0:
statements;
break;
case 1:
statements;
break;
default:
break;
}
Functions
函数
函数是用来干事儿的,它们的名字应该清晰.因此,通常都包含一个动词,没有例外!使用与变量相同的命名方式,这意味所有小写单词由下划线分隔开.这允许你在你的代码中使用小段句子以便让每个人都理解.
同样确保函数做的事情与它的名字相符,不要过多,也不要太少.所以如果由一个函数叫做”load_resources”,确信它只是装载资源而不会去初始化填充.某些时候,你图方便就把初始化的事情放在load_resources中,因为你在更高层已经调用它,但是这将在以后引起问题.我的deWiTTERS 风格使用非常少的注释,所以一个函数应该明确的展示它的名字叫它做的事情.并且当一个函数返回某些东西,确信它的名字清晰的反映它将返回什么.
某些函数以”阴和阳”对的形式出现,你应该统一你的命名方式.比如: get/set, add/remove, insert/delete, create/destroy, start/stop, increment/decrement, new/old, begin/end, first/last, up/down, next/prev, open/close, load/save, show/hide, enable/disable, resume/suspend等等.
下面是一个简单的函数调用.在开始的圆括号的后面以及末尾的圆括号前面使用空格,就像if结构一样.同样在都好后面空格,就像使用英语一样.
do_something( with, these, parameters );
当函数调用太长的时候,你应该将它们断开为几行.将下一行与前面对齐,这样结构非常明显,并以都好断开.
HWND hwnd = CreateWindow( "MyWin32App", "Cool application",
WS_OVERLAPPEDWINDOW,
my_x_pos, my_y_pos,
my_width, my_height
NULL, NULL, hInstance, NULL );
定义
下面是函数定义的例子:
bool do_something_today( with, these, parameters ) {
get_up();
go_to( work, car );
work();
go_to( home, car );
sleep();
return true;
}
确保你的代码不会太长,或者按照linus的说法是:函数的最长长度与函数的复杂性以及缩进层次成反比”.所以,如果你有一个概念性的简单函数,它是一个长(但是简单)case语句,你就必须对许多不同的case做许多不同的小事情.尽管如此,如果你有一个复杂的函数,并且你怀疑一个天赋不佳的一年级高中生都不知道函数是什么,你随时应该坚持最大限制.使用描述命名的辅助函数(如果考虑到注重性能的情况,你能够让编译器去inline它们,并且它会比你做的更好.)
类
对于class的命名方式我使用和类型一样的UpperCamelCase.不要为每个class都加上’C’前缀,那只是在浪费字节和时间而已.
对于任何事情,给class清晰并且明显的名字.如果一个class是”Window”类的子类,将它命名为”MainWindow”.
当创建一个新的class,记住任何事情都来自数据结构.
数据支配.如果你已经选择了正确的数据结构并将事情组织得当,算法将通常是不证自明的.数据结构而不是算法是编程的中心.
Fred Brooks
继承
“is a”关系应该使用继承.”has a”应该使用包含.确保不要过分使用继承.它本身是伟大的技术,但只有在被适当应用的情况.
成员
你应该明确成员和普通变量之间的差异.如果你不这样做.你将在以后后悔.某些情况下将它们命名为m_Member或者fMember.我喜欢使用对静态成员使用my_member,对静态普通变量使用our_member.这将在下面语句中非常不错:
if( my_help_button.is_pressed() ) {
our_screen_manager.go_to( HELP_SCREEN );
}
应用于变量命名的其它规则同样能够可用于成员.这里有一个问题我不能解决,那就是boolean成员.记住在boolean值中必须有”is”和”has”.当于”my_”结合的时候,你得到疯狂的名字,比如”my_is_old”和”my_has_children”.我也没有找到此问题的完美解决方案,所以如果你有任何建议,请发EMAIL给我.
你不应该将class的成员声明为public.某些时候它看起来能更快速的实现,并因此更好,但是你错了(我也曾经历过).你应该使用public的方法来取得class的成员.
方法
应用到函数的任何事物都能够应用在方法上,记住名字中要包含动词.确保在你的方法名字中不要包含class的名字.
代码结构
将相似的行对齐,能够让你的代码看起来更加统一:
int object_verts[6][3] = {
{-100, 0, 100}, {100, 0, 100}, { 100, 0, -100},
{-100, 11, 100}, (100, 0, -100}, {-100, 0, -100}
};
RECT rect;
rect.left = x;
rect.top = y;
rect.right = rect.left + width;
rect.bottom = rect.right + height;
千万不要将多行写在一行上面,除非你对此有好的理由.其它原因是相似的行应该为声明放每行一句:
if( x & 0xff00 ) { exp -= 16; x >>= 16; }
if( x & 0x00f0 ) { exp -= 4; x >>= 4; }
if( x & 0x000c ) { exp -= 2; x >>= 2; }
同种类型的相关变量能够以相同语句声明.这样使代码更加紧凑,并更加统一.但是不要将不相干的变量放在同一行.
int x, y;
int length;
命名控件,包
命名空间或者包应该用小写,而且不用任何下划线.为你写的每一个模块或者层使用命名空间,这样在代码中不同的层更加清晰.
设计
当我开始项目的时候我并不做太多前端设计.我只要在我的脑海中有一个全局结构就开始编写代码.代码进化—无论你喜欢还是不喜欢—给代码进化的机会.
进化的代码因为着重写糟糕的代码,并且在某些编码后你的代码将变糟糕.我使用下面的原则来保持代码中的良好结构.
- 当函数太长,将它划分为一些更小的辅助函数.
- 如果一个类包含太多的成员和方法,将这个类划分为辅助类并在主类中包含辅助类(不要在这里使用继承!)确保辅助类不会引用或者由于任何原因引用主类
- 当模块包含太多的类,将它划分为更多的模块,并且高层模块使用底层模块.
- 当你实现了功能或者修改了bug,通读一遍你改变的整个文件,并确保所有事物都处于非常完美的状态.
某些项目可能变大,非常大.处理这种增长的复杂性的方法是将你的项目分为不通的层.实践中,层作为命名空间实现的.底层被高层所使用.所以每一层为上一层提供功能.最高层为用户提供功能.
Files
文件
文件应该按照它包含的类的名字来命名.不要在一个文件中放多个class,这样在你搜索某个类的时候你才知道在那里去找.目录结构应该表示命名空间.
.h文件结构
C或者C++头文件显示了实现的接口.这是在设计a.h文件时候的关键知识.在一个class中,首先定义能够被其它类使用的”public”接口,然后定义所有”protected”方法和成员.人们使用类的非常重要的信息是使用首先显示出来的方法.我不使用private方法和成员,这样所有成员组织在class声明的底部.这样你能够能够快事看到类底部的内容.将方法以它们的意思进行分组.
/*
* license header
*/
#ifndef NAMESPACE_FILENAME_H
#define NAMESPACE_FILENAME_H
#include <std>
#include "others.h"
namespace dewitters {
class SomeClass : public Parent {
public:
Constructor();
~Destructor();
void public_methods();
protected:
void protected_methods();
int my_fist_member;
double my_second_member;
const static int MAX_WIDTH;
};
extern SomeClass the_some_class;
}
#endif
.java .cs文件结构
.java 或者 .cs文件并不提供class的界面,它们只包含实现.因为数据结构比算法更加重要,所以在方法之前定义成员.当浏览代码的时候,你能够得到关于此class的数据成员的印象.相似的代码应该组织在一起.
Here follows a sketchy overview of a .java or .cs file:
下面显示了对.java或者.cs文件的一个粗略概览:
/*
* license header
*/
package com.dewitters.example;
import standard.modules.*;
import custom.modules.*;
class SomeClass extends Parent {
public final int MAX_WIDTH = 100;
protected int my_first_member;
protected double my_second_member;
Constructor() {
}
Methods() {
}
}
笑话
某些人喜欢在他们的代码中放些小笑话,而其它人憎恨这类搞笑分子.以我来看只要不影响代码的可读性和程序的执行,你可以随便使用笑话.
deWiTTERS Style vs. others
这里我将给你看些活生生的代码.我偷了些别人的代码,并以我自己的方式重写.你可以自己判断一下我的风格到底是好是坏.在我看来,你能够快速的阅读我的代码,因为它们更断,并且所有标识符都被谨慎的命名.
如果你认为你已看到过能击败我的风格的代码,请给我写EMAIL,并且我将会把它写进更强的’deWiTTER’风格中,并发布在这里.
Indian Hill C Style
/**//*
* skyblue()
*
* Determine if the sky is blue.
*/
int /**//* TRUE or FALSE */
skyblue()
...{
extern int hour;
if (hour < MORNING || hour > EVENING)
return(FALSE); /**//* black */
else
return(TRUE); /**//* blue */
}
/**//*
* tail(nodep)
*
* Find the last element in the linked list
* pointed to by nodep and return a pointer to it.
*/
NODE * /**//* pointer to tail of list */
tail(nodep)
NODE *nodep; /**//* pointer to head of list */
...{
register NODE *np; /**//* current pointer advances to NULL */
register NODE *lp; /**//* last pointer follows np */
np = lp = nodep;
while ((np = np->next) != NULL)
lp = np;
return(lp);
}
Rewritten to deWiTTERS Style:
bool sky_is_blue() ...{
return the_current_hour >= MORNING && the_current_hour <= EVENING;
}
Node* get_tail( Node* head ) ...{
Node* tail;
tail = NULL;
Node* it;
for( it = head; it != NULL; it = it->next ) ...{
tail = it;
}
return tail;
}
"Commenting Code" from Ryan Campbell
/**//*
* Summary: Determine order of attacks, and process each battle
* Parameters: Creature object representing attacker
* | Creature object representing defender
* Return: Boolean indicating successful fight
* Author: Ryan Campbell
*/
function beginBattle(attacker, defender) ...{
var isAlive; // Boolean inidicating life or death after attack
var teamCount; // Loop counter
// Check for pre-emptive strike
if(defender.agility > attacker.agility) ...{
isAlive = defender.attack(attacker);
}
// Continue original attack if still alive
if(isAlive) ...{
isAlive = attacker.attack(defender);
}
// See if any of the defenders teammates wish to counter attack
for(teamCount = 0; teamCount < defender.team.length; i++) ...{
var teammate = defender.team[teamCount];
if(teammate.counterAttack = 1) ...{
isAlive = teammate.attack(attacker);
}
}
// TODO: Process the logic that handles attacker or defender deaths
return true;
} // End beginBattle
Rewritten to deWiTTERS Style:
function handle_battle( attacker, defender ) ...{
if( defender.agility > attacker.agility ) ...{
defender.attack( attacker );
}
if( attacker.is_alive() ) ...{
attacker.attack( defender );
}
var i;
for( i = 0; i < defender.get_team().length; i++ ) ...{
var teammate = defender.get_team()[ i ];
if( teammate.has_counterattack() ) ...{
teammate.attack( attacker );
}
}
// TODO: Process the logic that handles attacker or defender deaths
}