《Effective Objective-C 2.0》——Interface and API Design  

这是《Effective Objective-C 2.0》英文版的Chapter3的笔记

使用前缀避免命名空间冲突

  • 前缀以至少3个字母定义,因为Apple有权使用任何两个字母组合作为自己类、方法和框架等等的命名前缀。
  • .m中定义的C函数或常量也要在前面加上前缀,因为尽管它在实现文件中,而不是在头文件中声明的,但编译后它仍会出现在top-level symbol中,所以容易发生冲突。
1
2
3
4
5
6
7
8
void completion(SystemSoundID ssIdD, void *clientData) {
EOCSoundPlayer *player = (__bridge EOCSoundPlayer*)clientData;
if ([player.delegate respondsToSelector:@selector(soundPlayerDidFinish:)]) {
[player.delegate soundPlayerDidFinish:player];
}
}

// 可以将其命名为EOCSoundPlayerCompletion
  • 开源给别人使用的库时,更要加上在类、方法、常量命名前加上前缀。

定义一个特定的初始化方法

  • 要给类定义一个designated initializer,然后其他的初始化方法都通过该designated initializer初始对象。这样在类内部的变量要修改时只需要修改一个地方即可。
  • 涉及到继承时,要override父类的所有初始化方法。但是如果父类的某个初始化方法是不能用来初始化子类的,可以在override时抛异常出来。比如EOCSquare继承自EOCRectangle,但EOCSquare对象要求width和height必须一样,那么在EOCSquare中就可以像下面这样override父类中的初始化方法。
1
2
3
- (id)initWithWidth:(float)width andHeight:(float)height {
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Must use initWithDimension: instead." userInfo:nil];
}
  • 在严格意义上讲也可能会有多个designated initializer,比如上面的EOCRectangle实现了NSCoding protocol,那么在NSCoding要求实现的初始化方法中就不能调用EOCRectangle自己的designated initializer了,因为它的width和height要通过解码拿到。
1
2
3
4
5
6
7
8
- (id)initWithCoder:(NSCoder *)aDecoder {
// Call through to super's designated initializer
if (self = [super init]) {
_width = [aDecoder decodeFloatForKey:@"width"];
_height = [aDecoder decodeFloatForKey:@"height"];
}
return self;
}

实现description方法

  • 实现NSObject protocol中的descriptiondebugDescription方法来帮助我们快速的调试。description用于NSLog打印对象信息时,debugDescription用于在打断点调试时,可po出该对象的信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (NSString *)description {
return [NSString stringWithFormat:@"%@",
@{@"title":_title,
@"latitude":@(_latitude),
@"longitude":@(_longitude)
}];
}

- (NSString *)debugDescription {
return [NSString stringWithFormat:@"<%@: %p, %@>",
[self class],
self,
@{@"title":_title,
@"latitude":@(_latitude),
@"longitude":@(_longitude)
}];
}

倾向于定义不可变对象

  • 在默认情况下,property是readwrite的,这就使得你所有的classes都是可变的。 如果对象在被创建后就不会被改变了,那最好将其property指名为readonly的。
  • 但因为在class的内部还是要改变property的,所以在.h文件中声明为readonly的property,在.m的顶部extension中再声明一次readwrite就可以在class内部修改了。
  • 但是有一个潜在风险存在,如果property是nonatomic,在多线程的情况下,有可能外部正在读取property的值时,对象的内部正在修改其值。如果有必要,可以通过dispatch queue来保证同步。
  • 对于property中有集合的类,最好对外提供修改该集合的API,而不是把一个可变的集合直接暴露给别人。这样可以把集合的改变点都集中一个地方,而且对你来说是可控的,你可以在添加或移除时做一些额外的事情。
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
34
35
36
37
38
39
40
41
42
43
@interface EOCPerson : NSObject

@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSSet *friends;

- (instancetype)initWithFirstName:(NSString *)firstName
andLastName:(NSString *)lastName;
- (void)addFriend:(EOCPerson *)person;
- (void)removeFriend:(EOCPerson *)person;
@end

// .m
@interface EOCPerson()
@property (nonatomic, copy, readwrite) NSString *firstName;
@property (nonatomic, copy, readwrite) NSString *lastName;
@end
@implementation EOCPerson {
NSMutableSet *_internalFriends;
}

- (NSSet *)friends {
return [_internalFriends copy];
}

- (void)addFriend:(EOCPerson *)person {
[_internalFriends addObject:person];
}

- (void)removeFriend:(EOCPerson *)person {
[_internalFriends removeObject:person];
}

- (instancetype)initWithFirstName:(NSString *)firstName andLastName:(NSString *)lastName {
if (self = [super init]) {
_firstName = firstName;
_lastName = lastName;
_internalFriends = [[NSMutableSet alloc] init];
}
return self;
}

@end

私有方法名要加前缀

  • 私有方法名前加上前缀便于阅读代码时,快速区分时哪些方法是public的哪些是private的。可以使用p_作为前缀,p是private的缩写。但是不用只使用_,因为Apple自家的框架中的私有方法是以_为前缀的,避免覆盖掉框架中的方法。

Understand Objective-C Error Model

  • ARC是exception不安全的,也就是说在一个作用域中抛出exception时,会导致这个作用域内,在定义exception之前的对象无法释放。
  • 只在会发生fatal error时才使用exception,抛出exception时会让程序终止运行。
  • 在一般情况下可以通过返回nil/0来告知错误的发生,也可以使用NSError
  • 可以用delegate的方式,或传入pointer的方式来抛出NSError
1
2
3
4
// 此时要注意,如果调用者如果不关心error的话,最好传入nil,用null可能会导致crash,
- (void)doSomething:(NSError **)error;

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;
  • NSError的error domain最好定义为常量,error code最好定义为NSEnum。

NSCopying Protocol

  • 如果自定义的对象需要具备被copy的能力,那么要实现NSCopying protocol。
1
2
3
4
5
- (instancetype)copyWithZone:(NSZone *)zone {
EOCPerson *copy = [[[self class] allocWithZone:zone] initWithFirstName:_firstName andLastName:_lastName];
copy->_internalFriends = [_internalFriends mutableCopy];
return copy;
}
  • 如果对象同时有mutable和immutable变种,则要同时实现NSCopyingNSMutableCopying协议。
  • 如果要做深拷贝,可以加上一个deep-copy方法。
1
2
3
4
5
- (instancetype)deepCopy {
EOCPerson *copy = [[[self class] alloc] initWithFirstName:_firstName andLastName:_lastName];
copy->_internalFriends = [[NSMutableSet alloc] initWithSet:_internalFriends copyItems:YES];
return copy;
}

本文作者:意林
本文链接:http://www.shinancao.cn/2019/05/05/iOS-EffectiveObjC-1
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!