Skip to Content
Objective-CCocoa 和 Objective-C 特性

Cocoa 和 Objective-C 特性

标识指定初始化方法

清楚地标识你的指定初始化方法(Designated Initializer)。

对于子类化(Subclassing)来说,类清楚地标识其指定初始化方法很重要。这允许子类重写一部分初始化方法来初始化子类状态,或调用子类提供的新指定初始化方法。清楚标识的指定初始化方法也使跟踪和调试初始化代码更加容易。

优先通过使用指定初始化方法属性(Attribute)来标识指定初始化方法,例如 NS_DESIGNATED_INITIALIZER。当指定初始化方法属性不可用时,在注释中声明指定初始化方法。除非有令人信服的理由或要求需要多个指定初始化方法,否则优先使用单个指定初始化方法。

通过重写父类指定初始化方法来支持从父类继承的初始化方法,确保所有继承的初始化方法都被引导到子类的指定初始化方法。当有令人信服的理由或要求不应支持继承的初始化方法时,可以使用可用性属性(Availability Attribute)(例如 NS_UNAVAILABLE)来标注该初始化方法以阻止使用;但请注意,仅有可用性属性并不能完全防止无效初始化。

重写指定初始化方法

编写需要新指定初始化方法的子类时,确保重写父类的所有指定初始化方法。

在类上声明指定初始化方法时,请记住父类上被视为指定初始化方法的任何初始化方法都将成为子类的便捷初始化方法(Convenience Initializer),除非另有声明。未重写父类指定初始化方法可能导致使用父类初始化方法进行无效初始化的 bug。为避免无效初始化,确保便捷初始化方法调用到指定初始化方法。

重写的 NSObject 方法的放置

将 NSObject 的重写方法放在 @implementation 的顶部。

这通常适用于(但不限于)init...copyWithZone:dealloc 方法。init... 方法应分组在一起,包括那些不是 NSObject 重写的 init... 方法,后面跟着其他典型的 NSObject 方法,如 descriptionisEqual:hash

用于创建实例的便捷类工厂方法(Convenience Class Factory Method)可以放在 NSObject 方法之前。

初始化

不要在 init 方法中将实例变量初始化为 0nil;这样做是多余的。

新分配对象的所有实例变量都被初始化为  0(除了 isa),因此不要通过将变量重新初始化为 0nil 来使 init 方法变得杂乱。

头文件中的实例变量应为 @protected 或 @private

实例变量通常应在实现文件中声明或由属性自动合成(Auto-synthesize)。当实例变量在头文件中声明时,应标记为 @protected@private

// GOOD: @interface MyClass : NSObject { @protected id _myInstanceVariable; } @end

不要使用 +new

不要调用 NSObject 的类方法 new,也不要在子类中重写它。+new 很少使用,且与初始化方法的使用方式形成很大反差。应使用 +alloc-init 方法来实例化持有的对象。

保持公共 API 简洁

保持你的类简洁;避免”大杂烩”式的 API。如果一个方法不需要公开,就不要放在公共接口中。

与 C++ 不同,Objective-C 不区分公有方法和私有方法;任何消息都可以发送给一个对象。因此,除非方法实际上预期被类的使用者调用,否则避免将方法放在公共 API 中。这有助于减少在你不期望时被调用的可能性。这也包括从父类重写的方法。

由于内部方法并非真正私有,因此很容易意外重写父类的”私有”方法,从而制造一个非常难以排查的 bug。一般来说,私有方法应有一个相当独特的名称,以防止子类无意中重写它们。

#import 和 #include

使用 #import 导入 Objective-C 和 Objective-C++ 头文件,使用 #include 包含 C/C++ 头文件。

C/C++ 头文件使用 #include 包含其他 C/C++ 头文件。对 C/C++ 头文件使用 #import 会阻止后续使用 #include 的包含,并可能导致意外的编译行为。

C/C++ 头文件应提供自己的 #define 保护(Guard)。

包含顺序

头文件包含的标准顺序是:相关头文件、操作系统头文件、语言库头文件,最后是其他依赖项的头文件组。

相关头文件优先于其他头文件,以确保它没有隐藏的依赖。对于实现文件,相关头文件是其头文件。对于测试文件,相关头文件是包含被测试接口的头文件。

每个非空的包含组之间用一个空行分隔。在每个组内,包含应按字母顺序排列。

使用相对于项目源目录的路径导入头文件。

// 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 Header)而不是包含单个文件。

虽然从 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 中直接赋值和释放实例变量,而不是依赖访问器。

// 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. 对子类化或未来扩展来说很脆弱。 } 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]]; } // 不需要复制,generatedList 的所有权被转移。 return generatedList; }

此规则也适用于仅存在可变变体的类,只要所有权转移是明确的。Proto 是一个常见的例子。

// 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

复制可能可变的对象

接收并持有集合(Collection)或其他具有可变变体 的类型的代码应考虑到传递的对象可能是可变的,因此应持有不可变或可变副本而不是原始对象。特别是,初始化方法和 setter 应复制而非持有其类型具有可变变体的对象 

合成的访问器应使用 copy 关键字以确保生成的代码符合这些期望。

注意:copy 属性关键字仅影响合成的 setter,对 getter 没有影响。由于属性关键字对直接实例变量访问没有影响,自定义访问器必须实现相同的复制语义。

// 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 { // 确保我们持有一个不可变集合。 _filters = [filters copy]; }

同样,getter 必须返回与其返回的不可变类型的契约期望相匹配的类型。

// GOOD: @implementation Foo { NSMutableArray<ContentThing *> *_currentContent; } - (NSArray<ContentThing *> *)currentContent { return [_currentContent copy]; }

所有 Objective-C proto 都是可变的,通常应复制而非持有,除非在明确的所有权转移情况下

// GOOD: - (void)setFooMessage:(FooMessage *)fooMessage { // 复制 proto 以确保没有其他持有者可以修改我们的值。 _fooMessage = [fooMessage copy]; } - (FooMessage *)fooMessage { // 复制 proto 返回,以便调用者无法修改我们的值。 return [_fooMessage copy]; }

异步代码应在派发(Dispatch)之前复制可能可变的对象。被块捕获的对象会被持有但不会被复制。

// GOOD: - (void)doSomethingWithThings:(NSArray<Thing *> *)things { NSArray<Thing *> *thingsToWorkOn = [things copy]; dispatch_async(_workQueue, ^{ for (id<Thing> thing in thingsToWorkOn) { ... } }); }

注意:不需要复制没有可变变体的对象,例如 NSURLNSNumberNSDateUIColor 等。

使用轻量级泛型来文档化包含的类型

所有在 Xcode 7 或更新版本上编译的项目都应使用 Objective-C 轻量级泛型(Lightweight Generics)表示法来标注包含的对象类型。

每个 NSArrayNSDictionaryNSSet 引用都应使用轻量级泛型声明,以提高类型安全性并明确文档化用法。

// 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 异常(Exception),但你应该准备好从第三方或操作系统调用中捕获它们。

这遵循了 Apple 的异常编程主题介绍 中使用错误对象进行错误传递的建议。

我们使用 -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 参数值。

还要注意,这与检查 C/C++ 指针和块指针是否为 NULL 不同,运行时不会处理后者,这将导致你的应用程序崩溃。你仍然需要确保不解引用 NULL 指针。

可空性

接口可以用可空性注解(Nullability Annotation)来装饰,以描述接口应如何使用及其行为。使用可空性区域(例如 NS_ASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_END)和显式可空性注解都是可接受的。优先使用 _Nullable_Nonnull 关键字而非 __nullable__nonnull 关键字。对于 Objective-C 方法和属性,优先使用上下文敏感的、无下划线前缀的关键字,例如 nonnullnullable

// GOOD: /** 一个表示已拥有书籍的类。 */ @interface GTMBook : NSObject /** 书的标题。 */ @property(nonatomic, readonly, copy, nonnull) NSString *title; /** 书的作者(如果存在)。 */ @property(nonatomic, readonly, copy, nullable) NSString *author; /** 书的所有者。设置为 nil 会重置为默认所有者。 */ @property(nonatomic, copy, null_resettable) NSString *owner; /** 使用标题和可选的作者初始化一本书。 */ - (nonnull instancetype)initWithTitle:(nonnull NSString *)title author:(nullable NSString *)author NS_DESIGNATED_INITIALIZER; /** 返回 nil,因为书应该有标题。 */ - (nullable instancetype)init; @end /** 从给定路径指定的文件加载书籍。 */ NSArray<GTMBook *> *_Nullable GTMLoadBooksFromFile(NSString *_Nonnull path);
// AVOID: NSArray<GTMBook *> *__nullable GTMLoadBooksFromTitle(NSString *__nonnull path);

不要因为 nonnull 限定符就假设指针不为 null,因为编译器只检查此类情况的一个子集,并且不保证指针不为 null。避免故意违反函数、方法和属性声明的可空性语义。

BOOL 陷阱

BOOL 表达式和转换

将一般整数值转换为 BOOL 时要小心。避免直接与 YES 比较或使用比较运算符比较多个 BOOL 值。

在某些 Apple 平台上(特别是 Intel macOS、watchOS 和 32 位 iOS),BOOL 被定义为有符号 char,因此它可能具有 YES1)和 NO0)以外的值。不要将一般整数值直接转换为 BOOL

常见错误包括将数组大小、指针值或位逻辑运算的结果转换为 BOOL。这些操作可能依赖于整数值的最后一个字节的值,并导致意外的 NO 值。使用 NS_OPTIONS 值和标志掩码的操作是特别常见的错误。

将一般整数值转换为 BOOL 时,使用条件运算符返回 YESNO 值。

你可以安全地互换和转换 BOOL_Boolbool(参见 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. // ...太棒了! }
// GOOD: BOOL great = [foo isGreat]; if (great) { // GOOD. // ...太棒了! }

不要使用比较运算符直接比较 BOOL 值。为 true 的 BOOL 值可能不相等。使用逻辑运算符代替 BOOL 值的按位比较。

// AVOID: if (oldBOOLValue != newBOOLValue) { // AVOID. // ... 仅在值改变时运行的代码。 }
// GOOD: if ((!oldBoolValue && newBoolValue) || (oldBoolValue && !newBoolValue)) { // GOOD. // ... 仅在值改变时运行的代码。 } // GOOD, 对 BOOL 使用逻辑运算符的结果可以安全比较。 if (!oldBoolValue != !newBoolValue) { // ... 仅在值改变时运行的代码。 }

BOOL 字面量

BOOL NSNumber 字面量 @YES@NO,它们等同于 [NSNumber numberWithBool:...]

避免使用装箱表达式 来创建 BOOL 值,包括像 @(YES) 这样的简单表达式。装箱表达式与其他 BOOL 表达式一样存在相同的陷阱,因为装箱一般整数值可能产生不等于 @YES@NO 的 true 或 false NSNumber

将一般整数值转换为 BOOL 字面量时,使用条件运算符转换为 @YES@NO。不要在装箱表达式内嵌入条件运算符,因为即使操作结果是 BOOL,这也等同于装箱一般整数值。

// AVOID: [_boolArray addValue:@(YES)]; // AVOID, 即使在简单情况下也避免装箱。 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.

没有实例变量的容器

在没有任何实例变量声明的接口、类扩展(Class Extension)和实现上省略空的大括号。

// GOOD: @interface MyClass : NSObject // 做很多事情。 - (void)fooBarBam; @end @interface MyClass () - (void)classExtensionMethod; @end @implementation MyClass // 实际实现。 @end
// AVOID: @interface MyClass : NSObject { } // 做很多事情。 - (void)fooBarBam; @end @interface MyClass () { } - (void)classExtensionMethod; @end @implementation MyClass { } // 实际实现。 @end
Last updated on