代码改变世界

Objective-C 2.0 对象的初始化以及实例变量的作用域

2012-08-13 16:53  java环境变量  阅读(281)  评论(0编辑  收藏  举报


 7,对象的初始化以及实例变量的作用域

本系列讲座有着很强的前后相关性,如果你是第一次阅读本篇文章,为了更好的理解本章内容,笔者建议你最好从本系列讲座的第1章开始阅读,请点击这里 

到目前为止,我们都使用的是下列方式创建对象

[类名 new]; 

这种new的方式,实际上是一种简化的方式。笔者在这里总结一下前面几章里面曾经提到过关于创建对象的2个步骤:

第一步是为对象分配内存也就是我们所说的allocation,runtime会根据我们创建的类的信息来决定为对象分配多少内存。类的信息都保存在Class里面,runtime读取Class的信息,知道了各个实例变量的类型,大小,以及他们的在内存里面的位置偏移,就会很容易的计算出需要的内存的大小。分配内存完成之后,实际上对象里面的isa也就被初始化了,isa指向这个类的Class。类里面的各个实例变量,包括他们的超类里面的实例变量的值都设定为零。

需要注意的是,分配内存的时候,不需要给方法分配内存的,在程序模块整体执行的时候方法部分就作为代码段的内容被放到了内存当中。对象的内容被放到了数据段当中,编译好的方法的汇编代码被放到了代码段当中。在Objective C里面,分配内存使用下列格式:

id 对象名=[类名 alloc];

NSObject已经为我们提供了诸如计算内存空间大小以及初始化isa还有把各个实例变量清零,毫无疑问NSObject已经非常出色的完成了内存分配的工作,在一般情况下,我们不需要重写alloc方法。 

第二步是要对内存进行初始化也就是我们所说的Initialization。 初始化指的是对实例变量的初始化。虽然在alloc方法里面已经把各个实例变量给清零了,但是在很多情况下,我们的实例变量不能是零(对于指针的实例变量而言,就是空指针)的,这样就需要我们对实例变量进行有意义的初始化。

按照Objective-C的约定,当初始化的时候不需要参数的话,就直接使用init方法来初始化:

[对象名字 init];

init是一个定义在NSObject里面的一个方法,NSObject明显无法预测到派生类的实例变量是什么,所以同学们在自己的类里面需要重载一下init方法,在init方法里面把实例变量进行初始化。

但是,需要强调的是,由于某种原因我们的init也许失败了,比如说我们需要读取CNBLOGS.COM的某个RSS,用这个RSS来初始化我们的对象,但是由于用户的网络连接失败所以我们的init也许会失败,在手机应用当中的一些极端的情况下比如说有同学写一个读取网页内容的程序,在网页内容非常大的时候,那么alloc也有可能会失败,为了可以方便的捕获这些失败,所以我们在程序当中需要把上面的过程写在一起:

id 对象名 = [[类名 alloc] init];
if (对象名)

else

加上了上面的if语句我们的初始化过程就是完美的,当然我们有的时候不需要这个if语句。当我们的alloc和init永远不会失败的时候。关于初始化的时候的错误捕获,笔者将在后面的章节里面论述。

为了我们写程序方便和简洁,在创建一个从NSObject派生的类的对象的时候,苹果公司把alloc和init简化成为new,我们在程序代码当中使用任何一种方式都是可以的,具体怎么写是同学们的喜好和自由。

到这里,有同学会问,如果我们的init需要参数怎么办?按照Objective-C的约定,我们需要使用initWith...。也就是带参数的变量初始化,这个也是本章的主要内容。

本章在讲述initWith的同时,也将会顺便的给大家介绍一下实例变量的作用域。

7.1,本章程序的执行结果

在本章里面,我们将要继续使用我们在第4章已经构筑好的类Cattle和Bull。从一般的面向对象的角度上来说,是不鼓励我们改写已经生效的代码的。但是本章的目的是为了使同学们可以很好的理解主题,所以笔者在这里暂时违反一下规则改写了一下Cattle类,在里面追加了initWith方法,笔者也在Cattle类里面追加了一些实例变量为了阐述实例变量的作用域的问题。由于在Cattle类里面笔者追加了一些东西,所以在Bull类里面改写了saySomething这个函数,让我们的Bull可以说更多的内容。我们的redBull是这样说的:

 

图7-1,本章程序的执行结果

本章程序代码晴点击这里下载。  

再次强调在实际的编程过程中,尤其是写大型程序多人合作的时候,除非发现BUG,否则不要改写已经生效的代码。这样会产生一些意想不到的结果,从而使其他的弟兄们或者姐妹们对你充满怨言。

 7.2,实现步骤

第一步,按照我们在第2章所述的方法,新建一个项目,项目的名字叫做07-InitWithAndIvarScope。如果你是第一次看本篇文章,请到这里参看第二章的内容。

第二步,按照我们在第4章的4.2节的第二,三,四步所述的方法,把在第4章已经使用过的“Cattle.h”,“Cattle.m”,“Bull.h”还有“Bull.m”, 导入本章的项目里面。然后把第6章里面的“MyNSObject.h”也导入到项目当中。

第三步,打开“Cattle.h”,修改成为下面的代码并且保存:

复制代码
#import <Foundation/Foundation.h>


@interface Cattle : NSObject {
    
int legsCount;
    @private
    
bool gender;    //male = YES female = NO
    @protected
    
int eyesCount;
    @public
    NSString 
*masterName;
}
- (void)saySomething;
- (void)setLegsCount:(int) count;
- (id)initWithLegsCount:(int) theLegsCount 
                   gender:(
bool) theGender 
                eyesCount:(
int) theEyesCount 
               masterName:(NSString
*)theMasterName;
@end

复制代码

第4步,打开“Cattle.m”,修改成下面的代码并且保存:

复制代码
#import "Cattle.h"

@implementation Cattle
-(void) saySomething
{
    NSLog(
@"Hello, I am a cattle, I have %d legs.", legsCount);
}
-(void) setLegsCount:(int) count
{
    legsCount 
= count;
}
-(id)init
{
    [super init];
    
return [self initWithLegsCount:4 
                            gender:YES 
                         eyesCount:
2 
                        masterName:
@"somebody"];
}
- (id)initWithLegsCount:(int) theLegsCount 
                   gender:(
bool) theGender 
                eyesCount:(
int) theEyesCount 
               masterName:(NSString
*)theMasterName
{
    legsCount 
= theLegsCount;
    gender 
= theGender;
    eyesCount 
= theEyesCount;
    masterName 
= theMasterName;
    
return self;
}
@end
复制代码

第五步,打开“Bull.m”, ,修改成下面的代码并且保存:

复制代码
#import "Bull.h"

@implementation Bull
-(void) saySomething
{
    NSLog(
@"Hello, I am a %@ bull, I have %d legs.", [self getSkinColor],legsCount);
    NSLog(
@"I have %d eyes, my master is %@.", eyesCount,masterName);
    
//List below is illegal
    
//NSLog(@"My gender is %@",gender ? @"male" : @"female");
}
-(NSString*) getSkinColor
{
    
return skinColor;
}
- (void) setSkinColor:(NSString *) color
{
    skinColor 
= color;
}
@end
复制代码

第六步,打开“07-InitWithAndIvarScope.m”,修改成下面的代码并且保存:

复制代码
#import <Foundation/Foundation.h>
#import 
"Bull.h"
#import 
"Cattle.h"
#import 
"MyNSObject.h"

int main (int argc, const char * argv[]) {
    NSAutoreleasePool 
* pool = [[NSAutoreleasePool alloc] init];
    Bull 
*redBull = [[Bull alloc] initWithLegsCount:4 
                                             gender:YES 
                                          eyesCount:
2 
                                         masterName:
@"that cowboy"];
    [redBull setSkinColor:
@"red"];
    [redBull saySomething];
    
    
//legal, but not good
    redBull->masterName = @"that cowgirl";
    
//legal, but bad
    
//redBull->eyesCount = 3;
    
    
//Trying to access a private ivar, VERY bad thing
    
//MyClass bullClass = redBull->isa;
    bool *redBullGender = (bool *)(redBull) + 8;
    NSLog(
@"My gender is %@",*redBullGender ? @"male" : @"female");
    
    [pool drain];
    
return 0;
}
复制代码

第七步,选择屏幕上方菜单里面的“Run”,然后选择“Console”,打开了Console对话框之后,选择对话框上部中央的“Build and Go”,如果不出什么意外的话,那么应该出现入图7-1所示的结果。如果出现了什么意外导致错误的话,那么请仔细检查一下你的代码。如果经过仔细检查发现 还是不能执行的话,可以到这里下载笔者为同学们准备的代码。 如果笔者的代码还是不能执行的话,请告知笔者。 

7.3,实例变量的作用域(Scope) 

对于Objective-C里面的类的实例变量而言,在编译器的范围里面,是有作用域的。和其他的语言一样,Objective-C也支持public,private还有protected作用域限定。

如果一个实例变量没有任何的作用域限定的话,那么缺省就是protected。

如果一个实例变量适用于public作用域限定,那么这个实例变量对于这个类的派生类,还有类外的访问都是允许的。

如果一个实例变量适用于private作用域限定,那么仅仅在这个类里面才可以访问这个变量。

如果一个实例变量适用于protected作用域限定,那么在这个类里面和这个类的派生类里面可以访问这个变量,在类外的访问是不推荐的。

我们来看看“Cattle.h”的代码片断:

复制代码
1     int legsCount;
2     @private
3     bool gender;    //male = YES female = NO
4     @protected
5     int eyesCount;
6     @public
7     NSString *masterName;
复制代码

第一行的legsCount的前面没有任何作用域限定,那么它就是protected的。

第二行是在说从第二行开始的实例变量的定义为private的,和其他的关键字一样,Objective-C使用@来进行编译导向。

第三行的gender的作用域限定是private的,所以它适用于private作用域限定。

第四行是在说从第四行开始的实例变量的定义为protected的,同时第二行的private的声明作废。

第五行的eyesCount的作用域限定是protected的,所以它适用于protected作用域限定。

第六行是再说从第六行开始的实例变量的定义为public的,同时第四行的protected的声明作废。

第七行的masterName的作用域限定是public的,所以它适用于public作用域限定。

我们再来看看在派生类当中,private,protected还有public的表现。Bull类继承了Cattle类,笔者改写了一下“Bull.m”用来说明作用域的问题,请参看下面的代码:

复制代码
1 -(void) saySomething
2 {
3     NSLog(@"Hello, I am a %@ bull, I have %d legs.", [self getSkinColor],legsCount);
4     NSLog(@"I have %d eyes, my master is %@.", eyesCount,masterName);
5     //List below is illegal
6     //NSLog(@"My gender is %@",gender ? @"male" : @"female");
7 }
复制代码

我们来看看第3还有第4行代码,我们可以访问legsCount,eyesCount还有masterName。

在第6行代码当中,我们试图访问gender这个Cattle的私有(private)属性,这行代码产生了编译错误,所以我们不得不注释掉第6行代码。

好的,我们再来看看类的外部private,protected还有public的表现。请同学们打开“07-InitWithAndIvarScope.m”,参考一下下面的代码:

复制代码
1     //legal, but not good
2     redBull->masterName = @"that cowgirl";
3     //legal, but bad
4     //redBull->eyesCount = 3;
5     
6     //Trying to access a private ivar, VERY bad thing
7     //MyClass bullClass = redBull->isa;
8     bool *redBullGender = (bool *)(redBull) + 8;
9     NSLog(@"My gender is %@",*redBullGender ? @"male" : @"female");
复制代码

在第二行里面,我们访问了masterName,由于在Cattle里面masterName是public的,Bull继承了Cattle,所以我们可以直接访问masterName。但是这不是一种好的习惯,因为这不符合面向对象的基本思想。实际上,如果没有特殊的理由,我们不需要使用public的。

第四行,我们试图在类的外边访问protected变量eyesCount,在这里笔者的Xcode只是轻轻的给了一个警告,编译成功并且可以运行。同样,这种在类的外边访问类的protected变量是一个很糟糕的做法。

我们还记得在Bull的saySomething里面我们曾经试图访问过gender,但是编译器无情的阻止了我们,因为gender是私有的。但是,这仅仅是编译器阻止了我们,当我们有足够的理由需要在类的外边访问private实例变量的时候,我们还是可以通过一些强硬的方法合法的访问私有变量的,我们的方法就是使用指针偏移。

我们首先回忆一下第6章 的6.6节的内容,isa里面保存了对象里面的实例变量相对于对象首地址的偏移量,我们得到了这个偏移量之后就可以根据对象的地址来获得我们所需要的实例变量的地址。在正常情况下,我们需要通过访问类本身和它的超类的ivars来获得偏移量的,但是笔者在这里偷了一个懒,先使用第七行的代码MyClass bullClass = redBull->isa;通过Debugger获得gender的偏移量,数值为8。然后在第8行里面,笔者通过使用指针偏移取得了gender的指针然后在第9行实现了输出。

由此可见,在Objective-C里面, 所谓的private还有protected只是一个Objective-C强烈推荐的一个规则,我们需要按照这个规则来编写代码,但是如果我们违反了这个规则,编译器没有任何方法阻止我们。

笔者认为在类的外部直接访问任何实例变量,不管这个实例变量是public,private还是protected都是一个糟糕的做法,这样会明显的破坏封装的效果,尽管这样对编译器来说是合法的。

7.4,initWith...

NSObject为我们准备的不带任何参数的init,我们的类里面没有实例变量,或者实例变量可以都是零的时候,我们可以使用NSObject为我们准备的缺省的init。当我们的实例变量不能为零,并且这些实例变量的初始值可以在类的初始化的时候就可以确定的话,我们可以重写init,并且在里面为实例变量初始化。

但是在很多时候, 我们无法预测类的初始化的时候的实例变量的初始值,同时NSObject明显无法预测到我们需要什么样的初始值,所以我们需要自己初始化类的实例变量。

请同学们打开“Cattle.m”,我们参考一下下面的代码:

复制代码
 1 -(id)init
 2 {
 3     [super init];
 4     return [self initWithLegsCount:4 
 5                             gender:YES 
 6                          eyesCount:2 
 7                         masterName:@"somebody"];
 8 }
 9 - (id)initWithLegsCount:(int) theLegsCount 
10                    gender:(bool) theGender 
11                 eyesCount:(int) theEyesCount 
12                masterName:(NSString*)theMasterName
13 {
14     legsCount = theLegsCount;
15     gender = theGender;
16     eyesCount = theEyesCount;
17     masterName = theMasterName;
18     return self;
19 }
复制代码

从第3行到第7行,笔者重写了一下init。在init里面,笔者给通过调用initWith,给类的各个实例变量加上了初始值。这样写是很必要的,因为将来的某个时候,也许有人(或者是自己)很冒失的使用init来初始化对象,然后就尝试使用这个对象,如果我们没有重写init,那么也许会出现一些意想不到的事情。

从第9行到第19行,是我们自己定义的initWith,代码比较简单,笔者就不在这里赘述了。需要注意的一点是,笔者没有在这里调用[super init];。原因是Cattle的超类就是NSObject,初始化的过程就是初始化实例变量的过程,runtime已经为我们初始化好了NSObject的唯一实例变量isa,也就是Cattle的类的信息,所以我们不需要调用[super init];。在某些时候,超类的变量需要初始化的时候,请同学们在子类的init或者initWith里面调用[super init];

请同学们再次打开“07-InitWithAndIvarScope.m”,参考下面的代码片断:

复制代码
1     Bull *redBull = [[Bull alloc] initWithLegsCount:4 
2                                              gender:YES 
3                                           eyesCount:2 
4                                          masterName:@"that cowboy"];
5     [redBull setSkinColor:@"red"];
6     [redBull saySomething];
复制代码

从第1行到第4行就是调用的initWith来初始化我们的redBull。

7.5,本章总结

非常感谢大家对笔者的支持!

我们在本章里面介绍了2个比较轻松的话题,一个是实例变量的作用域,这个概念笔者个人认为对有一点面向对象编程经验的人来说,不是什么新鲜的概念了。但是需要注意的是,Objective-C并没有强制我们遵守它的规则,他仍旧为我们提供了违反规则的机会,这一点上根C++比较类似。只要支持指针,就无法避免使用者违反规则。事务都是一分为二的,当我们得到了访问任何变量的自由之后,我们必须为访问这些变量承担后果。

第二个话题就是initWith。和其他的面向对象的语言不同,Objective-C没有构造函数,它通过init还有initWith来初始化变量, 我们应该根据具体情况进行具体的分析,从而编写我们的init还有initWith方法。