读书总结《effective objectivec 2.0》编写高质量IOS与OS X代码的52个有效方法
下面简单分享下在平时开发中会常接触到的15条建议。
第二条:在类的头文件中尽量少引用其他头文件
1.除非必要情况,否则不要引入头文件。一般情况,应在某个类的头文件中使用向前声明来提及别的类(@class),并在实现文件中引入那些类的头文件。这样做可以降低类之间的耦合,并减少编译时间;
2.使用向前声明也解决了两个类互相引用的问题,使用#import时会出现无法被正确编译的问题;
3.有时无法使用向前声明,比如要声明某个类遵循一项协议。这种情况下,尽量把“该类遵循某协议”的这条声明移至“class-continuation分类中”。如果不行的话,就把协议单独放在一个头文件中,然后将其引入;
第五条:用枚举表示状态、选项、状态码
1.应该用枚举来表示状态机的状态、传递给方法的选项以及状态码等值,给这些值起个易懂的名字;
2.如果把传递给某个方法的选项表示为枚举类型,而多个选项又可同时使用,那么就将个选项值定义为2的幂,以便通过按位或操作将其组合起来(参见UIView的UIViewAutoresizing枚举定义);
3.用NS_ENUM和NS_OPTIONS宏来定义枚举类型,并指明其底层数据类型。这样做可以确保枚举是用开发者所选的底层数据类型出来的,而不采用编译器所选的类型;
4.在处理枚举类型的switch语句中不要实现default分支。这样的话,加入新枚举之后,编译器就会提示:switch语句并未处理所有枚举。使用default就会漏掉某种状态;
第七条:在对象内部尽量直接访问实例变量
1.对象内部读取数据时,应该直接通过实例变量来读,而写入数据时,则应通过属性来写;
2.在初始化方法及dealloc方法中,总是应该直接通过实例变量来读写数据;
3.有时会使用惰性初始化方法配置数据,这种情况下,需要通过属性来读取数据;
4.直接访问实例变量和属性的区别:
直接访问实例变量,由于不经过OBJ-C的方法派发,所以速度会比较快,编译器会直接访问保存对象实例的内存;
直接访问实例变量,不会调用设置方法,绕过了内存管理语义;
直接访问实例变量,不会触发KVO;
通过属性访问,有助于排查问题;
第八条:理解“对象等同性”这一概念
1.使用set集合的注意事项,等同性判定的执行深度
创建一个可变数组arr1=[@[@1,@2] mutbleCopy];将其放入NSMutableSet中,再创建一个可变数组arr2=[@[@1,@2] mutbleCopy];将其放入NSMutableSet中。这个时候我们打印set,set={((1,2))},因为arr1和arr2数组对象相 同,所以set不会改变;
再次创建一个可变数组arr3=[@[@1] mutbleCopy];将其放入NSMutableSet中,set={((1),(1,2))},这个时候我们修改[arr3 addObject:@2];发现set={((1,2),(1,2))},更神奇的地方是,如果我们set2=[set copy],set2={((1,2))}。这个地方一定要注意!
如果把某对象放入set之后又修改其内容,后面的行为将很难预料。
第九条:以“类簇模式”隐藏实现细节
1.类簇模式可以把实现细节隐藏在一套简单的公告接口后面;
类簇是Foundation框架中广泛使用的设计模式。类簇将一些私有的、具体的子类组合在一个公共的、抽象的超类下面,以这种方法来组织类可以简化一个面向对象框架的公开架构,而又不减少功能的丰富性。比如:
UIButton的buttonWithType:(UiButton)type;
另外还有很多系统框架都使用类簇;
这种模式和工厂模式有很多相似之处;
2.从类簇的公共抽象基类中继承子类时要当心,若有开发文档,则应首先阅读;
因为Obj-C没法标明某个基类是抽象的,所以需要开发者自己在文档中写明类的用法
第十四条:理解“类对象”的用意
1.如果对象类型无法在编译期确定,那么就应该使用类型信息查询方法来探知;
isMemberOfClass:能够判断出对象是否为某个特定类的实例;
isKindOfClass:能够判断出对象是否为某类或其派生类的实例;
2.尽量使用类型信息查询方法来确定对象类型,而不要直接比较对象类型,因为某些对象可能实现了消息转发功能;
第十五条:用前缀避免命名空间的冲突
1.选择与你的公司、app或者二者皆有关联的名称作为类前缀,并在所有代码中均使用这一前缀;
2.若自己所开发的的程序库中用到了第三方库,则应为其中的名称加上前缀;
第十八条:尽量使用不可变对象
1.尽量创建不可变的对象;
尽量把对外公布出来的属性设为只读(readonly),而且只在的确有必要时才将属性公布出来。
2.如果对象内部要进行修改,可以在对象内部设置成(readwrite).
不过即使设置成readonly,仍然能通过KVC的方式设置这些属性,通过setValue:forKey来修改。不过这样做苹果官方是不建议的
3.不要把可变的集合作为属性公开,而应提供相关方法,以此修改对象中的集合;
第二十二条:理解NSCopying协议
1.若想令自己所写的对象具有copy功能,则需要实现NSCopying协议;
要实现copyWithZone:方法,举个例子,定义一个类:EOCPerson
@interfaceEOCPerson :NSObject
@property(nonatomic,strong)NSString*firstName;
@property(nonatomic,strong)NSString*lastName;
@property(nonatomic,strong)NSString*friends;
-(id)initWithFirstName:(NSString*)firstName andLastName:(NSString*)lastName;
@end
实现协议方法:
-(id)copyWithZone:(NSZone*)zone
{
EOCPerson*copy = [[[selfclass]allocWithZone:zone]initWithFirstName:_firstName
andLastName:_lastName];
returncopy;
}
2.如果自定义的对象分为可变版本与不可变版本,那么就要同时实现NSCopying与NSMutableCopying协议;
-(id)mutableCopyWithZone:(NSZone *)zone;
对于不可变的NSArray和可变的NSMutableArray来说:
[NSMutableArray copy] =>NSArray
[NSArray mutableCopy] =>NSMutableArray
copy所返回的拷贝对象与当前对象的类型一致,而mutableCopy和immutableCopy反别返回可变和不可变的拷贝。
3.复制对象时需要决定采用浅拷贝还是深拷贝,一般情况下应该尽量执行浅拷贝;
Foundation框架中的所有集合类,默认情况下都执行浅拷贝;
在NSArray 类中,提供了一个初始化方法用以执行深拷贝:
NSArray *arr=[[NSArray alloc]initWithArray:_arrComment copyItems:YES]; 当copyItems:YES时,会向数组中的每个元素发送copy消息,用拷贝好的元素创建新的数组。同样NSDictionary、NSSet等类也 存在该方法;
如果集合里要包含其他类的对象,需要在该类中实现相应的copyWithZone:方法
4.如果你所写的对象需要深拷贝,那么可以考虑新增一个专门执行深拷贝的方法;
第二十四条:将类的实现代码分散到便于管理的数个分类之中
1.使用分类机制把类的实现代码划分成容易管理的小块;
如果一个类较为复杂的话,可以按照一定的规则把他划分为多个小类,比如:
@interfaceEOCPerson :NSObject
@property(nonatomic,strong)NSString*firstName;
@property(nonatomic,strong)NSString*lastName;
@property(nonatomic,strong)NSString*friends;
-(void)addFirend:(EOCPerson*)person;
-(void)removeFirend:(EOCPerson*)person;
-(void)performDaysWork;
-(void)takeVacationFromwork;
-(void)gotoTheCinema;
-(void)gotoSportGame;
@end
改写为:
@interfaceEOCPerson :NSObject
@property(nonatomic,strong)NSString*firstName;
@property(nonatomic,strong)NSString*lastName;
@property(nonatomic,strong)NSString*friends;
@end
@interfaceEOCPerson(Friendship)
-(void)addFirend:(EOCPerson*)person;
-(void)removeFirend:(EOCPerson*)person;
@end
@interfaceEOCPerson(Work)
-(void)performDaysWork;
-(void)takeVacationFromwork;
@end
@interfaceEOCPerson(Play)
-(void)gotoTheCinema;
-(void)gotoSportGame;
@end
2.将应该视为“私有”的方法归入名为Private的分类中,以隐藏实现细节。
第三十七条:理解“块”这一概念
1.块是C、C++、Obj-C中的词法闭包;
借由此机制,开发者可将代码像对象一样传递,令其在不同环境下运行。
2.块可以接受参数、也可以返回值;
其语法结构:return_type (^block_name) (parameters) 举例:
int (^addBlock) (int a,int b)=^(int a,int b){
return a+b;
}
int add=addBlock(1,2);
3.块的强大之处是:在声明它的范围里,所有变量都可以为其所使用。不过默认情况下这些变量是不能修改的,除非在声明变量时加上__block;
如果块捕获的变量是对象类型,那么就会自动保留它,也包括:self,所以要定义一个weak形式的self来解决循环引用的问题;
4.块可以分配在栈或堆上,也可以是全局的。分配在栈上的块可以拷贝到堆里,这样的话,就和标准的Obj-C对象一样,具备引用计数了;
void(^block)();
if (self) {
block=^{
DLog(@"[block]-a");
};
}
else{
block=^{
DLog(@"[block]-b");
};
}
block();
由于定义块的时候,其所占的内存区域是分配在栈中得,也就是说,块只在定义它的那个范围内有效。上面的例子中得块只能保证在if或else语句范围内有效,虽然可以编译,但是运行时有时会正确有时会错误,甚至导致crush。
为解决此问题,可以给块对象发送copy消息,这样的话,块从栈复制到堆了。并且可以在定义的范围之外使用。
void(^block)();
if (self) {
block=[^{
DLog(@"[block]-a");
} copy];
}
else{
block=[^{
DLog(@"[block]-b");
} copy];
}
block();
第三十八条:为常用的块类型创建typedef
1.以typedef重新定义块类型,可令块变量用起来更简单;
typedef int (^FBlock) (int var,BOOL flag);
FBlock block=^(int var, BOOL flag){};
注意:定义方法参数所用的块类型语法,和定义变量时不同:
typedef int (^FRequestHandler) (int var,BOOL flag);
-(void)requestHttp:(FRequestHandler)handler;
2.定义新类型时应遵从现有的命名习惯,务使其名称与别的类型相冲突;
3.不妨为同一个块签名多个类型别名。如果要重构的代码使用了块类型的某个别名,那么只需修改相应typedef中的签名即可,无须改动其他typedef;
如果好几个类都要执行相似但各有区别的异步任务,而这几个类又不能放入同一个继承体系,那么,每个类就应该有自己的处理方法,并在每个类中定义各自的别名;反之若这些类能纳入同一个继承体系中,则应该将类型定义语句放在超类,供各个子类使用。
第三十九条:用handler块降低代码分散程度
1.在创建对象时,可以使用内联的handler块将相关业务逻辑一并声明;
2.在有多个实例需要监控时,如果采用委托模式,那么经常需要根据传入的对象来切换,而若改用handler块来实现,则可直接将块与相关对象放在一起;
委托模式和handler块经常可以实现同样的功能,但是在有些情况handler会比委托模式更灵活:如果一个类中有多个方法实现同一个代理(处理不 同业务数据),那么代理方法中需要根据不同业务数据做判断,使得delegate回调方法变得很长。而使用块的话,针对每一个业务数据处理,执行各自的业 务逻辑。
3.在基于块的API都是用块来处理错误,分别处理成功和失败情况(目前我们工程大部分代码都是这么做的),这种写法令代码更易读懂。
不过也有些情况会把两者放在一起,虽说放在一起会令代码变长并且复杂。但是有些特殊情况使用此种方法会更好,比如:成功响应的过程中,发现错误。数据不完整,这种情况可能也是错误的一种,要按照失败去处理。分开处理的话就没法共享同一份错误处理代码。
总的来说作者建议用同一个块处理成功与失败,苹果公司的API也是这么设计的。
4.设计API时如果用到了handler块,那么可以增加一个参数,使调用者可通过此参数来决定应该把块安排在哪个队列上执行;
比如NSNotification就属于这种API:
[[NSNotificationCenter defaultCenter] addObserverForName:@"notification"
object:self
queue:[NSOperationQueue currentQueue]
usingBlock:^(NSNotification *note) {
}];
第四十二条:多用GCD,少用performSelector系列方法
1.performSelector系列方法在内存管理方面容易有疏失。它无法确定将要执行的选取器具体是什么,因而ARC编译器也就无法插入适当的内存管理方法;
如果根据判断得到选取器,我们只有在运行时才知道真正的选取器,如下:
SEL selector;
if (expression) {
selector=@selector(@"new");
}
else if (expression){
selector=@selector(@"copy");
}
else{
selector=@selector(@"action");
}
id ret=[self performSelector:selector];
如果用非ARC前两个方法需要由变量ret释放,第三个方法不需要释放;如果用ARC系统很难判断添加内存管理方法;
2.performSelector系列方法所能处理选取器过于局限,返回类型及发送给方法的参数个数都有限制;
3.如果想把任务放在另一个线程执行,那么最好通过GCD来去替代performSelector系列方法;
比如延迟调用可以使用:
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
});
其他替代方法完全可以使用dispatch_async等实现;
第四十八条:多用块枚举,少用for循环
1.遍历collection有四种方式。最基本的就是for循环,其次是NSEnumerator遍历法及快速遍历法,最新、最先进的方式则是“块枚举法”;
NSArray *arr=@[@"a",@"b"];
[arr enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {}];
NSDictionary *dic=@{@"a": @"1"};
[dic enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {}];
obj就是当前遍历的对象;idx当前索引;stop类似于循环中使用break,用于终止遍历操作。
2.快枚举法本身就能通过GCD来并发执行遍历操作,无须另行编写代码。而采用其他遍历方式则无法轻易实现这一点;
3.若提前知道待遍历的集合含有何种对象,则应修改块签名,指出对象的具体类型;
NSArray *arr=@[@"a",@"b"];
[arr enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {}];
如果已知数组里的对象类型,可以将id obj改为NSString *string,否则还需要自己做类型装换。
以上就是本次分享的15条开发建议。全书总共52条,需要重复阅读来强化记忆和加强理解,同时结合实际应用来理解推荐的这些方法的好处。有感兴趣的欢迎借阅~~