再谈 Objective-C 中的 init 和属性 | Soledad 

JerryXia 发表于 , 阅读 (0)
介绍

这篇文章我们来谈论一些 iOS 面试中常问的 init 和属性相关问题。

init

NSObject 类有一个名为 init 的方法,示例代码如下:

1
NSMutableArray *things = [[NSMutableArray alloc] init];

其中,alloc 负责分配对象空间,init 负责初始化对象。请注意,init 是实例方法,返回的是初始化后的对象地址。

一般的,很多自己定义的类没有实现 init 方法,所以会执行由 NSObject 定义的 init 方法。这样,所有的实例变量都会被初始化为 0(其中,指针对象的值将是 nil )。

然而,很多情况下我们是需要将实例变量初始化为非 0 的值。我们可以在类中的 .m 文件中,实现新的 init 方法,以覆盖 NSObject 的版本。例:

1
2
3
4
5
6
7
8
- (instancetype)init
{
self = [super init];
if(self){
_voltage = 120;
}
return self;
}

这个 init 方法会返回一个 instancetype 类型的值,instancetype 这个关键字会告诉编译器方法返回什么类型的对象。你编写的或是覆盖的任何初始化方法都应该返回 instancetype 类型的值

(在 Xcode4.3 以前,初始化方法返回的是 id 类型,也可以,但是现在使用 instancetype 是更好的选择)

带实参的 init 方法

有些情况下,调用方必须为初始化方法提供额外的信息,才能正确地初始化对象。例如,我们在 .h 文件中声明一个新方法:

1
-(instancetype)initWithProduceName:(NSString *)pn;

在 .m 文件中,找到 init 方法的实现代码,将方法名改成 initWithProductName:,并将传入的实参赋给 _productName ,如下:

1
2
3
4
5
6
7
8
9
-(instancetype)initWithProductName:(NSString *)pn
{
self = [super init];
if (self){
_produceName = [pn copy];
_voltage = 120;
}
return self;
}

通过这样做,会有不明就里的程序员依然通过 init 方法来初始化,但是那样并不能满足我们要的结果,解决方案很简单。在 .m 文件中,增加一个 init 方法,传入一个默认值:

1
2
3
4
-(instancetype)init
{
return [self initWithProductName:@"Unknown"];
}

综合来说,每个类都有一个初始化方法被称为指定初始化方法,指定初始化方法扮演的是单一入口的角色。任何类都有且只有一个指令初始化方法。如果某个类还有其他初始化方法,那么这些方法应该(直接或间接地)调用指定初始化方法。
编写初始化方法时,应遵循以下原则:

  • 其他的初始化方法都应该(直接或间接地)调用指定初始化方法。
  • 指定初始化方法应该先调用父类的指定初始化方法,然后再对实例变量进行初始化。
  • 如果某个类的指定初始化方法和父类的不同(这里指的是方法名不同),就必须覆盖父类的指定初始化方法,并调用新的指定初始化方法。
  • 如果某个类有多个初始化方法,就应该在相应的头文件中明确的注明哪个方法是指定初始化方法。

属性

生命周期类型

生命周期类型(lifetime specifier)的特性包括: unsafe_unretained,assign,strong,weak 和 copy。这些特性决定了存方法将如何处理与其相关的内存管理问题。

assign 是默认的也是最简单的:存方法会将传入的值直接赋给实例变量。

strong 特性,要求保留传入的对象,并放弃原有对象。(如果原有对象不再有其他拥有方,就会被释放)。凡是指向对象的实例变量,通常都应该使用 strong 特性。
weak 特性要求不保留传入的对象,如果该对象被释放,那么相应的实例变量会被自动赋为 nil。
unsafe_unretained 特性和 weak 特性类似,要求不保留传入的对象,然后,如果该对象被释放,那么相应的实例变量不会被自动赋为 nil。

copy 特性要求拷贝传入的对象,并将新对象赋给实例变量。下面将对 copy 特性进行详细介绍。

copy

copy 特性要求拷贝传入的对象,并将新对象赋给实例变量。例如:

1
@property (copy) NSString *lastName;

以上的存方法等同于以下代码:

1
2
3
4
-(void)setLastName:(NSString *)d
{
lastName = [d copy];
}

再例如:

1
2
3
NSMutableString *x = [[NSMutableString alloc] initWithString:@"Ono"];
[myObj setLastName:x];
[x appendString:@"Lennon"];//由于 setLastName: 方法会拷贝传入的对象,所以修改 x 不会对实例变量产生影响

另外,大多数不是来自苹果公司的类并没有实现 copyWithZone:方法,因此调用 copy 方法会失败。

实现存取方法

编译器会默认为你声明的所有属性合成存取方法。通常来说,实现存取方法很简单,因此很适合交给编译器来做。

然而,有的时候你需要使用存取方法来处理一些特殊逻辑,这种情况下,就需要自己实现存取方法。

例如,当我们修改实例变量时,需要更新用户界面。

首先在头文件中声明:

1
@property (nonatomic,copy)NSString* currentState;

然后在实现文件中:

1
2
3
4
5
6
-(void)setCurrentState:(NSString *)currentState
{
_currentState = [currentState copy];
//以下是更新界面部分
...
}

编译器知道已经实现存方法之后,就不会再创建存方法,它只会创建取方法。
如果你声明一个属性,手动实现存取方法,编译器就不会合成实例变量。但是这时候我们一般还都需要实例变量,就必须自己创建。创建的方法是在类的实现文件中添加 @synthesize 指令。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#import "Badger.h"
@interface Badger:NSObject()
@property (nonatomic) Mushroom *mushroom;
@end
@implementation Badger;
@synthesize mushroom = _mushroom;//告诉编译器有一个叫做 _mushroom 的实例变量,它是 mushroom 和 setMushroom:的实例变量,如果它不存在,就要将它创建出来。
-(Mushroom *)mushroom
{
return _mushroom;
}
-(void)setMushroom:(Mushroom *)mush
{
_mushroom = mush;
}

这时候你可能会想,为什么还要声明属性呢?答案是:声明属性仍然是声明存取方法的快捷方法,它会给代码带来视觉上的连贯性。。。

联系我