第二章 对象、消息、运行期
OC是面向对象语言,“对象”是其中最重要的东西,我们通过对象类存储数据和数据传递,理解好对象以及消息传递,会对我们编写程序有很大的帮助。
6. 理解“属性”这一概念
相信大家对属性有一定的认识,不在赘述,下面只说一下大家可能不会在意的地方。
- 变量声明方式@interface和@property的区别
在最早的OC中,属性是OC的新特性,那时候定义属性必须声明与之对应的实例变量,像这样:
在.h文件中
@interface aViewController :UIViewController{
UIView *_aView;
}
@property UIView *aView;
@end
在.m文件中
@synthesize aView = _aView;
后来发现这么写很费劲,经过一步步的改进,把有些事交给编译器干,于是后来只要这样写:
@interface aViewController :UIViewController
@property UIView *aView;
@end
这样写,编译器会自动为我们生成一个_aView实力变量,和set、get方法
注意的点:
1.如果只有@interface没有@property,定义的实力变量是不能用self.调用的,因为self.实际是调用set或get方法,而不写@property是不会自动生成set和get方法的(self.在=右边调用set方法,在=左边调用get方法)。
2.如果还在.m中写@synthesize
,相当于给_aView又取了个名字。
3.现在苹果官方推荐只写@property的写法。
4.如果我们不想让编译器自动给我们生成set和get方法我们可以在.m中添加这样的语句:
@dynamic aView;
5.类别中的属性不生成实例变量,例如:
@interface UIViewController (UINavigationControllerItem)
@property(nonatomic,readonly,strong) UINavigationItem *navigationItem;
@property(nonatomic) BOOL hidesBottomBarWhenPushed;
@property(nullable, nonatomic,readonly,strong) UINavigationController *navigationController;
@end
上面三个属性就没有对应的实例变量
6.我们要注意定义属性的位置,一般只把要暴露的属性定义在.h中,并且一般暴露在外面的属性都是自读,实现方法中自己用的属性一般定义在.m的分类中。
7.熟练的自定义set、get方法,熟练运用懒加载等形式
- 属性的特质
后来发现,在属性定义的时候可以给属性一些特质,这时候就可以这样定义属性了(这些特质很重要,决定了编译器对属性的最终生成形式):
@property (nonatomic, strong) UIView *aView;
属性有四个特质,分别是原子性、读写权限、内存管理语义、方法名,具体的每个特质不详细介绍,只说一下可能忽略的细节:
1.原子性一般都用nonatomic,如果我们用atomic会调用iOS中的同步锁,内存开销较大,开发Mac OS程序时用atomic较多。
2.内存管理语义决定编译器合成存取方法,慎重选择,特别是我们在自定义set、get方法的时候,例如我们声明一个属性的特质是copy,那么我们在自定义set方法的时候一定要拷贝对象,否则会误导使用者,产生BUG。
3.方法名可以指定存取方法的方法名,但是一般很少用setter=
4.如果想在其他方法里设置属性值,同样要遵守属性定义中的特质,例如我们自定义了一个类,并自定义了一个初始化方法:
@interface aPeopleClass : NSObject
@property(copy) NSString *firstName;
@property(copy) NSString *lastName;
-(id)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName;
@end
在实现这个自定义初始化方法的时候一定要遵循“copy”语义,初始化方法可以这样写:
-(id)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName{
if (self = [super init]){
_firstName = [firstName copy];
_lastName = [lastName copy];
}
}
注意:在init和dealloc中不应该用self.调用set或get方法
另外,写好一个属性的这些特质,可以让我们易于理解这个属性,使开发更简单。
7. 对象内部应尽量直接访问实力变量
在对象外部访问实例变量,总是通过属性,但是在内部访问实力变量的时候该通过什么方法,总是在争论,书中的建议是,读取实力变量的时候用直接访问的形式,设置实力变量的时候通过属性来做。
两种方法区别:
1.直接访问速度比属性访问快,因为是直接访问内存,不经过方法。
2.直接访问不会触发“键值观测(KVO)”。
3.通过属性访问有助于排查问题,因为可以在设置方法中添加断点。
需要注意的点:
1.和前面提到的一样,在初始化方法中总应该直接访问实例变量,因为子类可能会覆写设置方法(有时我们可能在初始化方法中不得不采用属性访问,比如待初始化的实力变量是在父类中声明的)。
2.当我们使用“惰性初始化”的时候,必须通过设置方法来访问属性,否则实力变量永远不会初始化(“惰性初始化”的好处是节省内存)。
8. 理解“对象等同性”这一概念
等同性就是相等,我们在编写代码的时候经常会用到。
首先就要强调的一点就是==
不能用作对象相等的判断,因为==
比较的是指针本身,而不是指针指的对象。我们常用的比较相等的方法有:
对象类型 | 方法 |
---|---|
NSString | isEqualToString: |
NSArray | isEqualToArray: |
NSDictionary | isEqualToDictionary: |
NSSet | isEqualToSet: |
在NSObject协议中有两个用于判断相等的重要方法(这两个方法在自定义等同性方法的时候非常有用):
- (BOOL)isEqual:(id)object;
- (NSUInteger)hash;
下面isEqualToString:
和isEqual:
的区别去就是isEqual:
先判断对象类型,然后判断对象中的具体细节是否相等。另外,如果用isEqual:
判断两个对象相等的条件是,若两个对象相等,那么其hash方法必须返回用一个值,反之,hash值相同的两个对象,isEqual:
方法未必会认为两者相等。
下面举个特定类判断等同性的例子,首先编写一个简单的类:
@interface EOCPerson : NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, assign) NSUInteger age;
@end
然后自定义等同性方法
- (BOOL)isEqualToPerson:(EOCPerson*)otherPerson {
if (self == object) return YES;
if (![_firstName isEqualToString:otherPerson.firstName])
return NO;
if (![_lastName isEqualToString:otherPerson.lastName])
return NO;
if (_age != otherPerson.age)
return NO;
return YES;
}
- (BOOL)isEqual:(id)object {
if ([self class] == [object class]) {
return [self isEqualToPerson:(EOCPerson*)object];
} else {
return [super isEqual:object];
}
}
- (NSUInteger)hash {
NSUInteger firstNameHash = [_firstName hash];
NSUInteger lastNameHash = [_lastName hash];
NSUInteger ageHash = _age;
return firstNameHash ^ lastNameHash ^ ageHash;
}
如果经常需要判断等同性,那么自己创建的等同性判断方法,可以增加检测速度,另外这样写的代码更易懂。上面的代码中哈希码的生成算法是推荐的,这样的算法高效,并且生成的哈希码碰撞率低。
在自己写等同性判定方法的时候要注意下等同性判定的执行深度,听上去很吓人,实际很简单,就是我们判定等同性的标准,以及判定步骤,例如上面的自定义方法,先判定是不是同一种类型,然后在判定里面的属性是否相等,这样有级别的判定可以提高我们代码的效率,另外,自定义的判定标准是由我们自己决定,我们自己决定达到什么样的标准算相等,更利于我们开发。
容器中可变类的等同性,这点主要是针对容器中放入可变类而产生的问题,这个问题让我们遵守一个标准就是生成哈希码的时候,要根据类的不可变部分生成,举个例子说明(我们要在一个可变集合中添加几个可变数组):
NSMutableSet *aSet = [NSMutableSet new];
NSMutableArray *arrayA = [@[@1, @2] mutableCopy];
[aSet addObject:arrayA];
此时如果我们想向集合中再添加一个和arrayA一样的数组,由于集合的特性(集合中不会有重复的元素)
NSMutableArray *arrayB = [@[@1, @2] mutableCopy];
[aSet addObject:arrayB];
是不会添加成功的,这时我们换一种方法,先填一个可变数组arrayC,再将arrayC变成和arrayA一样的数组
NSMutableArray *arrayC = [@[@1] mutableCopy];
[aSet addObject:arrayC];
//
[arrayC addObject:@2];
这个时候我们发现,集合中竟然有两个一样的数组了!
但是当我们拷贝这个集合的时候,集合中的数组又变成一个了,这种结果是我们不想看到的。
9. 以“类族模式”隐藏实现细节
很多人听过类族,并且也在天天用类族,但是可能没有意识到这一点,并且自己也没有写过,其实造成这种情况原因有很多,第一,类族表现形式不明显,我们可能不会在意,第二,我们在自己写程序的时候没有提前做好规划,没有建立起一整套东西,所以也就不会考虑类族这个东西。
解释一下类族,每一个类族都有一个基类和一些子类,基类定义了基本的功能和属性,而子类是继承于这个基类的,但是每一个子类又有各自不同的形态,这些基类定义的功能和属性是在子类自己内部实现的,而接口是基类暴露出来的,所以我们在调用基类接口的时候根本不知道子类具体是怎么实现的,只要知道每一个子类独特的功能即可,类族有一个很显著的特点就是,调用基类方法返回的实例,不是基类的实例,而是子类的实例,可能理解起来费劲,结合代码:
定义一个基类:
typedef NS_ENUM(NSInteger, EOCEmployeeType){
EOCEmployeeTypeDeveloper,
EOCEmployeeTypeDesigner,
EOCEmployeeTypeFinance,
}; // 这个基类下面有三种类型的子类
@interface EOCEmployee : NSObject
// 公共属性
@property (copy) NSString *name;
@property NSUInteger salary;
// 公共方法
+ (EOCEmployee *)employeeWithType:(EOCEmployeeType)type;
- (void)doADayWork;
@end
基类的实现:
@implementation EOCEmployee
+ (EOCEmployee *)employeeWithType:(EOCEmployeeType)type{
switch (type) {
case EOCEmployeeTypeDeveloper:
return [EOCEmployeeTypeDeveloper new];
break;
case EOCEmployeeTypeDesigner:
return [EOCEmployeeTypeDesigner new];
break;
case EOCEmployeeTypeFinance:
return [EOCEmployeeTypeFinance new];
break;
default:
break;
}
// 返回的是子类的类型
}
- (void)doADayWork{
// 由子类实现具体工作
}
@end
定义其中的一个子类:
@interface EOCEmployeeTypeDeveloper : EOCEmployee
@end
子类的实现:
@implementation EOCEmployeeTypeDeveloper
- (void)doADayWork{
// 子类内部自己实现
}
@end
上面就是一个自定义类族的例子,其实在我们用的系统框架中有许多类族,比如创建UIButton的类方法
UIButton *aButton = [UIButton buttonWithType:UIButtonTypeCustom];
如果想自己写好类族,要找一些好的例子,多模仿,多练习。
至于向原有类族中增加实体子类,我们务必遵循下面的原则:
1.子类应该继承自类族中的基类。
2.子类应该定义自己的数据存储方法,这一点是最重要的,也是最容易忽略的(我们不能期望父类帮我们存储数据,因为父类本身只是个外壳,没有实例)。
3.子类要覆写父类中必须覆写的方法。