【GAD翻译馆】Ronimo编程风格指南
译者: 刘超(君临天下) 审校:梁君(君儿)
除非另有说明,以下博客文章均由Gamasutra论坛的成员撰写。
其所表达的观点和想法均是这些作者的,而并非是Gamasutra公司或其母公司的。
在上周,我展示了我们在Ronimo中使用的编程方法学,它描述了我们的工作流程。这周,我们将看看实际中我们用到的代码是什么样子的,这在我们的编程风格指南中被定义。我们的指南背后的想法是:如果所有的代码都以类似的方法进行呈现,这将对于代码的阅读和编辑变得更加的容易。例如,阅读一个不同风格的代码或者约定命名通常将会花费一定的时间来适应,借助于严格的编程风格指南,所有的Ronimo的程序员必须遵循,这可以避免上述现象的发生。
我并没有在别的公司看到大量的编程风格指南的东西,但是我听说过我们的编程风格指南比一般的其它公司要更加严格。我不确定这是不是真的,但是我可以完全假设,这是由于我被认为是严格的(有时候可能更甚之)。我们的风格指南并非是一成不变的:对于每一个规则都允许例外情况。如果我们的风格指南在某些特定的情况下真的没有任何意义,那么如果一个程序员在某些地方忽略了它,这是没有关系的。只要你有一个好的理由就行。
本文档中的一些选择是相当随意的。有时候替代方案可能也是很好的,但是没有明确的选择,你不能够对所有的程序员都有相似的格式要求。这对于主要的风格尤其更加重要。我知道这是一个热门的话题,并且我有一个明确的倾向性,对于替代主要风格有着很好的论据。(不过,如果其它主要风格的倡导者不会称之为“一个真正的主要风格”,那将会是一件好事情。)
我们的风格指南中的一个关键元素是我们想要代码尽可能读起来向英文。变量和函数的命名应该是可描述的,并且仅允许最常见的单词存在缩写。简洁并不是我所关心的,可读性才是。
并非我们编程风格指南中的所有内容都要被格式化。其它的则是关于实际的语言的构建。C 是一种有着巨大可能性的非常丰富的语言,但是有很多内容相当混乱或者在实际使用过程中有着太多的风险bug。例如,嵌套三进制运算在C 中是完全可行的,但是在实际中可读性很差,因此我们不允许这样使用。
我们的编程风格指南也包含一些使得跨平台开发变得容易的规则。在控制台上你通常无法选择你的编译器,因此你不得不与任天堂、索尼或者是微软这些平台进行合作,同样也包含这些编译器的限制。我们已经研究了C 在不同的平台上分别支持哪些功能,并且禁用了我们认为可能在其中一个平台上无法进行工作的新的C 特性的构建。由于我们目前没有实际在某些平台上进行开发,所以我们只是通过文档进行处理,但是我宁愿在这方面更加的严格。
在我们的编程风格指南中你可以看到另外一件事情是我不喜欢复杂的语言结构。C 允许一些令人影响深刻的特性,尤其是在使用模板或者是宏时。虽然我明白这些技巧有时候是非常有用的,但是我通常不喜欢它们,一旦它们变得难以阅读时。在极少数的情况下,如果这些技巧是真正需要的,它们会被允许,但是通常我更加喜欢避免复杂的语言结构。
关于编程风格的一个特别热门的争论是如何对类成员变量进行标记。如果Car类拥有一个浮点数类型的speed,我们应该把它写成speed,mSpeed,_speedor还是别的什么?我选择简单的称之为speed。这也是因为我想要尽可能的像英文一样进行编程。如果存在更多的前缀和下划线,它会离自然语言越来越远,这也让代码的阅读和理解变得越来越困难。
然而,很多程序员都有一个很好的理由来标记他们的成员变量;在编程中,了解一个变量是类成员变量,还是函数参数,或者是一个局部变量是十分重要的。这个争论是真实存在的,但是我认为我们的指南已经涵盖了所有内容:我们的编程指南包含了一个类或者函数的命名长度的限制。如果一个函数名很短,并且能够在屏幕上进行完整的显示,那么我们很容易看到变量来自哪里。我认为如果类和函数足够短,那么对于成员变量的标记显得不是真的很需要。
注意,对于函数和类的命名长度的问题是最容易在内容进行破坏的规则。有时候,将一个类或者是函数进行整齐的划分是相当困难的。在最后,我们的编程风格指南的目的是用来撰写清晰的代码,而不是通过笨拙的分割来阻止。尽管如此,在处理如何将类或者函数划分为更小的单元仍然存在一些真正的技巧。因此,如果你没有丰富的经验,更多的时候分割看起来不是整齐的,你只是看不到而已。我的建议是,对于任何类的理想的尺寸是在200-400行之间,但是严格规定是不可行的,因此编程风格指南中列出的要求更加宽松。
现在我已经讨论了我们编程风格指南背后的原因,让我们真正看它在实际中是什么样的!
TheRonimo Coding Style Guide
每一个规则都有例外。但是,尽可能的对于所有的代码保持固定的布局和风格。绝大多数是很难接受的,固定的编程风格需要一个人舍弃自己的风格来遵守这些规则。一旦习惯了这些规则,那么将会很容易的阅读类似的代码。
当使用其它编程语言而不是C 进行编程时,尽量保持与C 编程标准的一致性,但是当然也包括一些原因。在底层也会有一些关于C#的特殊的声明。
C
l 所有的代码和注释应该是英式英文。而不是美式英文。因此,以下这些是正确的:colour, centre, initialiser。而这些是不正确的:color,center, initializer.
l 每一个逗号之后应该有一个空格,例如 doStuff(5, 7, 8)
l 每一个操作符前后应该有空格,例如:使用5 7来替代5 7
l Tabs按键应该有相同的四个空格的大小。应该将Tabs作为制表符,而不是空格。
l 函数和静态变量在.h文件和.cpp文件中应该具有相同的顺序。
l 使用#pragmaonce代替旧的文件防护(我们最近才开始使用这个,因此你仍然能在我们的代码中看到大量的旧的文件防护)。
l 尽量让函数长度短,每个函数最好不要超过50行。
l 避免使用非常大的类。尽可能的将一个类进行分割为你能够清晰的进行分割的样子。通常,尽可能的让一个类保持在750行以下。但是如果分割会造成代码的混乱,那么不要对类进行分割。以下是一些混乱的例子:过渡耦合的类,友元类和复杂继承类。
l 不要写太长的超过普通屏幕1920*1080分辨率的代码(确保资源管理器能够很好的出现在屏幕上)。
l 当将一条较长的语句分割为多条语句时,确保相关括号的缩进是正确的。例如:
1 2 3 4 5 | myReallyLongFunctionName(Vector2(bananaXPos xOffset, bananaYPos * multiplier), explodingkiwi); |
l 尽可能的在使用之前进行声明:尽可能在头文件中使用较少的include语句。例如,在使用Bert类时,将#include“Bert.h”语句移动到cpp文件中。
l include语句和声明的顺序如下:
n 在我们代码的最开始使用进行声明(按照字母表顺序)
n 然后开始include语句(按照字母表顺序)
n 空一行
n 然后对于每一个库:
u 对用到的库进行声明(按照字母表顺序)
u 库对应的include语句(按照字母表顺序)
l 与cpp文件对应的h文件应该被包含在最顶端
l 不要在函数中定义静态变量。而是使用类成员变量进行代替(尽可能使用静态的)。
l 正确的使用const。
l 变量和函数的命名应该具有可读性。长一些的命名是没有问题的,而不可读的命名是不行的。只有在命名是十分清晰而且被大众所知的情况下才可以使用缩写。
l 类、结构体和枚举类型的首字母应该大写。每一个单词首字母大写。在变量名中不要使用下划线。例如:
1 2 3 4 5 6 7 8 9 | class MyClass { void someFunction(); int someVariable; }; |
l 成员变量不要使用在变量之前加m或者_进行命名。函数应该尽可能的短来确保在类中的声明。不要使用this->来标记成员变量。
l 函数的实现永远不要写在头文件中。
l 模板函数不要在cpp文件中实现,而是要在主头文件包含的另外一个头文件中实现。这样一个类会包含3个文件:MyClass.h, MyClassImplementation.h和MyClass.cpp。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 | class MyClass { template
|
l 模板类型使用T。如果有别的相关的信息,你可以在T后面增加单词,例如TString。
l 几个类永远不要定义在同一个头文件中,除非一个类是另外一个类的一部分。
l 在函数之间要有两个空白行(在cpp文件中)。
l 使用空白行对代码进行结构化和组合使得代码具有可读性。
l 为代码添加大量的注释。
l 在每一个类之前写一段短的解释来说明该类是用来干什么的。尤其确保解释他们之间的关系(例如:该类是用来帮助类X做Y的)。
l 分支之间的{和}独自占据一行,不要将它们和if或者是for放在同一行。当然也不要漏掉。特殊情况是如果在if声明之后存在大量的相似的语句,在这种情况下,把它们放在同一行是允许的。例如:
1 2 3 4 5 6 7 | if ( banana && kiwi && length > 5 ) return cow; else if ( banana && !kiwi && length > 9 ) return pig; else if ( banana && !kiwi && length < 7 ) return duck; else return dragon; |
l 当写do-while函数时,将while与}放在同一行。
1 2 3 4 5 6 7 | do { blabla; } while (bleble); |
l switch语句的缩进如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | switch (giraffeCount) { case 1: text = “one giraffe”; break ; case 2: text = “two giraffes”; break ; case 3: // If it’s more than one line of code doStuffOnSeveralLines; text = “three giraffes”; break ; case 4: { // Can add curly braces for readability int x = getComplexThing(); text = “quadruple giraffe”; break ; } } |
l h文件和cpp文件中的函数参数应该具有相同的命名。
l 如果一个函数参数和类成员变量有着相同的命名,那么重新换一个命名,或者在函数参数之后增加一个_。例如:
1 2 3 4 5 6 7 | void setHealth( float health_) { health = health_; } |
l 预编译指令(以#开始的语句)应该尽可能的少,除了#include和#pragma once。
l 不要写宏。
l 函数内部变量只有在需要的时候进行声明,不要将它们全部放在函数开始。
l 在构造函数中,尽可能的使用初始化列表,而不是在结构体内部进行变量初始化。初始化列表的每一个变量独自占据一行。保证初始化列表中变量的顺序与类头文件中变量的顺序是一致的。
l 不要使用异常处理(除非你用到的第三方库需要它)。
l 不要使用RTTI(因此也不要使用动态转换)。RTTI会一定程度的影响性能,但是更重要的是RTTI常常违背面向对象设计原则。
l 只有在十分需要的情况下使用reinterpret_cast和const_cast。
l 不要提交编译时包含错误和警告的代码(也不要禁用警告/错误)。
l 不要提交违背现有功能性的代码。
l 不要全局变量。使用静态成员变量来代替。
l 使用我们自己的MathTools::abs来代替std::abs。这是由于std::abs在不同平台的实现是不一致的,这样会造成难以发现的bug。
l 总是显式的使用命名空间。不要在代码中使用 using namespace std 这种语句。
l 永远也不要使用go-to语句。如果你使用了,我们会检测出来的。
l 不要使用逗号操作符,例如 if(I = 7, I < 10)
l 不要使用联合体。
l 不要使用函数指针,除非一些库(例如STL排序函数)需要它。
l 只有在非常简单的情况下才能使用三元运算。永远不要嵌套使用三元运算。可以使用的一个例子如下所示:
1 | print(I > 5 ? “big” : “small”); |
l 对于艺术家或者是设计者,在进行计数时从0开始,正如代码中的数组一样。一些旧的工具可能仍然使用从1开始计数,但是任何新的艺术家的开发要从0开始。
l 当确认指针是否为空时,确保使用显示的方法。因此,使用 if(myPointer != nullptr) 来代替 if(myPointer)。
l 尽可能的使用资源获取即初始化(RAII)。不要创建一个独立的初始化函数 initialise(), 而是在它的构造函数里进行完整的初始化,这样将不会出现一个不完整的状态。
l 把构造函数和析构函数写在一起:为每一个new及时的写上对应的delete,这样你就不会在后面忘记写了。
l 如果你添加了临时的调试代码,那么使用QQQ进行注释。永远不要将有QQQ的代码进行提交:在提交代码之前将调试的内容进行删除。
l 如果你想要为之后做的事情进行标记,那么请使用QQToDo。如果这些事情是后续你自己完成的,那么在之后添加你的名字,例如QQToDoJ。只有在这件事情真的无法完成的时候,才可以将QQToDo进行提交。
l 在每一个类中先定义函数,再定义变量。关键词顺序是public/protected/private。这意味着在一个头文件中你不得不多次使用关键词public/protected/private(首先用于定义函数,然后用于定义变量)。
l 对于每一个类,先定义构造函数,接下来紧跟析构函数。如果它们是private或者protected类型,同样将它们放在类的最开始。
l 当一个变量有两种选择时,但是很清晰的并非是true/false,那么考虑使用枚举变量来代替布尔变量。例如,对于direction不要使用布尔类型isRight。取而代之的是使用包含Left和Right的枚举类型Direction。
l 不要使用std::string和std::stringstream,使用我们自己的类RString,RStringstream,WString和WStringstream(这些类有着我们的内存管理机制)。
l 浮点数变量time总是表示上一帧的时间,以秒为单位。如果time表示其它内容,请显示声明,例如使用timeExisting进行命名。
l 确保所有的代码与帧率都是无关的,这样使用浮点数变量time总是能够获取时间。
l 对于预期的结构进行显示清晰的定义。例如,避免使用下述定义:
1 2 3 4 5 6 7 8 9 | if (yellow) { return banana; } return kiwi; |
哪一个是真正预期的结果,它们和yellow有什么关系,banana和kiwi都可能是返回结果。使得该代码更容易阅读的显示的定义如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | if (yellow) { return banana; } else { return kiwi; } |
l 使用nullptr替代NULL。
l 我们仅对复杂的迭代类型使用auto。对于其它变量,我们使用显式的类型进行定义。
l 尽可能使用基于范围的循环,例如:
1 | for ( const Banana& ultimateFruit : myList) |
(注意当没有使用指针时,引用类型的定义是很重要的,在这个例子中,如果使用其它定义方式,那么Banana将会被复制一份。)
l 尽可能的使用override和final关键词。
l 如果一个函数是虚函数,总是要在函数之前添加关键词virtual,不仅仅在父类中添加该关键词,在它的每一个实现的子类中同样也要添加。
l 使用强制的类型定义枚举类型,因此使用class关键词进行定义。这样它们的转换和类一样。例如:
1 2 3 4 5 6 7 8 9 10 11 | enum class Fruit { Banana, Kiwi, ApplePie }; |
l 不要使用右值引用,除非你真的非常需要它们。
l 尽可能的使用unique_ptr。如果一个对象不是其所有的,那么将它存储为一个普通指针。
l 避免在初始化列表中创建复杂类型的实例。简单的复制和设置操作在初始化列表中完成,但是更加复杂的代码像是调用new操作,应该在构造函数内部完成。下面是上述操作的一个例子:
1 2 3 4 5 6 7 8 9 | FruitManager::FruitManager(Kiwi* notMyFruit): notMyFruit(notMyFruit) { bestFruitOwnedHere.reset( new Banana()); } |
l 只有当真正存在共享关系的情况下才可以使用shared_ptr。在通常情况下,尽量避免共享关系并使用unique_ptr。
l 对于单行过长的代码可以写成如下形式:
1 2 3 4 5 6 7 8 9 10 11 | auto it = std::find_if(myVec.begin(), myVec.end(), [id = 42] ( const Element& e) { return e.id() == id; }); |
l 不要使用MyConstructor= delete(这在我们可能用到的一些编译器上不支持)。
l 不要在初始化列表中使用C 11的特性(这在我们可能用到的一些编译器上不支持)。因此,我们不使用如下形式:
std::vector
l 不要使用任何C 14所增加的特性。
C#
l 将变量定义在文件的开头而并非末尾。在函数和变量之间进行一个严格的区分(就像我们在C 中做的一样),因此,首先定义所有变量,然后定义所有函数。
l 所有异步函数必须以Async作为名称的结尾,例如:eatBananaAsync。
这就是我们的编程风格指南!^_^尽管我能想到在某一些特殊的规则上你可能不同意,但是我认为对于任何公司有一些形式的编程风格之南是有用的。利用我们的编程风格指南对于创建你自己的指南是一个很好的开始。让我好奇的是:你们公司的编程风格指南是什么样的,你喜欢吗?你们有这样的指南吗?
【版权声明】
原文作者未做权利声明,视为共享知识产权进入公共领域,自动获得授权。