Objective-C 是 C 的动态、面向对象的扩展。它旨在易于使用和阅读,同时支持复杂面向对象的设计。它是 Apple 平台上应用程序的主要开发语言之一。
Apple 已经编写了一份非常优秀且被广泛接受的 Cocoa 编码规范,请在阅读本文档之外,也阅读它。
本文档旨在描述 Objective-C (和 Objective-C++) 的编码规范和实践。这些指南经过了时间的推移,并在其他项目和团队中得到了验证。Google 开发的开源项目符合本指南的要求。
请注意,本指南不是 Objective-C 教程。我们假设读者熟悉该语言。如果您是 Objective-C 新手或需要复习,请阅读 Objective-C 编程。
代码库通常具有较长的生命周期,并且阅读代码的时间多于编写代码的时间。我们明确选择为我们的普通软件工程师在我们的代码库中阅读、维护和调试代码的体验进行优化,而不是为编写代码的容易程度进行优化。例如,如果一小段代码中发生了一些令人惊讶或不寻常的事情,为读者留下文字提示是有价值的。
当风格指南允许多个选项时,最好选择一个选项,而不是混合使用多个选项。在整个代码库中始终如一地使用一种风格,可以让工程师专注于其他(更重要的)问题。一致性还可以实现更好的自动化,因为一致的代码允许更有效地开发和操作格式化或重构代码的工具。在许多情况下,归因于“保持一致”的规则可以归结为“只需选择一个并停止担心它”;允许在这些点上具有灵活性的潜在价值,会被人们争论它们的成本所抵消。
与 Apple SDK 使用 Objective-C 的方式保持一致,其价值与我们代码库中的一致性相同。如果 Objective-C 功能解决了问题,那么这是一个使用它的理由。但是,有时语言特性和习惯用法是有缺陷的,或者只是在并非普遍适用的假设下设计的。在这些情况下,约束或禁止语言特性或习惯用法是合适的。
一个风格规则的好处必须足够大,才能证明要求工程师记住它是合理的。衡量好处的标准是相对于我们没有该规则的代码库而言的,因此,即使人们不太可能做,针对非常有害的做法的规则仍然可能只有很小的好处。这一原则主要解释了我们没有的规则,而不是我们有的规则:例如,goto 违反了以下许多原则,但由于其极端罕见性而未被讨论。
他们说一个例子胜过千言万语,所以让我们从一个例子开始,这个例子应该能让你感受到风格、间距、命名等等。
这是一个示例头文件,演示了 @interface
声明的正确注释和间距。
// GOOD:
#import <Foundation/Foundation.h>
@class Bar;
/**
* A sample class demonstrating good Objective-C style. All interfaces,
* categories, and protocols (read: all non-trivial top-level declarations
* in a header) MUST be commented. Comments must also be adjacent to the
* object they're documenting.
*/
@interface Foo : NSObject
/** The retained Bar. */
@property(nonatomic) Bar *bar;
/** The current drawing attributes. */
@property(nonatomic, copy) NSDictionary<NSString *, NSNumber *> *attributes;
/**
* Convenience creation method.
* See -initWithBar: for details about @c bar.
*
* @param bar The string for fooing.
* @return An instance of Foo.
*/
+ (instancetype)fooWithBar:(Bar *)bar;
/**
* Initializes and returns a Foo object using the provided Bar instance.
*
* @param bar A string that represents a thing that does a thing.
*/
- (instancetype)initWithBar:(Bar *)bar NS_DESIGNATED_INITIALIZER;
/**
* Does some work with @c blah.
*
* @param blah
* @return YES if the work was completed; NO otherwise.
*/
- (BOOL)doWorkWithBlah:(NSString *)blah;
@end
一个示例源文件,演示了接口的 @implementation
的正确注释和间距。
// GOOD:
#import "Shared/Util/Foo.h"
@implementation Foo {
/** The string used for displaying "hi". */
NSString *_string;
}
+ (instancetype)fooWithBar:(Bar *)bar {
return [[self alloc] initWithBar:bar];
}
- (instancetype)init {
// Classes with a custom designated initializer should always override
// the superclass's designated initializer.
return [self initWithBar:nil];
}
- (instancetype)initWithBar:(Bar *)bar {
self = [super init];
if (self) {
_bar = [bar copy];
_string = [[NSString alloc] initWithFormat:@"hi %d", 3];
_attributes = @{
@"color" : UIColor.blueColor,
@"hidden" : @NO
};
}
return self;
}
- (BOOL)doWorkWithBlah:(NSString *)blah {
// Work should be done here.
return NO;
}
@end
名称应尽可能具有描述性,在合理的范围内。遵循标准的 Objective-C 命名规则。
避免非标准缩写(包括非标准首字母缩略词和词首字母缩略词)。不要担心节省水平空间,因为让新读者立即理解您的代码更为重要。例如
// GOOD:
// Good names.
int numberOfErrors = 0;
int completedConnectionsCount = 0;
tickets = [[NSMutableArray alloc] init];
userInfo = [someObject object];
port = [network port];
NSDate *gAppLaunchDate;
// AVOID:
// Names to avoid.
int w;
int nerr;
int nCompConns;
tix = [[NSMutableArray alloc] init];
obj = [someObject object];
p = [network port];
任何类、类别、方法、函数或变量名称都应使用全大写字母表示名称内的首字母缩略词和词首字母缩略词(包括在名称的开头)。这遵循 Apple 在名称中使用全大写字母表示 URL、ID、TIFF 和 EXIF 等首字母缩略词的标准。
C 函数和 typedef 的名称应大写,并根据周围的代码使用驼峰式大小写。
在所有代码中,包括命名和注释,使用包容性语言,避免其他程序员可能会觉得不尊重或冒犯的术语(例如“master”和“slave”、“blacklist”和“whitelist”或“redline”),即使这些术语也具有表面上中性的含义。同样,使用性别中立的语言,除非您指的是特定的人(并使用他们的代词)。例如,对未指定性别的人使用“他们”/“她们”/“它们的”(即使是单数),对非人使用“它”/“它的”。
文件名应反映它们包含的类实现的名称,包括大小写。
遵循您的项目使用的约定。
文件扩展名应如下所示
扩展名 | 类型 |
---|---|
.h | C/C++/Objective-C 头文件 |
.m | Objective-C 实现文件 |
.mm | Objective-C++ 实现文件 |
.cc | 纯 C++ 实现文件 |
.c | C 实现文件 |
包含可能在项目之间共享或在大型项目中使用的代码的文件应具有清晰唯一的名称,通常包括项目或类前缀。
类别的文件名应包括被扩展的类的名称,如 GTMNSString+Utils.h 或 NSTextView+GTMAutocomplete.h
通常需要在 Objective-C 中使用前缀以避免全局命名空间中的命名冲突。类、协议、全局函数和全局常量通常应使用以前缀命名的,该前缀以大写字母开头,后跟一个或多个大写字母或数字。
警告:Apple 保留两个字母的前缀——参见Objective-C 编程中的约定——因此,至少包含三个字符的前缀被认为是最佳实践。
// GOOD:
/** An example error domain. */
GTM_EXTERN NSString *GTMExampleErrorDomain;
/** Gets the default time zone. */
GTM_EXTERN NSTimeZone *GTMGetDefaultTimeZone(void);
/** An example delegate. */
@protocol GTMExampleDelegate <NSObject>
@end
/** An example class. */
@interface GTMExample : NSObject
@end
类名(以及类别和协议名称)应以大写字母开头,并使用混合大小写来分隔单词。
在多个应用程序之间共享的代码中的类和协议必须具有适当的前缀(例如 GTMSendMessage)。对于其他类和协议,建议使用前缀,但不是必需的。
类别名称应以适当的前缀开头,以标识类别是项目的一部分还是可供通用使用。
类别源文件名应以被扩展的类名开头,后跟一个加号和类别名称,例如 NSString+GTMParsing.h
。为了防止 Objective-C 全局命名空间中的冲突,类别中的方法应以类别名称的前缀的小写版本开头,后跟一个下划线(例如,gtm_myCategoryMethodOnAString:
)。
类名和类别的左括号之间应该有一个空格。
// GOOD:
// UIViewController+GTMCrashReporting.h
/** A category that adds metadata to include in crash reports to UIViewController. */
@interface UIViewController (GTMCrashReporting)
/** A unique identifier to represent the view controller in crash reports. */
@property(nonatomic, setter=gtm_setUniqueIdentifier:) int gtm_uniqueIdentifier;
/** Returns an encoded representation of the view controller's current state. */
- (nullable NSData *)gtm_encodedState;
@end
如果一个类不与其他项目共享,则扩展它的类别可以省略名称前缀和方法名称前缀。
// GOOD:
/** This category extends a class that is not shared with other projects. */
@interface XYZDataObject (Storage)
- (NSString *)storageIdentifier;
@end
方法和参数名称通常以小写字母开头,然后使用混合大小写。
应尊重正确的大小写,包括在名称的开头。
// GOOD:
+ (NSURL *)URLWithString:(NSString *)URLString;
如果可能,方法名称应该像一个句子一样阅读,这意味着您应该选择与方法名称一起使用的参数名称。Objective-C 方法名称往往很长,但这样做的好处是,代码块几乎可以像散文一样阅读,从而不需要许多实现注释。
仅在必要时在第二个和后面的参数名称中使用诸如“with”、“from”和“to”之类的介词和连词,以阐明方法的含义或行为。
// GOOD:
- (void)addTarget:(id)target action:(SEL)action; // GOOD; no conjunction needed
- (CGPoint)convertPoint:(CGPoint)point fromView:(UIView *)view; // GOOD; conjunction clarifies parameter
- (void)replaceCharactersInRange:(NSRange)aRange
withAttributedString:(NSAttributedString *)attributedString; // GOOD.
如果方法返回接收器的属性,则以该属性命名该方法。
// GOOD:
/** Returns this instance's sandwich. */
- (Sandwich *)sandwich; // GOOD.
- (CGFloat)height; // GOOD.
// GOOD; Returned value is not an attribute.
- (UIBackgroundTaskIdentifier)beginBackgroundTask;
// AVOID:
- (CGFloat)calculateHeight; // AVOID.
- (id)theDelegate; // AVOID.
访问器方法应该与它正在获取的对象同名,但不应该以单词 get
作为前缀。例如
// GOOD:
- (id)delegate; // GOOD.
// AVOID:
- (id)getDelegate; // AVOID.
返回布尔形容词值的访问器的方法名称以 is
开头,但这些方法的属性名称省略 is
。
点表示法仅用于属性名称,而不用于方法名称。
// GOOD:
@property(nonatomic, getter=isGlorious) BOOL glorious;
// The method for the getter of the property above is:
// - (BOOL)isGlorious;
BOOL isGood = object.glorious; // GOOD.
BOOL isGood = [object isGlorious]; // GOOD.
// AVOID:
BOOL isGood = object.isGlorious; // AVOID.
// GOOD:
NSArray<Frog *> *frogs = [NSArray<Frog *> arrayWithObject:frog];
NSEnumerator *enumerator = [frogs reverseObjectEnumerator]; // GOOD.
// AVOID:
NSEnumerator *enumerator = frogs.reverseObjectEnumerator; // AVOID.
有关 Objective-C 命名的更多详细信息,请参见 Apple 的方法命名指南。
这些准则仅适用于 Objective-C 方法。 C++ 方法名称继续遵循 C++ 风格指南中设置的规则。
函数名称应以大写字母开头,并且每个新单词都应使用大写字母(又称“驼峰式大小写”或“帕斯卡大小写”)。
// GOOD:
static void AddTableEntry(NSString *tableEntry);
static BOOL DeleteFile(const char *filename);
由于 Objective-C 不提供命名空间,因此非静态函数应具有前缀,以最大程度地减少名称冲突的机会。
// GOOD:
GTM_EXTERN NSTimeZone *GTMGetDefaultTimeZone(void);
GTM_EXTERN NSString *GTMGetURLScheme(NSURL *URL);
变量名称通常以小写字母开头,并使用混合大小写来分隔单词。
实例变量具有前导下划线。文件作用域或全局变量具有前缀 g
。例如:myLocalVariable
、_myInstanceVariable
、gMyGlobalVariable
。
读者应该能够从名称中推断出变量类型,但不要对语法属性使用匈牙利表示法,例如变量的静态类型(int 或指针)。
在方法或函数范围之外声明的文件作用域或全局变量(与常量相反)应该很少见,并且应该具有前缀 g
。
// GOOD:
static int gGlobalCounter;
实例变量名称是混合大小写,应该以下划线作为前缀,如 _usernameTextField
。
注意:Google 以前的 Objective-C ivar 约定是尾随下划线。现有项目可以选择在新代码中继续使用尾随下划线,以便在项目代码库中保持一致性。前缀或后缀下划线的一致性应在每个类中保持。
常量符号(const 全局变量和静态变量以及使用 #define 创建的常量)应使用混合大小写来分隔单词。
全局和文件作用域常量应具有适当的前缀。
// GOOD:
/** The domain for GTL service errors. */
GTL_EXTERN NSString *const GTLServiceErrorDomain;
/** An enumeration of GTL service error codes. */
typedef NS_ENUM(int32_t, GTLServiceError) {
/** An error code indicating that a query result was missing. */
GTLServiceErrorQueryResultMissing = -3000,
/** An error code indicating that the query timed out. */
GTLServiceErrorQueryTimedOut = -3001,
};
由于 Objective-C 不提供命名空间,因此具有外部链接的常量应具有前缀,以最大程度地减少名称冲突的机会,通常如 ClassNameConstantName
或 ClassNameEnumName
。
为了与 Swift 代码互操作,枚举值应具有扩展 typedef 名称的名称
// GOOD:
/** An enumeration of supported display tinges. */
typedef NS_ENUM(int32_t, DisplayTinge) {
DisplayTingeGreen = 1,
DisplayTingeBlue = 2,
};
小写 k 可用作在实现文件中声明的静态存储持续时间的常量的独立前缀
// GOOD:
static const int kFileCount = 12;
static NSString *const kUserKey = @"kUserKey";
注意:以前的约定是公共常量名称以小写 k 开头,后跟项目特定的前缀。不再推荐这种做法。
正如示例中所示,建议在@interface
声明中使用以下声明顺序:属性、类方法、初始化方法,最后是实例方法。类方法部分应以任何便捷构造器开始。
在尽可能小的实际作用域内声明变量,并靠近它们的使用位置。在声明中初始化变量。
// GOOD:
CLLocation *location = [self lastKnownLocation];
for (int meters = 1; meters < 10; meters++) {
reportFrogsWithinRadius(location, meters);
}
有时,为了效率,将变量声明在它的使用范围之外可能更合适。以下示例将 meters 的声明与初始化分开,并且每次循环都毫无必要地发送 lastKnownLocation 消息。
// AVOID:
int meters; // AVOID.
for (meters = 1; meters < 10; meters++) {
CLLocation *location = [self lastKnownLocation]; // AVOID.
reportFrogsWithinRadius(location, meters);
}
在自动引用计数下,指向 Objective-C 对象的强指针和弱指针会自动初始化为 nil
,因此在这些常见情况下,不需要显式地初始化为 nil
。但是,自动初始化不适用于许多 Objective-C 指针类型,包括使用 __unsafe_unretained
所有权限定符声明的对象指针和 CoreFoundation 对象指针类型。如有疑问,最好初始化所有 Objective-C 局部变量。
当实现文件中的文件作用域变量/常量声明不需要在该文件之外被引用时,将它们声明为 static(或在 Objective-C++ 中使用匿名命名空间)。不要在 .h 文件中使用静态存储持续时间(或在匿名命名空间中)声明文件作用域变量或常量。
// GOOD:
// file: Foo.m
static const int FOORequestLimit = 5;
// AVOID:
// file: Foo.h
static const int FOORequestLimit = 5; // AVOID.
除了匹配系统接口使用的类型外,应避免使用无符号整数。
当使用无符号整数进行数学运算或倒数到零时,会出现细微的错误。除了在系统接口中匹配 NSUInteger 之外,在数学表达式中仅依赖有符号整数。
// GOOD:
NSUInteger numberOfObjects = array.count;
for (NSInteger counter = numberOfObjects - 1; counter >= 0; --counter)
// AVOID:
for (NSUInteger counter = numberOfObjects - 1; counter >= 0; --counter) // AVOID.
无符号整数可以用于标志和位掩码,尽管通常 NS_OPTIONS 或 NS_ENUM 会更合适。
请注意,long、NSInteger、NSUInteger 和 CGFloat 类型的大小在 32 位和 64 位构建中有所不同。当匹配系统接口时,可以使用它们,但在处理需要精确大小的 API(例如 proto API)时应避免使用它们。
// GOOD:
int32_t scalar1 = proto.intValue;
int64_t scalar2 = proto.longValue;
NSUInteger numberOfObjects = array.count;
CGFloat offset = view.bounds.origin.x;
// AVOID:
NSInteger scalar2 = proto.longValue; // AVOID.
文件和缓冲区大小通常超过 32 位限制,因此应使用 int64_t
声明它们,而不是使用 long
、NSInteger
或 NSUInteger
。
在定义 CGFloat
常量时,请记住以下几点。
以前,对于目标为 32 位平台的项目,可能需要使用 float
字面量(带有 f
后缀的数字)以避免类型转换警告。
由于所有 Google iOS 项目现在仅以 64 位运行时为目标,因此 CGFloat
常量可以省略后缀(使用 double
值)。但是,团队可以选择继续使用 float
数字以保持遗留代码的一致性,直到它们最终迁移到所有地方都使用 double
值。避免在同一代码中混合使用 float
和 double
值。
// GOOD:
// Good since CGFloat is double
static const CGFloat kHorizontalMargin = 8.0;
static const CGFloat kVerticalMargin = 12.0;
// This is OK as long as all values for CGFloat constants in your project are float
static const CGFloat kHorizontalMargin = 8.0f;
static const CGFloat kVerticalMargin = 12.0f;
// AVOID:
// Avoid a mixture of float and double constants
static const CGFloat kHorizontalMargin = 8.0f;
static const CGFloat kVerticalMargin = 12.0;
注释对于保持我们的代码可读性至关重要。以下规则描述了您应该注释什么以及在哪里注释。但请记住:虽然注释很重要,但最好的代码是自描述的。为类型和变量赋予合理的名称远比使用晦涩的名称然后尝试通过注释来解释它们要好得多。
注意标点符号、拼写和语法;阅读书写良好的注释比阅读书写糟糕的注释更容易。
注释应像叙述性文本一样可读,具有适当的大写和标点符号。在许多情况下,完整的句子比句子片段更具可读性。较短的注释(例如,代码行末尾的注释)有时可以不太正式,但请使用一致的风格。
在编写注释时,要为您的受众编写:下一个需要理解您的代码的贡献者。慷慨一点——下一个可能就是你!
文件可以选择以对其内容的描述开始。
每个文件可以按顺序包含以下项目
如果您对包含作者行的文件进行了重大更改,请考虑删除作者行,因为修订历史记录已经提供了更详细和准确的作者记录。
每个重要的接口,无论是公共接口还是私有接口,都应该附带描述其目的以及它如何适应整个蓝图的注释。
注释应该用于记录类、属性、成员变量、函数、类别、协议声明和枚举。
// GOOD:
/**
* A delegate for NSApplication to handle notifications about app
* launch and shutdown. Owned by the main app controller.
*/
@interface MyAppDelegate : NSObject {
/**
* The background task in progress, if any. This is initialized
* to the value UIBackgroundTaskInvalid.
*/
UIBackgroundTaskIdentifier _backgroundTaskID;
}
/** The factory that creates and manages fetchers for the app. */
@property(nonatomic) GTMSessionFetcherService *fetcherService;
@end
鼓励对接口使用 Doxygen 风格的注释,因为它们会被 Xcode 解析以显示格式化的文档。有各种各样的 Doxygen 命令;在项目中一致地使用它们。
如果您已经在文件顶部的注释中详细描述了一个接口,请随意简单地声明“有关完整描述,请参阅文件顶部的注释”,但请确保有某种注释。
此外,每个方法都应该有一个注释来解释它的功能、参数、返回值、线程或队列假设以及任何副作用。对于公共方法,文档注释应该在头文件中,或者对于重要的私有方法,应该紧接在该方法之前。
对于方法和函数注释,请使用描述性形式(“打开文件”),而不是祈使形式(“打开文件”)。注释描述的是函数,而不是告诉函数该做什么。
记录类、属性或方法所做的线程使用假设(如果有)。如果该类的实例可以被多个线程访问,请格外小心地记录围绕多线程使用的规则和不变量。
属性和成员变量的任何哨兵值,例如 NULL
或 -1
,都应该在注释中记录。
声明注释解释了方法或函数是如何使用的。解释方法或函数如何实现的注释应该位于实现中,而不是声明中。
如果注释不会传达超出方法名称之外的任何额外信息,则可以省略测试用例类和测试方法上的声明注释。测试或特定于测试的类(例如 helper)中的实用程序方法应该被注释。
提供注释来解释代码中棘手、微妙或复杂的部分。
// GOOD:
// Set the property to nil before invoking the completion handler to
// avoid the risk of reentrancy leading to the callback being
// invoked again.
CompletionHandler handler = self.completionHandler;
self.completionHandler = nil;
handler();
在有用时,还可以提供有关已考虑或放弃的实现方法的注释。
行尾注释应与代码至少间隔 2 个空格。如果您在后续行上有多个注释,那么将它们对齐通常会更具可读性。
// GOOD:
[self doSomethingWithALongName]; // Two spaces before the comment.
[self doSomethingShort]; // More spacing to align the comment.
在需要避免歧义的地方,使用反引号或竖线来引用注释中的变量名和符号,而不是使用引号或内联命名符号。
在 Doxygen 风格的注释中,更喜欢使用等宽文本命令(例如 @c
)来标记符号。
当符号是一个常用词,可能使句子读起来像写得很差时,标记有助于提供清晰度。一个常见的例子是符号 count
// GOOD:
// Sometimes `count` will be less than zero.
或者在引用已经包含引号的内容时
// GOOD:
// Remember to call `StringWithoutSpaces("foo bar baz")`
当符号本身很明显时,不需要反引号或竖线。
// GOOD:
// This class serves as a delegate to GTMDepthCharge.
Doxygen 格式也适用于识别符号。
// GOOD:
/** @param maximum The highest value for @c count. */
对于不由 ARC 管理的对象,当指针所有权模型超出最常见的 Objective-C 用法习惯时,使其尽可能明确。
NSObject 派生对象的实例变量假定被保留;如果它们没有被保留,则应该注释为 weak 或使用 __weak
生命周期限定符声明。
一个例外是 Mac 软件中标记为 @IBOutlets
的实例变量,它们被假定为不被保留。
当实例变量是指向 Core Foundation、C++ 和其他非 Objective-C 对象的指针时,它们应该始终使用 strong 和 weak 注释来表明哪些指针被保留,哪些指针不被保留。即使在为自动引用计数构建时,Core Foundation 和其他非 Objective-C 对象指针也需要显式的内存管理。
强声明和弱声明的示例
// GOOD:
@interface MyDelegate : NSObject
@property(nonatomic) NSString *doohickey;
@property(nonatomic, weak) NSString *parent;
@end
@implementation MyDelegate {
IBOutlet NSButton *_okButton; // Normal NSControl; implicitly weak on Mac only
AnObjcObject *_doohickey; // My doohickey
__weak MyObjcParent *_parent; // To send messages back (owns this instance)
// non-NSObject pointers...
CWackyCPPClass *_wacky; // Strong, some cross-platform object
CFDictionaryRef *_dict; // Strong
}
@end
使用 ARC 时,对象所有权和生存期是显式的,因此对于自动保留的对象,不需要额外的注释。
避免使用宏,尤其是在可以使用 const
变量、枚举、Xcode 代码段或 C 函数的地方。
宏使您看到的代码与编译器看到的代码不同。现代 C 使得传统上使用宏来表示常量和实用程序函数变得不必要。只有在没有其他解决方案可用时才应使用宏。
在需要宏的地方,使用唯一的名称以避免编译单元中出现符号冲突的风险。如果可行,在使用后通过 #undefining
宏来限制作用域。
宏名称应使用 SHOUTY_SNAKE_CASE
—所有大写字母,单词之间用下划线分隔。类似函数的宏可以使用 C 函数命名约定。不要定义看起来像是 C 或 Objective-C 关键字的宏。
// GOOD:
#define GTM_EXPERIMENTAL_BUILD ... // GOOD
// Assert unless X > Y
#define GTM_ASSERT_GT(X, Y) ... // GOOD, macro style.
// Assert unless X > Y
#define GTMAssertGreaterThan(X, Y) ... // GOOD, function style.
// AVOID:
#define kIsExperimentalBuild ... // AVOID
#define unless(X) if(!(X)) // AVOID
避免扩展为不平衡的 C 或 Objective-C 结构的宏。避免引入作用域或可能模糊块中值捕获的宏。
避免在头文件中生成类、属性或方法定义的宏,以用作公共 API。这些只会使代码难以理解,并且该语言已经有更好的方法来做到这一点。
避免生成方法实现或生成稍后在宏外部使用的变量声明的宏。宏不应通过隐藏变量的声明位置和方式来使代码难以理解。
// AVOID:
#define ARRAY_ADDER(CLASS) \
-(void)add ## CLASS ## :(CLASS *)obj toArray:(NSMutableArray *)array
ARRAY_ADDER(NSString) {
if (array.count > 5) { // AVOID -- where is 'array' defined?
...
}
}
可接受的宏使用的例子包括断言和调试日志宏,这些宏是基于构建设置有条件地编译的——通常,这些宏不会被编译到发布版本中。
除非另有说明,否则不得使用 C/Objective-C 的非标准扩展。
编译器支持各种不属于标准 C 的扩展。示例包括复合语句表达式(例如 foo = ({ int x; Bar(&x); x })
)。
__typeof__
关键字在类型不能帮助读者理解的情况下,允许使用 __typeof__
关键字。 鼓励使用 __typeof__
关键字,而不是其他类似的关键字(例如,typeof
关键字),因为它在所有语言变体中都受支持。
// GOOD:
__weak __typeof__(self) weakSelf = self;
// AVOID:
__typeof__(data) copiedData = [data copy]; // AVOID.
__weak typeof(self) weakSelf = self; // AVOID.
__auto_type
关键字和类型推断仅对于块和函数指针类型的局部变量,才允许使用 __auto_type
关键字进行类型推断。 如果块或指针类型已经存在 typedef,请避免类型推断。
// GOOD:
__auto_type block = ^(NSString *arg1, int arg2) { ... };
__auto_type functionPointer = &MyFunction;
typedef void(^SignInCallback)(Identity *, NSError *);
SignInCallback signInCallback = ^(Identity *identity, NSError *error) { ... };
// AVOID:
__auto_type button = [self createButtonForInfo:info];
__auto_type viewController = [[MyCustomViewControllerClass alloc] initWith...];
typedef void(^SignInCallback)(Identity *, NSError *);
__auto_type signInCallback = ^(Identity *identity, NSError *error) { ... };
__attribute__
关键字在 Apple API 声明中使用,因此已批准使用。A ?: B
已获批准。清楚地标识您的指定初始化器。
对于子类化来说,类清楚地标识其指定初始化器非常重要。 这允许子类重写初始化器的子集以初始化子类状态或调用子类提供的新指定初始化器。 清楚地标识指定初始化器也使跟踪和调试初始化代码更加容易。
首选通过使用指定初始化器属性(例如,NS_DESIGNATED_INITIALIZER
)注释指定初始化器来标识它们。 当指定初始化器属性不可用时,在注释中声明指定初始化器。 除非有令人信服的理由或要求,否则首选单个指定初始化器。
通过重写超类指定初始化器来支持从超类继承的初始化器,以确保所有继承的初始化器都通过子类指定初始化器来定向。 当存在令人信服的理由或要求不应支持继承的初始化器时,可以使用可用性属性(例如,NS_UNAVAILABLE
)来注释初始化器,以阻止使用;但是,请注意,仅可用性属性不能完全防止无效初始化。
编写需要新的指定初始化器的子类时,请确保重写超类的任何指定初始化器。
在类上声明指定初始化器时,请记住,超类上被认为是指定初始化器的任何初始化器都将成为子类的便利初始化器,除非另有声明。 由于使用超类初始化器的无效初始化,未能重写超类指定初始化器可能会导致错误。 为了避免无效初始化,请确保便利初始化器调用指定初始化器。
将 NSObject 的重写方法放在 @implementation
的顶部。
这通常适用于(但不限于)init...
、copyWithZone:
和 dealloc
方法。 init...
方法应分组在一起,包括那些不是 NSObject
重写的 init...
方法,然后是其他典型的 NSObject
方法,例如 description
、isEqual:
和 hash
。
用于创建实例的便捷类工厂方法可以在 NSObject
方法之前。
不要在 init
方法中将实例变量初始化为 0
或 nil
;这样做是多余的。
新分配对象的所有实例变量都初始化为 0
(除了 isa),因此不要通过将变量重新初始化为 0
或 nil
来弄乱 init 方法。
实例变量通常应在实现文件中声明或由属性自动合成。 当 ivar 在头文件中声明时,应标记为 @protected
或 @private
。
// GOOD:
@interface MyClass : NSObject {
@protected
id _myInstanceVariable;
}
@end
不要调用 NSObject
类方法 new
,也不要在子类中重写它。 +new
很少使用,并且与初始化器用法形成鲜明对比。 而是使用 +alloc
和 -init
方法来实例化保留的对象。
保持你的类简单;避免“大杂烩” API。 如果一个方法不需要是公共的,请将其保留在公共接口之外。
与 C++ 不同,Objective-C 不区分公共方法和私有方法; 可以将任何消息发送到对象。 因此,除非类的使用者实际上希望使用它们,否则避免将方法放置在公共 API 中。 这有助于降低在您不期望时调用它们的可能性。 这包括从父类重写的方法。
由于内部方法实际上不是私有的,因此很容易意外地重写超类的“私有”方法,从而导致非常难以解决的错误。 通常,私有方法应具有相当独特的名称,以防止子类无意中重写它们。
#import
Objective-C 和 Objective-C++ 标头,以及 #include
C/C++ 标头。
C/C++ 标头使用 #include
包含其他 C/C++ 标头。 在 C/C++ 标头上使用 #import
会阻止将来使用 #include
进行包含,并可能导致意外的编译行为。
C/C++ 标头应提供自己的 #define
保护。
标头包含的标准顺序是相关标头、操作系统标头、语言库标头,最后是其他依赖项的标头组。
相关标头先于其他标头,以确保它没有隐藏的依赖项。 对于实现文件,相关标头是头文件。 对于测试文件,相关标头是包含被测接口的标头。
用一个空行分隔每个非空包含组。 在每个组中,包含应按字母顺序排序。
使用相对于项目源目录的路径导入标头。
// GOOD:
#import "ProjectX/BazViewController.h"
#import <Foundation/Foundation.h>
#include <unistd.h>
#include <vector>
#include "base/basictypes.h"
#include "base/integral_types.h"
#import "base/mac/FOOComplexNumberSupport"
#include "util/math/mathutil.h"
#import "ProjectX/BazModel.h"
#import "Shared/Util/Foo.h"
为系统框架和系统库导入 Umbrella Headers,而不是包含单个文件。
虽然包含来自 Cocoa 或 Foundation 等框架的单个系统标头似乎很诱人,但实际上,如果您包含顶级根框架,编译器的工作量会更少。 根框架通常是预编译的,可以更快地加载。 此外,请记住对 Objective-C 框架使用 @import
或 #import
而不是 #include
。
// GOOD:
@import UIKit; // GOOD.
#import <Foundation/Foundation.h> // GOOD.
// AVOID:
#import <Foundation/NSArray.h> // AVOID.
#import <Foundation/NSString.h>
...
-dealloc
中向当前对象发送消息初始化器和 -dealloc
中的代码应尽可能避免调用实例方法。
超类初始化在子类初始化之前完成。 在所有类都有机会初始化其实例状态之前,对 self 的任何方法调用都可能导致子类对未初始化的实例状态进行操作。
对于 -dealloc
存在类似的问题,其中方法调用可能导致类对已释放的状态进行操作。
其中一个不太明显的例子是属性访问器。 这些可以像任何其他选择器一样被重写。 在初始值设定项和 -dealloc
中,在实际可行的情况下,直接分配给 ivar 并释放它们,而不是依赖访问器。
// GOOD:
- (instancetype)init {
self = [super init];
if (self) {
_bar = 23; // GOOD.
}
return self;
}
注意将常见的初始化代码分解到辅助方法中
// AVOID:
- (instancetype)init {
self = [super init];
if (self) {
self.bar = 23; // AVOID.
[self sharedMethod]; // AVOID. Fragile to subclassing or future extension.
}
return self;
}
// GOOD:
- (void)dealloc {
[_notifier removeObserver:self]; // GOOD.
}
// AVOID:
- (void)dealloc {
[self removeNotifications]; // AVOID.
}
在某些常见情况下,类可能需要在初始化期间使用超类提供的属性和方法。 这通常发生在从 UIKit 和 AppKit 基类派生的类中,以及其他基类中。 在决定是否对此规则例外时,请使用您的判断力和对常见实践的了解。
代码应避免冗余属性访问。 当属性值预计不会更改并且需要多次使用时,首选将属性值分配给局部变量。
// GOOD:
UIView *view = self.view;
UIScrollView *scrollView = self.scrollView;
[scrollView.leadingAnchor constraintEqualToAnchor:view.leadingAnchor].active = YES;
[scrollView.trailingAnchor constraintEqualToAnchor:view.trailingAnchor].active = YES;
// AVOID:
[self.scrollView.loadingAnchor constraintEqualToAnchor:self.view.loadingAnchor].active = YES;
[self.scrollView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor].active = YES;
当重复引用链式属性调用时,首选在局部变量中捕获重复的表达式
// AVOID:
foo.bar.baz.field1 = 10;
foo.bar.baz.field2 = @"Hello";
foo.bar.baz.field3 = 2.71828183;
// GOOD:
Baz *baz = foo.bar.baz;
baz.field1 = 10;
baz.field2 = @"Hello";
baz.field3 = 2.71828183;
重复访问相同的属性会导致多次消息分派以获取相同的值,并且在 ARC 下需要保留和释放任何返回的对象;编译器无法优化掉这些额外的操作,从而导致执行速度变慢并显着增加二进制文件大小。
对于 包含不可变和可变子类的 Foundation 和其他层次结构,只要遵守不可变的约定,就可以用可变子类代替不可变子类。
这种替换最常见的例子是所有权转移,特别是对于返回值。 在这些情况下,不需要额外的副本,并且返回可变子类更有效。 调用者应该将返回值视为其声明的类型,因此,返回值将被视为不可变的。
// GOOD:
- (NSArray *)listOfThings {
NSMutableArray *generatedList = [NSMutableArray array];
for (NSInteger i = 0; i < _someLimit; i++) {
[generatedList addObject:[self thingForIndex:i]];
}
// Copy not necessary, ownership of generatedList is transferred.
return generatedList;
}
只要所有权转移清晰,此规则也适用于仅存在可变变体的类。 Protos 是一个常见的例子。
// GOOD:
- (SomeProtoMessage *)someMessageForValue:(BOOL)value {
SomeProtoMessage *message = [SomeProtoMessage message];
message.someValue = value;
return message;
}
只要可变参数在方法调用期间不会更改,就不需要创建可变类型的本地不可变副本来匹配正在调用的方法的方法签名。 预期被调用方法将参数视为声明的类型,并采用 防御性副本(Apple 称为“快照”),如果他们打算在调用持续时间之外保留这些参数。
// AVOID:
NSMutableArray *updatedThings = [NSMutableArray array];
[updatedThings addObject:newThing];
[_otherManager updateWithCurrentThings:[updatedThings copy]]; // AVOID
接收和保留集合或其他类型的代码,如果这些类型具有可变变体,应考虑传入的对象可能是可变的,因此应保留不可变或可变的副本,而不是原始对象。 特别是,初始化器和设置器应该复制,而不是保留具有可变变体类型的对象。
合成的访问器应该使用 copy
关键字,以确保生成的代码符合这些预期。
注意:copy
属性关键字仅影响合成的 setter,对 getter 没有影响。 由于属性关键字对直接 ivar 访问没有影响,因此自定义访问器必须实现相同的复制语义。
// GOOD:
@property(nonatomic, copy) NSString *name;
@property(nonatomic, copy) NSSet<FilterThing *> *filters;
- (instancetype)initWithName:(NSString *)name
filters:(NSSet<FilterThing *> *)filters {
self = [super init];
if (self) {
_name = [name copy];
_filters = [filters copy];
}
return self;
}
- (void)setFilters:(NSSet<FilterThing *> *)filters {
// Ensure that we retain an immutable collection.
_filters = [filters copy];
}
同样,getter 必须返回与它们返回的不可变类型的合约期望相匹配的类型。
// GOOD:
@implementation Foo {
NSMutableArray<ContentThing *> *_currentContent;
}
- (NSArray<ContentThing *> *)currentContent {
return [_currentContent copy];
}
所有 Objective-C 协议缓冲区都是可变的,通常应该复制而不是保留 除非存在明确的所有权转移情况。
// GOOD:
- (void)setFooMessage:(FooMessage *)fooMessage {
// Copy proto to ensure no other retainer can mutate our value.
_fooMessage = [fooMessage copy];
}
- (FooMessage *)fooMessage {
// Copy proto to return so that caller cannot mutate our value.
return [_fooMessage copy];
}
异步代码应该在分发之前复制潜在的可变对象。 block 捕获的对象会被保留,但不会被复制。
// GOOD:
- (void)doSomethingWithThings:(NSArray<Thing *> *)things {
NSArray<Thing *> *thingsToWorkOn = [things copy];
dispatch_async(_workQueue, ^{
for (id<Thing> thing in thingsToWorkOn) {
...
}
});
}
注意:没有可变变体的对象,例如 NSURL
、NSNumber
、NSDate
、UIColor
等,无需复制。
所有在 Xcode 7 或更高版本上编译的项目都应使用 Objective-C 轻量级泛型表示法来键入包含的对象。
每个 NSArray
、NSDictionary
或 NSSet
引用都应使用轻量级泛型进行声明,以提高类型安全性并显式记录用法。
// GOOD:
@property(nonatomic, copy) NSArray<Location *> *locations;
@property(nonatomic, copy, readonly) NSSet<NSString *> *identifiers;
NSMutableArray<MyLocation *> *mutableLocations = [otherObject.locations mutableCopy];
如果完全注释的类型变得复杂,请考虑使用 typedef 来保持可读性。
// GOOD:
typedef NSSet<NSDictionary<NSString *, NSDate *> *> TimeZoneMappingSet;
TimeZoneMappingSet *timeZoneMappings = [TimeZoneMappingSet setWithObjects:...];
使用最具描述性的通用超类或协议。 在最一般的情况下,当没有其他信息时,将集合声明为使用 id 显式异构。
// GOOD:
@property(nonatomic, copy) NSArray<id> *unknowns;
不要 @throw
Objective-C 异常,但你应该准备好捕获来自第三方或操作系统调用的异常。
这遵循了 Apple 的 Cocoa 异常编程主题简介 中使用错误对象进行错误传递的建议。
我们使用 -fobjc-exceptions
进行编译(主要是为了获得 @synchronized
),但我们不 @throw
。 在需要正确使用第三方代码或库时,允许使用 @try
、@catch
和 @finally
。 如果你使用它们,请准确记录你期望哪些方法抛出异常。
nil
检查避免仅为了防止向 nil
发送消息而存在的 nil
指针检查。 向 nil
发送消息可靠地返回 nil
作为指针、零作为整数或浮点值、初始化为 0
的结构体,以及等于 {0, 0}
的 _Complex
值。
// AVOID:
if (dataSource) { // AVOID.
[dataSource moveItemAtIndex:1 toIndex:0];
}
// GOOD:
[dataSource moveItemAtIndex:1 toIndex:0]; // GOOD.
请注意,这适用于 nil
作为消息目标,而不是作为参数值。 单个方法可能安全地处理 nil
参数值,也可能不安全。
还要注意,这与针对 NULL
检查 C/C++ 指针和 block 指针不同,运行时不处理这种情况,并且会导致你的应用程序崩溃。 你仍然需要确保你不会取消引用 NULL
指针。
可以使用可空性注释来装饰接口,以描述接口应如何使用以及它的行为方式。 接受使用可空性区域(例如,NS_ASSUME_NONNULL_BEGIN
和 NS_ASSUME_NONNULL_END
)和显式可空性注释。 与 __nullable
和 __nonnull
关键字相比,更喜欢使用 _Nullable
和 _Nonnull
关键字。 对于 Objective-C 方法和属性,更喜欢使用上下文相关的非下划线关键字,例如 nonnull
和 nullable
。
// GOOD:
/** A class representing an owned book. */
@interface GTMBook : NSObject
/** The title of the book. */
@property(nonatomic, readonly, copy, nonnull) NSString *title;
/** The author of the book, if one exists. */
@property(nonatomic, readonly, copy, nullable) NSString *author;
/** The owner of the book. Setting nil resets to the default owner. */
@property(nonatomic, copy, null_resettable) NSString *owner;
/** Initializes a book with a title and an optional author. */
- (nonnull instancetype)initWithTitle:(nonnull NSString *)title
author:(nullable NSString *)author
NS_DESIGNATED_INITIALIZER;
/** Returns nil because a book is expected to have a title. */
- (nullable instancetype)init;
@end
/** Loads books from the file specified by the given path. */
NSArray<GTMBook *> *_Nullable GTMLoadBooksFromFile(NSString *_Nonnull path);
// AVOID:
NSArray<GTMBook *> *__nullable GTMLoadBooksFromTitle(NSString *__nonnull path);
不要基于 nonnull 限定符假定指针不是 null,因为编译器仅检查此类情况的子集,并且不保证指针不是 null。 避免故意违反函数、方法和属性声明的可空性语义。
将一般整数值转换为 BOOL
时要小心。 避免直接与 YES
进行比较,或者使用比较运算符比较多个 BOOL
值。
BOOL
在某些 Apple 平台(特别是 Intel macOS、watchOS 和 32 位 iOS)上定义为有符号 char
,因此它可能具有除 YES
(1
) 和 NO
(0
) 之外的值。 不要将常规整数值直接转换为 BOOL
。
常见的错误包括将数组的大小、指针值或按位逻辑运算的结果转换为 BOOL
。 这些操作可能取决于整数值的最后一个字节的值,并导致意外的 NO
值。 使用 NS_OPTIONS 值和标志掩码的操作是特别常见的错误。
将常规整数值转换为 BOOL
时,使用条件运算符返回 YES
或 NO
值。
你可以安全地互换和转换 BOOL
、_Bool
和 bool
(请参阅 C++ Std 4.7.4、4.12 和 C99 Std 6.3.1.2)。 在 Objective-C 方法签名中使用 BOOL
。
使用逻辑运算符 (&&
, ||
和 !
) 与 BOOL
也是有效的,并且将返回可以安全转换为 BOOL
的值,而无需条件运算符。
// AVOID:
- (BOOL)isBold {
return [self fontTraits] & NSFontBoldTrait; // AVOID.
}
- (BOOL)isValid {
return [self stringValue]; // AVOID.
}
- (BOOL)isLongEnough {
return (BOOL)([self stringValue].count); // AVOID.
}
// GOOD:
- (BOOL)isBold {
return ([self fontTraits] & NSFontBoldTrait) ? YES : NO;
}
- (BOOL)isValid {
return [self stringValue] != nil;
}
- (BOOL)isLongEnough {
return [self stringValue].count > 0;
}
- (BOOL)isEnabled {
return [self isValid] && [self isBold];
}
不要直接将 BOOL
变量与 YES
进行比较。 对于那些精通 C 语言的人来说,这不仅更难阅读,而且上述第一点表明返回值可能并不总是你期望的。
// AVOID:
BOOL great = [foo isGreat];
if (great == YES) { // AVOID.
// ...be great!
}
// GOOD:
BOOL great = [foo isGreat];
if (great) { // GOOD.
// ...be great!
}
不要使用比较运算符直接比较 BOOL
值。 为 true 的 BOOL
值可能不相等。 使用逻辑运算符代替 BOOL
值的按位比较。
// AVOID:
if (oldBOOLValue != newBOOLValue) { // AVOID.
// ... code that should only run when the value changes.
}
// GOOD:
if ((!oldBoolValue && newBoolValue) || (oldBoolValue && !newBoolValue)) { // GOOD.
// ... code that should only run when the value changes.
}
// GOOD, the results of logical operators on BOOLs are safe to compare.
if (!oldBoolValue != !newBoolValue) {
// ... code that should only run when the value changes.
}
BOOL NSNumber 字面量 是 @YES
和 @NO
,它们等价于 [NSNumber numberWithBool:...]
。
避免使用装箱表达式来创建 BOOL 值,包括像 @(YES)
这样的简单表达式。 装箱表达式与[其他 BOOL 表达式存在相同的陷阱](#BOOL_Expressions_Conversions),因为装箱常规整数值可以产生不等于 @YES
和 @NO
的 true 或 false NSNumbers
。
将常规整数值转换为 BOOL 字面量时,使用条件运算符转换为 @YES
或 @NO
。 不要将条件运算符嵌入到装箱表达式中,因为这等效于装箱常规整数值,即使操作的结果是 BOOL。
// AVOID:
[_boolArray addValue:@(YES)]; // AVOID boxing even in simple cases.
NSNumber *isBold = @(self.fontTraits & NSFontBoldTrait); // AVOID.
NSNumber *hasContent = @([self stringValue].length); // AVOID.
NSNumber *isValid = @([self stringValue]); // AVOID.
NSNumber *isStringNotNil = @([self stringValue] ? YES : NO); // AVOID.
// GOOD:
[_boolArray addValue:@YES]; // GOOD.
NSNumber *isBold = self.fontTraits & NSFontBoldTrait ? @YES : @NO; // GOOD.
NSNumber *hasContent = [self stringValue].length ? @YES : @NO; // GOOD.
NSNumber *isValid = [self stringValue] ? @YES : @NO; // GOOD.
NSNumber *isStringNotNil = [self stringValue] ? @YES : @NO; // GOOD.
在没有任何实例变量声明的接口、类扩展和实现中,省略空的大括号集。
// GOOD:
@interface MyClass : NSObject
// Does a lot of stuff.
- (void)fooBarBam;
@end
@interface MyClass ()
- (void)classExtensionMethod;
@end
@implementation MyClass
// Actual implementation.
@end
// AVOID:
@interface MyClass : NSObject {
}
// Does a lot of stuff.
- (void)fooBarBam;
@end
@interface MyClass () {
}
- (void)classExtensionMethod;
@end
@implementation MyClass {
}
// Actual implementation.
@end
当委托、目标对象和 block 指针会创建保留环时,不应保留它们。
为了避免导致保留环,一旦明确不再需要向对象发送消息,就应立即释放委托或目标指针。
如果不存在明确的时间点不再需要委托或目标指针,则该指针应仅被弱引用。
Block 指针无法被弱引用。 为了避免在客户端代码中引起保留环,block 指针应仅用于在调用后或不再需要时可以显式释放的回调。 否则,回调应通过弱委托或目标指针完成。
在 Objective-C++ 源文件中,遵循你正在实现的函数或方法的语言的样式。 为了最大限度地减少混合 Cocoa/Objective-C 和 C++ 时不同命名样式之间的冲突,请遵循正在实现的方法的样式。
对于 @implementation
代码块中的代码,使用 Objective-C 命名规则。 对于 C++ 类的方法中的代码,使用 C++ 命名规则。
对于类实现之外的 Objective-C++ 文件中的代码,请在文件中保持一致。
// GOOD:
// file: cross_platform_header.h
class CrossPlatformAPI {
public:
...
int DoSomethingPlatformSpecific(); // impl on each platform
private:
int an_instance_var_;
};
// file: mac_implementation.mm
#include "cross_platform_header.h"
/** A typical Objective-C class, using Objective-C naming. */
@interface MyDelegate : NSObject {
@private
int _instanceVar;
CrossPlatformAPI* _backEndObject;
}
- (void)respondToSomething:(id)something;
@end
@implementation MyDelegate
- (void)respondToSomething:(id)something {
// bridge from Cocoa through our C++ backend
_instanceVar = _backEndObject->DoSomethingPlatformSpecific();
NSString* tempString = [NSString stringWithFormat:@"%d", _instanceVar];
NSLog(@"%@", tempString);
}
@end
/** The platform-specific implementation of the C++ class, using C++ naming. */
int CrossPlatformAPI::DoSomethingPlatformSpecific() {
NSString* temp_string = [NSString stringWithFormat:@"%d", an_instance_var_];
NSLog(@"%@", temp_string);
return [temp_string intValue];
}
项目可以选择使用 80 列的行长度限制,以与 Google 的 C++ 样式指南保持一致。
仅使用空格,并且一次缩进 2 个空格。 我们使用空格进行缩进。 不要在你的代码中使用制表符。
你应该将你的编辑器设置为在点击 Tab 键时发出空格,并修剪行尾的空格。
Objective-C 文件的最大行长度为 100 列。
在 -
或 +
和返回类型之间应使用一个空格。 通常,除了参数之间,参数列表中不应有间距。
方法应该看起来像这样
// GOOD:
- (void)doSomethingWithString:(NSString *)theString {
...
}
星号前的空格是可选的。在添加新代码时,请与周围文件的风格保持一致。
如果方法声明无法放在一行上,则将每个参数放在单独的一行上。除了第一行之外,所有行都应缩进至少四个空格。参数前的冒号应在所有行上对齐。如果方法声明第一行参数前的冒号的位置导致后续行的缩进小于四个空格,则仅要求除第一行外的所有行进行冒号对齐。如果在方法声明或定义中,在:
之后声明的参数会导致超出行的限制,则将内容换到下一行,并缩进至少四个空格。
// GOOD:
- (void)doSomethingWithFoo:(GTMFoo *)theFoo
rect:(NSRect)theRect
interval:(float)theInterval {
...
}
- (void)shortKeyword:(GTMFoo *)theFoo
longerKeyword:(NSRect)theRect
someEvenLongerKeyword:(float)theInterval
error:(NSError **)theError {
...
}
- (id<UIAdaptivePresentationControllerDelegate>)
adaptivePresentationControllerDelegateForViewController:(UIViewController *)viewController;
- (void)presentWithAdaptivePresentationControllerDelegate:
(id<UIAdaptivePresentationControllerDelegate>)delegate;
- (void)updateContentHeaderViewForExpansionToContentOffset:(CGPoint)contentOffset
withController:
(GTMCollectionExpansionController *)controller;
首选将返回类型放在与函数名相同的行上,如果所有参数都适合,则将它们追加在同一行上。将不适合单行的参数列表像函数调用中的参数一样进行换行。
// GOOD:
NSString *GTMVersionString(int majorVersion, int minorVersion) {
...
}
void GTMSerializeDictionaryToFileOnDispatchQueue(
NSDictionary<NSString *, NSString *> *dictionary,
NSString *filename,
dispatch_queue_t queue) {
...
}
函数声明和定义还应满足以下条件
在 if
、while
、for
和 switch
之后以及比较运算符周围包含一个空格。
// GOOD:
for (int i = 0; i < 5; ++i) {
}
while (test) {};
当循环体或条件语句适合单行时,可以省略大括号。
// GOOD:
if (hasSillyName) LaughOutLoud();
for (int i = 0; i < 10; i++) {
BlowTheHorn();
}
// AVOID:
if (hasSillyName)
LaughOutLoud(); // AVOID.
for (int i = 0; i < 10; i++)
BlowTheHorn(); // AVOID.
如果 if
子句有一个 else
子句,则两个子句都应使用大括号。
// GOOD:
if (hasBaz) {
foo();
} else { // The else goes on the same line as the closing brace.
bar();
}
// AVOID:
if (hasBaz) foo();
else bar(); // AVOID.
if (hasBaz) {
foo();
} else bar(); // AVOID.
有意地继续执行下一个 case 语句应该用注释记录,除非该 case 在下一个 case 之前没有任何中间代码。
// GOOD:
switch (i) {
case 1:
...
break;
case 2:
j++;
// Falls through.
case 3: {
int k;
...
break;
}
case 4:
case 5:
case 6: break;
}
在二元运算符和赋值运算符周围使用空格。省略一元运算符的空格。不要在括号内添加空格。
// GOOD:
x = 0;
v = w * x + y / z;
v = -y * (x + z);
表达式中的因子可以省略空格。
// GOOD:
v = w*x + y/z;
方法调用的格式应与方法声明的格式非常相似。
当有多种格式样式可供选择时,请遵循给定源文件中已使用的约定。调用应该将所有参数放在一行上
// GOOD:
[myObject doFooWith:arg1 name:arg2 error:arg3];
或每行有一个参数,并对齐冒号
// GOOD:
[myObject doFooWith:arg1
name:arg2
error:arg3];
不要使用以下任何样式
// AVOID:
[myObject doFooWith:arg1 name:arg2 // some lines with >1 arg
error:arg3];
[myObject doFooWith:arg1
name:arg2 error:arg3];
[myObject doFooWith:arg1
name:arg2 // aligning keywords instead of colons
error:arg3];
与声明和定义一样,当第一个关键字比其他关键字短时,将后面的行缩进至少四个空格,保持冒号对齐
// GOOD:
[myObj short:arg1
longKeyword:arg2
evenLongerKeyword:arg3
error:arg4];
包含多个内联块的调用可以将其参数名称左对齐,并缩进四个空格。
函数调用应包含尽可能多的参数,以适应每行,除非为了清晰或记录参数而需要较短的行。
函数参数的续行可以缩进以与左括号对齐,也可以缩进四个空格。
// GOOD:
CFArrayRef array = CFArrayCreate(kCFAllocatorDefault, objects, numberOfObjects,
&kCFTypeArrayCallBacks);
NSString *string = NSLocalizedStringWithDefaultValue(@"FEET", @"DistanceTable",
resourceBundle, @"%@ feet", @"Distance for multiple feet");
UpdateTally(scores[x] * y + bases[x], // Score heuristic.
x, y, z);
TransformImage(image,
x1, x2, x3,
y1, y2, y3,
z1, z2, z3);
使用具有描述性名称的局部变量来缩短函数调用并减少调用的嵌套。
// GOOD:
double scoreHeuristic = scores[x] * y + bases[x];
UpdateTally(scoreHeuristic, x, y, z);
使用与前面的 }
在同一行上的 @catch
和 @finally
标签来格式化异常。在 @
标签和左大括号({
)之间,以及在 @catch
和捕获的对象声明之间添加一个空格。如果必须使用 Objective-C 异常,请按以下方式格式化它们。但是,请参阅避免抛出异常,了解为什么不应该使用异常的原因。
// GOOD:
@try {
foo();
} @catch (NSException *ex) {
bar(ex);
} @finally {
baz();
}
首选短小精悍的函数。
长函数和方法有时是合适的,因此不对函数长度进行硬性限制。如果函数超过 40 行左右,请考虑是否可以在不损害程序结构的情况下将其分解。
即使您的长函数现在可以完美运行,几个月后修改它的人也可能会添加新的行为。这可能会导致难以找到的错误。保持函数的短小和简单使得其他人更容易阅读和修改您的代码。
在更新遗留代码时,还可以考虑将长函数分解为更小和更易于管理的部分。
谨慎使用垂直空白。
为了便于在屏幕上查看更多代码,请避免在函数的大括号内放置空行。
将函数之间和逻辑代码组之间的空行限制为一到两行。
不希望遵守这些风格建议的代码行需要在行尾添加 // NOLINT
,或者在前一行的末尾添加 // NOLINTNEXTLINE
。有时,Objective-C 代码的某些部分必须忽略这些风格建议(例如,代码可能是机器生成的,或者代码构造使得无法正确设置风格)。
可以使用该行上的 // NOLINT
注释或前一行上的 // NOLINTNEXTLINE
来向读者表明代码有意忽略风格指南。此外,这些注释也可以被诸如 linters 之类的自动化工具拾取并正确处理代码。请注意,//
和 NOLINT*
之间有一个空格。