欢迎大家关注我的公众号,我会定期分享一些我在项目中遇到问题的解决办法和一些iOS实用的技巧,现阶段主要是整理出一些基础的知识记录下来
文章也会同步更新到我的博客:
ppsheep.com
之前有读过Effective Objective-C 2.0 觉得受益匪浅,决定再读一遍,应该会有不一样的感受,这里就将阅读的过程记录下来,供自己查阅,也供大家参考。
在类的头文件中尽量少引用其他头文件
与C和C++一样,OC也是使用头文件(.h)和实现文件(.m)来分隔代码。
代码看上去是这样:
//PPSTeacher.h
#import <Foundation/Foundation.h>
@interface PPSTeacher : NSObject
@property (nonatomic, copy) NSString *name;
@end
//PPSTeacher.m
#import "PPSTeacher.h"
@implementation PPSTeacher
//具体实现
@end
在OC中 每个类都需要引入Foundation.h。如果在该类本身不引用,那么就需要引用与其超类所对应的基本头文件,比如我们常见的UIViewController 通常继承UIViewControlelr的子类都需要引入UIKit.h 因为在每个用到的控件都会用到UIKit中的大部分内容,在需要用到的那些控件中都已经引入了Foundation.h 所以实际上 还是引入了Foundatiuon框架
好我们现在需要再创建一个学生类 PPSStudent ,每位老师有一位学生,那么我们的代码可能就需要这么写了
#import <Foundation/Foundation.h>
#import "PPSStudent.h"
@interface PPSTeacher : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) PPSStudent *student;
@end
这样写当然不会有什么问题,但是我们想想,在Teacher中,我只需要知道有学生这么一个类就好,我不想知道他到底能干什么,我也不关心。OC提供了这样一种方法,叫做“向前声明”:
@class PPSStudent;
#import <Foundation/Foundation.h>
@class PPSStudent;
@interface PPSTeacher : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) PPSStudent *student;
@end
在实现文件中我们需要引入 #import "PPSStudent.h",因为在实现过程中,我们需要知道PPSStudent能做哪些事情
#import "PPSTeacher.h"
#import "PPSStudent.h"
@implementation PPSTeacher
//实现
@end
这样做的好处有两个:
- 能够缩短编译器的编译时间
- 还能够避免循环引用
避免循环引用:
之前是每个老师拥有一个学生,我们再加上逻辑每个学生需要一个老师
如果按照之前直接#import的引入方式
PPSTeacher.h
#import <Foundation/Foundation.h>
#import "PPSStudent.h"
@interface PPSTeacher : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) PPSStudent *student;
@end
PPSStudent.h
#import <Foundation/Foundation.h>
#import "PPSTeacher.h"
@interface PPSStudent : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *name;
- (void)addTeacher:(PPSTeacher *)teacher;
@end
如果我们是按照这种方式去引用了 那么就会造成循环引用,编译是会报错的
在编译PPSStudent时 import了PPSTeacher,然后编译器编译PPSTeacher,编译PPSTeacher时,又发现了PPSStudent, 这样就造成了引用循环。
多用字面量,少用与之等价的方法
使用字面量,能够使代码简洁易读
字面量数值
常规的方法我们需要
NSNumber *number = [NSNumber numberWithInt:1];
使用字面量,我们只需要
NSNumber *number = @1;
并且NSNumber实例表示的所有数据类型都可以使用字面量:
NSNumber *number = @1;
NSNumber *number = @2.5f;
NSNumber *number = @23.312121;
NSNumber *number = @YES;
NSNumber *number = @'a';
字面量数组
一般的创建数组方法:
NSArray *arr = [NSArray arrayWithObjects:@"1",@"2", nil];
使用字面量
NSArray *arr = @[@"1",@"2"];
字面量字典
一般创建方法:
NSDictionary *dic = [NSDictionary dictionaryWithObjectsAndKeys:@"key1",@"object1",@"key2",@"object2", nil];
这样会给我们带来困惑,key和value完全不能一样区分开来
使用字面量:
NSDictionary *dic = @{
@"key1" : @"value1",
@"key2" : @"value2",
};
使用字面量要点
- 对于字符串、数值、数组、字典,应尽量使用字面量创建
- 访问数组或字典,应尽量使用下标发来访问 例如:arr[1] dic[@"key1"]
- 创建字面量时,需要保证值中没有nil对象,否则会报异常
多用类型常量,少用#define预处理指令
首先讨论不需要对外开放使用的常量
编写代码时,我们经常会定义一些常量。例如,要写一个UIView视图类,此视图显示出来后开始播放动画,播放完成后消失。那么我们可能就会想把这个动画的播放时间,提取为一个常量,一般来说,我们会写成这样:
当然是在.m中定义,因为不需要对外开放
#define ANIMATION_DURATION 0.3
这样定义有一个坏处,就是我们并不知道这个常量 是一个什么类型的常量,也不知道他究竟是干什么的,有一个办法比这种预处理指令更好
static const NSTimeInterval kAnimationDuration = 0.3;
按照此方法定义的常量表明了他的类型为NSTimeInterval,有助于其他团队成员理解代码,并且有助于编写开发文档,如果有更多的常量定义,那么这种方法就更能展现他的优势
对于常量的命名,一般用法是:
如果常量只是作用于当前的编译单元(就是当前的.m实现类),那么应该在常量的名称前加上k
如果常量还要作用于外部,需要以当前的类名为前缀
常量一定要用static const两个一起定义,因为我们本来就是希望它是一个常量,不能够被更改
还有一个原因,因为我们常量只作用于当前的.m类,如果不加上static,那么编译器在编译我们当前的类时,会给它加上一个外部符号(external symbol),如果其他类也定义了一个相同的同名变量,那么编译器就会报错
需要对外开放的常量
这种情况,一般我们比较常见的是,在当前类中需要完成一项操作,需要发送一个全局通知(NSNotificationCenter),用以通知他人,在派发通知时,我们需要用到当前的一个常量字符串,在外部,接收者也需要知道这样一个字符串
我们通常这样定义
在头文件中:(假定当前的类名是PPSView)
extern NSString *const PPSViewNotofication;
在实现文件中
NSString *const PPSViewNotofication = @"PPSViewNotofication";
这样定义的话,在引入该头文件的文件中,当编译器知道extern关键字时,就能明白,在全局符号表中需要一个PPSViewNotofication的符号,编译器无需知道这个符号的定义,当链接成二进制文件后,就能找到这个常量。
在对象内部尽量直接访问实例变量
在对象之外,我们知道总是通过属性(property)来对实例变量进行操作,那么在实例内部应该怎么做呢? 强烈建议在除了在懒加载中,其他情况下,都应该是:
在读取变量时,都应该采用直接访问的形式(_变量名),在设置实例变量时通过属性来设置
我们来看个例子:
当前有个PPSPerson类
//PPSPerson.h
@interface PPSPerson : NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
- (NSString *)fullName;
- (void)setFullName:(NSString *)funllName;
@end
//PPSPerson.m
@implementation PPSPerson
-(NSString *)fullName{
return [NSString stringWithFormat:@"%@ %@",self.firstName,self.lastName];
}
-(void)setFullName:(NSString *)funllName{
NSArray *components = [funllName componentsSeparatedByString:@" "];//通过空格 将firstName 和lastName分割开来
self.firstName = components[0];
self.lastName = components[1];
}
@end
在上面的代码中,我们都通过了点语法,来存取相关的实例变量,现在假设我们不经过存取方法,而是直接访问实例变量:
@implementation PPSPerson
-(NSString *)fullName{
return [NSString stringWithFormat:@"%@ %@",_firstName,_lastName];
}
-(void)setFullName:(NSString *)funllName{
NSArray *components = [funllName componentsSeparatedByString:@" "];//通过空格 将firstName 和lastName分割开来
_firstName = components[0];
_lastName = components[1];
}
@end
这两种写法有以下几个区别:
- 由于不经过Objective-C的“方法派发”(后面会讲到),所以直接访问实例变量的速度当然比较快。编译器所生成的代码会直接访问保存对象实例变量的那块内存
- 直接访问实例变量时,不会调用其“设置方法”,这就绕过了为相关属性所定义的“内存管理语义”。比如,如果在ARC下直接访问一个声明为copy的属性,那么并不会拷贝该属性,只会白柳新值并释放旧值
- 如果直接访问,不会触发KVO,当然这具体还是需要看需求
- 通过属性访问有助于排查与之相关的错误,可以再set和get方法中新增断点调试
之前讲的懒加载方法,就必须使用属性来访问了,否则实例变量永远不会初始化
要点
- 在对象内部读取数据时,应该直接通过实例变量来读,而写入数据时,则应该通过属性来写
- 在初始化及dealloc方法中,总是应该通过实例变量来读写数据
- 在懒加载中应该通过属性来读取数据