
iPhone项目新成立,也没有编码规范的积累,项目组本来是想拿老的C编码规范套用的,但评审一下就发现问题多多,之后找到了Google的Objective-C的编码规范,大家就先翻译一下咯
声明这是无版权翻译,也不对任何错误负责,不保证文章的完整性,我到现在也认不全语法。
(大半年的事后,决定对这份文档做重审,当然不是对修辞手法,而是处理内部的硬伤)
总览
背景知识
Objective-C是一个C语言的扩展语言,非常动态,非常的“面向对象”,它被设计成既拥有复杂的面向对象设计理念又可以轻松使用与阅读的语言,也是Mac OS X和iPhone开发的首选语言。
Cocoa是Mac OS X的主要应用框架,提供迅速开发各种功能的Mac OS X应用的Objective-C类集合。
Apple已经有一个很好也被广泛接受的Objective-C的编码规范,Google也有类似的C++编码规范,这份Objective-C编码规范很自然是Apple和Google的共同推荐的组合。所以,在阅读本规范前,确保你已经阅读了:
Apple's Cocoa Coding Guidelines
Google's Open Source C++ Style Guide
注意所有已在Google的C++编码规范里的禁用条款在Objective-C里也适用,除非本文档明确指出反对意见。
本文档旨在描述可供可适用于所有Mac OS X代码的Objective-C(包括Objective-C++)编码规范和实践。规范中的许多条款已经改进也不断的被其他的项目和团队所证明其指导性。Google的相关开源项目都遵守此规范。
Google已经发布了一份作为Google Toolbox for Mac project (文档中简称为GTM)的组成部分的遵守本规范的开源代码。这份开放代码也是本文很好的例证(原文看不太懂--Code meant to be shared across different projects is a good candIDate to be included in this repository. )
注意本文不是Objective-C的教学指南,我们假设读者已经了解语言。如果你是一个Objective-C的初学者或需要重温,请阅读The Objective-C Programming Language .
示例
人们说一个例子胜过千言万语,所以就让我们用例子来让你感受以下编码规范的风格,留间距,命名等等。
下例是一份头文件,展示对@interface 声明正确的注释和留间距
下例是一份源文件,展示对接口的@implementation 的实现的正确注释和留间隔。它也包括了主要方法如getters,setters,init ,和dealloc 的相关实现。
间隔与格式化
空格对tab键
仅使用空格,缩进两个。
我们使用空格用于缩进,不要在编码时使用tab键,你应该设置你的编辑器将tab键转换成对应的空格。
行长度
代码中的每行文本不要超过80个字符的长度。
尽管Objective-C正变得比C++更加繁冗,为了保持规范的互通性,我们还是决定保持80字符长度的限制。这比你想象中的更容易做到。
我们知道本条款是有争议的,但已有此多的代码已经遵从了本条款,即使只是保持一致性也是一个充足的理由。
你可以在Xcode里清楚地发现代码中的违规,设置 Xcode > Preferences > Text Editing > Show page guIDe. (之后就可以在代码编辑区域里看到一条指定字符长度的指示线了)
方法声明与定义
留一个空格在-或+和返回类型之间,但参数列表里的参数之间不要留间隔。
方法应该写成这样: - (voID)doSomethingWithString:(Nsstring *)theString { ... } ? - (voID)doSomethingWithString:(Nsstring *)theString { ... }
星号前的空格是可选的,你可以根据原来的代码风格自行决定。
如果参数过多,推荐每个参数各占一行。使用多行的情况下,以参数前的冒号用于对齐:
(很遗憾这里仅有Google Chrome浏览器能看出是冒号对齐的......)
当第一个关键字比其他的短时,后续行至少缩进四个空格。这样你可以让后续的关键字垂直对齐,而不是用冒号对齐: - (voID)short:(GTMFoo *)theFoo longKeyword:(NSRect)theRect evenLongerKeyword:(float)theInterval { ? - (voID)short:(GTMFoo *)theFoo longKeyword:(NSRect)theRect evenLongerKeyword:(float)theInterval {
方法调用
方法调用的格式和方法声明时的格式时一致的,如果格式风格可选,遵从原有代码的风格。
调用应该将所有参数写在一行: [myObject doFooWith:arg1 name:arg2 error:arg3]; ? [myObject doFooWith:arg1 name:arg2 error:arg3];
或者每个参数一行,用冒号对齐:
(对齐效果如前说明)
不要使用如下风格的写法
在声明和定义时,如果因为关键字长度使就算有四个空格在前仍然无法用冒号对齐,那么就让后续行缩进四个空格而不是冒号对齐:
@public 和 @private
权限控制符@public 和@private 缩进一个空格.
类似C++的public,protected,private:
异常
每个异常标签的@ 和开括号({ )写在统一行,标签和开括号间隔一个空格,同样适用于@catch 语句。
如果你决定使用Objective-C的异常,那么就按如下格式,不过你最好先看看AvoID Throwing Exceptions(见后)条款,了解为何你不应使用异常。 @H_765_1301@@try { foo(); @catch (NSException *ex) { bar(ex); @finally { baz(); ? @try { foo(); @catch (NSException *ex) { bar(ex); @finally { baz();
命名
命名规则对于维护代码来说是非常重要的。Objective-C方法名往往很长,不过这也有好处,读代码就像读散文(放屁),让很多注释变得毫无意义。
写纯Objective-C代码时,我们基本上遵守标准Objective-C naming rules ,这些规则和C++的规则有很大的不同。比如Google的C++代码规范推荐变量名构词之间使用下划线隔开,而本文档推荐驼峰法,也是Objective-C社区的标准。
所有类,类别,方法,以及变量如包括缩写,则缩写部分使用全大写的缩写(Initialisms )形式。这遵守Apple的标准,比如URL,TIFF以及EXIF。
当写Objective-C++代码时,情况就不是那么单一了。许多项目需要实现带一些Objective-C代码的跨平台的C++APIs或者连接后台的C++代码与前台的原生Cocoa代码.这会造成两种规范直接冲突。
我们的解决方法是根据方法/函数风格来决定。如果在@implementation 块,就使用Objective-C的命名规则;如果在C++的方法实现块,就使用C++的命名规则。避免了实体变量和本地变量在一个函数内命名规则冲突的情况,而这种情况是对可读性的极大损害。
文件命名
文件名反映了它所包含的实现类的名字,遵从你所在项目的习惯。
文件扩展名使用如下规则
类别的文件名应该包含扩展类的名字,比如GTMNsstring+Utils.h or GTMNSTextVIEw+autocomplete.h
Objective-C++
在一份源文件里,Objective-C++代码遵守当前方法/函数的风格
为了尽量减少不同命名风格间的冲突,使用当前方法的风格。如果在@implementation块,使用Objective-C命名规则,如果在C++类的函数实现块,使用C++命名规则。
类命名
类名(不包括类别和协议名)应该用大写开头的驼峰命名法。
在应用级别的代码里,尽量不要使用带前缀的类名。每个类都有相同的前缀不能提高可读性。不过如果是编写多个应用间的共享代码,前缀就是可接受并推荐的做法了(型如 GTMSendMessage )。
类别命名
类别命名应该以两三个字符的分类前缀作为一个项目或通用的公用部分。类别名应该包含类的扩展。
举个例子,如果我们想要创建一个基于Nsstring 的类别用于解析,我们应该把类别放到名字是GTMNsstring+Parsing.h 的文件里,而类别本身的名字则是GTMStringParsingAdditions (是的,我们明白这个类别和其文件名字不匹配,但这个文件可能还包括其他用于解析相关的类别)。类别的方法应该都使用一个前缀(型如gtm_mycategoryMethodonAString ),以防止Objective-C代码在单名空间里冲突。如果代码本来就不考虑共享或在不同的地址空间(address-space),方法命名规则就没必要恪守了。
Objective-C 方法命名
方法使用小写开头的驼峰法命名,每个参数都应该小写开头。
方法名应该尽可能读起来像一句话,参数名就相对方法名的补充说明(比如convertPoing:fromrect: 或者replaceCharactersInRange:withString: ),详见Apple's Guide to Naming Methods
存取(Accessor)方法应该一致的在"取(getting)"的时候直接用变量名而不是在签名加"get",如下: - (ID)getDelegate; // AVOID - (ID)delegate; // GOOD ? - (ID)getDelegate; // AVOID - (ID)delegate; // GOOD
不过这仅针对Objective-C代码,C++代码仍然遵循自己的代码规范。
变量命名
变量名使用小写开头的驼峰法,类成员变量名最后加一个下划线,比如:myLovalVariable,myInstanceVariable_ . 下面看不懂,原文Members used for KVO/KVC bindings may begin with a leading underscore iff use of Objective-C 2.0's @property isn't allowed.
一般变量命名
不要使用匈牙利命名法去标记语法,比如静态类型或变量类型(int或pointer之类的)。使变量名尽量可以推测其用途属性具有描述性。别一心想着少打几个字母,让你的代码可以迅速被理解更加重要。比如:
实体变量
实体变量用驼峰法命名并后缀下划线,就像usernameTextFIEld_ . 然而我们允许一种例外就是用KVO/KVC去绑定一个实体变量而Objective-C 2.0 不能用(因为 *** 作系统的限制)的情况,此时也可用前缀下划线的方法给每个变量命名。如果可以使用Objective-C 2.0,@property 和 @synthesize 提供了遵守命名规范的解决方法。
常量
常量(预定义,枚举,局部常量等)使用小写k开头的驼峰法,比如kInvalIDHandle , kWritePerm .
注释
尽管写起来很痛苦,但注释仍然是使代码保持可读性的极端重要的方式。下面的条款描述了你应该注释什么以及在哪里做注释。但是记住:即使注释是如此重要,最好的代码还是自说明式的。起一个有意义的名字比起一个晦涩的名字然后在用注释去解释它好的多。
当你写注释的时候,记住注释是写给读者,即下一个要理解你的代码并继续开发的人。"下一个"完全可能就是你自己。
同样,所有C++编码规范的条款仍然适用,只是增加了一些条款,如下.
文件注释
每个文件的开头都是版权声明,接着是文件内容的描述。
法律声明和作者栏
每个文件都应该包含如下信息:
一份版权声明(比如 copyright 2008 Google Inc .)
许可版本 为项目选择合适的许可版本(比如Apache 2.0,BSD,LGPL,GPL)
如果你把别人写的文件做了相当大的改动,就把自己添加到作者栏去。这样别的开发者就方便联系这个文件的实际开发人员了。
声明注释
每个接口,类别,协议都必须伴随描述它的用途以及如何整合的注释。
如果已经在文件的顶部写了接口的详细描述,你也可以简单的写如"见文件顶部的完整描述",当然要有这些注释的顺序安排。
此外public接口的每个方法都应该添加关于函数,参数,返回值以及副作用的注释。
文档默认类都是同步的,如果类实例可以多线程访问,必须要加上额外的说明。
实现注释
使用竖线引用变量或符号,而不是用引号。
这可以减少歧义,特别是当符号本身就是个常见的词时,可能使句子显得支离破碎,比如符号是"count": // Sometimes we need |count| to be less than zero. ? // Sometimes we need |count| to be less than zero.
或者是对于那些已经存在引号的情况
对象所有权
使指针所有权的模型尽可能清晰,当它属于Objective-C的使用惯例时(不懂,原文是Make the pointer ownership model as explicit as possible when it falls outsIDe the most common Objective-C usage idioms. )
实例变量指向NSObject派生类的对象时都假定是retain的,如果它们不是retain的则需要加上"weak"的文档说明。对应的,实体变量如果标记上IBOutlets则是假定为非retain的,若实际上用了retain,就必须加上"strong"的说明。
当实例变量指向核心库,C++或其他非Objective-C对象时,必须永远用注释说明是strong还是weak的。必须注意为了支持Objective-C对象里的自动化C++对象的封装是默认被关闭的的(这句话有歧义,原文是Be mindful that support for automatic C++ objects encapsulated in Objective-C objects is Disabled by default),这里 有说明。
strong和weak说明的文档示例:
strong
对象会在类中retain
weak
对象不会在类中retain (比如一个委托)
Cocoa和Objective-C特性
成员变量应该定义为@private ID myInstanceVariable_; // public accessors, setter takes ownership - (ID)myInstanceVariable; - (voID)setMyInstanceVariable:(ID)theVar; ? ID myInstanceVariable_; - (ID)myInstanceVariable; - (voID)setMyInstanceVariable:(ID)theVar;
明确指定初始化
注释并说明指定的初始化。
明确指定初始化对想要子类化你的类的时候时很重要的。那样,子类化时只需要做一个或多个初始化去保证初值即可。这也有助于在以后调试你的类时明了初始化流程。
重写指定初始化
当重写一个子类并需要init... 方法,注意要重写父类的指定初始化方法。
如果你没有正确重写父类的指定初始化方法,你的初始化方法可能不会被调用,这会导致很多微妙而难以排除的错误。
初始化
没必要在初始化方法里把变量初始化为0 或者nil ,这是多余的。
所有新分配内存的对象内容都初始化为0(除了 isa ),所以不要在init 方法里做无谓的重初始化为0的 *** 作。
保持公有API简明
保持你的类简单,避免"厨房水槽"似的APIs,如果一个方法没必要公开就不要公开。使用私有类别保证公开头文件的简洁。
和C++不同,Objective-C无法区分公有私有方法,因为它全是公有的。因此,除非就是为了让用户调用所设计,不要把其他的方法放到公有API里。这样可以减少不期调用的可能性。这还包括重写父类的方法。对于那些内部实现的方法,在实现文件里使用类别而不是将方法定义在公有头文件里。 // GTMFoo.m @interface GTMFoo (PrivateDelegateHandling) - (Nsstring *)doSomethingWithDelegate; // Declare private method @implementation GTMFoo(PrivateDelegateHandling) ... - (Nsstring *)doSomethingWithDelegate { // Implement this method ? // GTMFoo.m #import "GTMFoo.h" @interface GTMFoo (PrivateDelegateHandling) - (Nsstring *)doSomethingWithDelegate; // Declare private method @end @implementation GTMFoo(PrivateDelegateHandling) ... - (Nsstring *)doSomethingWithDelegate { // Implement this method
在Objective-C 2.0之前,如果你在私有@interface 里声明了一个方法,但忘记在主@implementation 文件里实现了,编译器不会有什么反应(这是因为你没有在不同的类别里实现这些私有方法)。解决方案在是把函数写到@implementation 里并指明类别。
如果你用的是 Objective-C 2.0,你应该使用类扩展 而不是声明私有类别,如下: @interface GMFoo () { ... } ? @interface GMFoo () { ... }
如此就可以保证函数做了声明但没有在@implememtation 里实现的时候编译器会警报。
再者,"private"方法并不是真正的private,你可能会无意间重写了父类的一个"private"方法,这回导致BUG的涌现。总的来说,私有方法应该使用更特别的名字以阻止子类化时并不期望的重写。
最后,对于绝大多数类而言,Objective-C的类别是将@implelemtation做可理解的分块,添加新的应用级别的功能的最佳途径。比如,与其在你的项目里随便找个类来实现字符串的"中间截断"功能,不如创建一个新的Nsstring 类别。
#import 和 #include
用#import 导入Objective-C或Objective-C++头文件,用#include 导入C或C++头文件
根据头文件的语言去选择合适的导入方式。
当导入的头文件使用Objective-C或Objective-C++语言时,使用#import .
当导入标准C或 C++头文件时,使用#include . 头文件应该使用自己的#define 重加载保护
有些Objective-C头文件没有#define 重加载保护,所以只应该用#import 导入。因此Objective-C头文件只应该被Objective-C源文件或其他的Objective-C头文件所导入。这种情况下全部使用#import 是合适的。
标准C和C++头文件不包含任何Objective-C元素都可以被一般的C或C++文件导入。因为标准C和C++里根本没有#import ,所以也只能用#include 导入。在Objective-C代码中使用#include 一致的导入这些头文件。
本条款有助于跨平台项目的无意错误。一位Mac开发者引入一份新C或C++头文件时可能会忘记添加#define重加载保护,因为在Mac上用#import 导入文件不会引发问题,但在别的使用#include 的平台就可能出问题。在所有平台一致的使用#include 意味着要么全部成功要么全部失败,避免了那种另人沮丧的一些平台上可以运作而另一些不行的情况。
使用框架根
导入框架根的头文件而不是分别导入框架头文件
看起来从Cocoa或Foundation这些框架里导入个别的文件很不错,但实际上你直接导入框架根头文件效率更高。框架根已经被预编译故可更快的被加载。还有,记住用#import 指令而不是#include 导入Objective-C的框架。
构建时即设定autorelease
当创建新的临时对象时,在同一行代码里就设定autorelease而不是写到这个方法的后面几行去
即使这样可能会造成一些轻微的延迟,但这样避免了谁不小心把release 去掉,或在release 之前就return 而造成的内存泄露,如下
优先autorelease而非retain
对象赋值时尽量采用autorelease 而不是retian 模式。
当把一个新创建的对象赋予一个变量的时候,第一件要做的事情就是先释放原来变量指向的对象以防止内存泄露。这里也有很多"正确的"方法去做这件事。我们选择autorelease时因为它更不倾向于出错。小心在密集的循环里可能会很快填满autorelease池,而且它也确实会降低效率,但权衡下来还是可以接受的。
以声明时的顺序dealloc处理实例变量
dealloc 应该用在@interface 声明时同样的顺序处理实例变量,这也有助于评审者鉴别。
代码评审者检查或修正dealloc 的实现要确保所有retain 的实例变量都获得了释放。
为了简化评审dealloc ,将释放retain 的实例变量代码保持和@interface 里声明的顺序一致。如果dealloc 调用了其他方法去释放实例变量,添加注释说明那些实例变量被这些方法所处理了。
Setters copy Nsstrings
在Nsstring 上调用Setters 方法时,永远使用copy 方式。(不太懂,原文是Setters taking an Nsstring,should always copy the string it accepts. )
永远不要retain 一个字符串,这可以防止调用者在你不知到的情况下修改了字符串。不要以为你可以改变Nsstring 的值,只有NSMutableString 才能做到。
避免抛出异常
不要 @throw Objective-C的异常,不过你还是要做好准备捕获第三方以及系统调用抛出的异常。
我们的确在编译时加入了-fobjc-exceptions 指令(主要是为了获得@synchronized ),但我们并不@throw 。当然在使用第三方库的时候是允许使用@try,@catch, 以及@finally 的。如果你确实使用了,请务必明确到文档中哪个方向你想抛出什么异常。
除非你写的代码想要泡在MacOS 10.2或更之前,否则不要使用NS_DURING,NS_HANDLER,NS_ENDHANDLER,NS_VALUERETURN and NS_VOIDRETURN 这些宏。
另外你要小心当写Objective-C++代码的时候,如果抛出Objective-C异常,那些栈上的对象不会被清理。示例: class exceptiontest { exceptiontest() { NSLog(@"Created"); } ~exceptiontest() { NSLog(@"Destroyed"); } voID foo() { exceptiontest a; NSException *exception = [NSException exceptionWithname:@"foo" reason:@"bar" userInfo:nil]; @throw exception; int main(int argc, char *argv[]) { GMautoreleasePool pool; @try { foo(); @catch(NSException *ex) { NSLog(@"exception raised"); return 0; ? class exceptiontest { exceptiontest() { NSLog(@"Created"); } ~exceptiontest() { NSLog(@"Destroyed"); } voID foo() { exceptiontest a; NSException *exception = [NSException exceptionWithname:@"foo" reason:@"bar" userInfo:nil]; @throw exception; int main(int argc, char *argv[]) { GMautoreleasePool pool; foo(); @catch(NSException *ex) { NSLog(@"exception raised"); } return 0;
将会有如下输出: 2006-09-28 12:34:29.244 exceptiontest[23661] Created 2006-09-28 12:34:29.244 exceptiontest[23661] exception raised ? 23661] Created 23661] exception raised
注意这里的析构函数永远没有机会被调用。这是在你想用栈上的智能指针比如shared_ptr,linked_ptr ,还有STL对象的时候不得不关注的一个核心问题。我们不得不痛心地说,如果你一定要在Objective-C++代码里抛出异常,那就请一定使用C++的异常。永远不要重新抛出一个Objective-C的异常,也不允许在异常块即@try,@catch,@finally 里生成栈上的 C++对象(比如std::string,std::vector 等).
nil检查
仅在校验逻辑流程时做nil检查。
使用nil检查不是为了防止程序崩溃,而是校验逻辑流程。向一个空对象发送一条消息是由Objective-C运行时处理的。方法没有返回结果,你也可以安心走下去.然而这里也有一种,依执行架构不同而返回尺寸和OS X的版本(这段不懂)(见)Apple's documentation for specifics 。
注意这里和C/C++的空指针检查是完全不同的,在那些环境里,并不处理空指针情况并可能导致你的应用程序崩溃。不过你仍要自己确保提领的指针不为空。
BOol类型陷阱
整形的转换为BOol 型的时候要小心。不要直接和YES做比较。
BOol 在Objective-C里被定义为unsigned char,这意味着它不仅仅只有YES (1)和NO (0)两个值。不要直接把整形强制转换为BOol 型。常见的错误发生在把数组大小,指针的值或者逻辑位运算的结果赋值到BOol型中,而这样就导致BOol 值的仅取决于之前整形值的最后一个字节,有可能出现整形值不为0但被转为NO的情况。应此把整形转为BOol型的时候请使用ternery *** 作符,保证返回YES 或NO 值。
在BOol,_BOol 以及bool (见C++ Std 4.7.4,4.12以及C99 Std 6.3.1.2)之间可以安全的交换值或转型。但BOol 和Boolean 之间不可,所以对待Boolean 就像上面讲的整形一样就可以了。在Objective-C函数签名里仅使用BOol 。
对BOol值使用逻辑运算(&&,||,! )都是有效的,返回值也可以安全的转为BOol型而不需要ternery *** 作符。
以上是内存溢出为你收集整理的Google的Objective-C编码规范全部内容,希望文章能够帮你解决Google的Objective-C编码规范所遇到的程序开发问题。
如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。
欢迎分享,转载请注明来源:内存溢出
微信扫一扫
支付宝扫一扫
评论列表(0条)