第三部分
- 向对象发送消息时,如何选择正确的方法。
- #import与@class指令的区别
- 多态
- @try处理异常
- 重载类的初始化方法
- 关于属性、存取方法和实例变量以及@synthesize
向对象发送消息时,如何选择正确的方法。
首先,检查对象所属的类,查看在类中是否明确定义了一个具有指定名称的方法。如果有的话,就使用这个方法;如果没有,就寻找它的父类是否有这个方法,如果还没有,就一直寻找其父类。
#import与@class指令的区别
iOS---Objective-C中@class与#import的区别
iOS @class 和#import“”引用
#import会包含类的所有信息,包括方法和实例变量。而@class只是告诉编译器,其后面声明的名称是类的名称,至于这些类是如何定义的,暂时不用考虑。(@class指令是让编译器知道这是一个类名,并不需要知道这个类的方法和属性。但如果要用到这个类的方法和属性时,还是需要导入头文件#import"")
一般来说,@class是放在interface中的,只是为了在interface中引用这个类,把这个类作为一个类型来用的。 在实现这个接口的实现类中,如果需要引用这个类的实体变量或者方法之类的,还是需要import在@class中声明的类进来。
通常, 使用#import引入一个类的头文件, 编译时会将该类的头文件中的所有信息都引入, 包含属性和方法, 但有时候却不需要这么多, 且包含这些信息会降低编译性能。而@class就表示这只是一个类而已, 我们所关心的仅此一点, 而不需要知道该类的内部有哪些属性和方法. 这种情况下, 我们在.h头文件中就可以仅仅使用@class, 以提升编译性能. 而在.m实现文件中, 往往就需要知道类的内部信息了, 这时就需使用#import来引入这些信息。
编译性能的考虑:在编译效率方面考虑,如果你有100个头文件都#import了同一个头文件,或者这些文件是依次引用的,如A–>B,B–>C,C–>D这样的引用关系。当最开始的那个头文件有变化的话,后面所有引用它的类都需要重新编译,如果你的类有很多的话,这将耗费大量的时间。而是用@class则不会。(关于编译性能, 还有一点需要补充: 若类文件依次使用#import来引用, 则最开始的头文件有变化,则后续所有引用它的类都需要重新编译。而@class则不会有这个问题。)
#import会导致循环引用:如果有循环依赖关系,如:A–>B,B–>A这样的相互依赖关系,如果使用#import来相互包含,那么就会出现编译错误,如果使用@class在两个类的头文件中相互声明,则不会有编译错误出现。(另外, 以#import的方式引入一个类, 有可能造成两个类之间相互引入对方的头文件, 造成递归引用。这一点目前在xcode中不会再报错误了, 但依然是存在的。)
@class提高编译效率,避免循环引用。
#import能够保证头文件只被引入一次, 而#include则可能会出现重复引入.
#import
@class XYPoint;
@interface Kaobei : NSObject
@property (strong,nonatomic) XYPoint *origin;
- (XYPoint *)origin;
- (void)setOrigin:(XYPoint *)pt;
@end
多态
多态的定义:不同的类共享相同方法名称的能力成为多态。就是虽然不同的类里面的方法名是相同的,但是系统在运行时,会先确定对象是哪一个类,再去调用类的方法。
id是一个通用的对象类型。也就是说,id可以用来存储属于任何类的对象。id后面的类没有星号。OC系统总是跟踪对象所属的类。动态类型和动态绑定:就是说,先判定对象所属的类,然后在运行时确定需要动态调用的方法,而不是在编译的时候。
id a;//动态类型
Fraction *a;//静态类型
@try处理异常
- @try块里可以正常执行语句,但是如果在执行过程中出现了异常,就会跳到@catch块中处理异常。无论@try是否出现了异常,@finally块都会被执行。
- @throw 可以抛出异常。
@try{
}
@catch(NSException *e){
}
@finally{
}
重载类的初始化方法
如果希望类对象在初始化的时候做一些事情,比如给实例变量赋值。我们可以重写init方法。也可以另写一个以init开头的初始化方法。
- (instancetype)init{
self = [super init];
if(self){
//初始化代码
}
return self;
}
初始化代码解释:
这个方法首先会调用父类的初始化方法。执行父类的初始化方法,可以使继承的实例变量正常初始化。
必须将父类init方法的执行结果赋值给self(self = [super init];),因为初始化过程改变了对象在内存中的位置(意味着引用将要改变)。
如果父类的初始化成功,那么返回的值是非空的,通过if语句可以验证。注释说明可以在这个代码块的位置放入自定义的初始化代码。通常可以在这个位置初始化实例变量。
关于属性、存取方法和实例变量以及@synthesize
(《OC程序设计》P203)
目前的趋势是使用下划线(_)作为实例变量的起始字符。
@synthesize window = _window;
@synthesize window = _window;表明合成(synthesize)属性window的取值方法和设置方法,并将属性与实例变量(_window)关联起来。这对区分属性和实例变量是有区别的。鼓励通过摄者方法来设置实例变量的值,通过取值方法来获取实例变量。
[window makeKeyAndVisible];//无法运行
无法运行,因为没有实例变量叫做window。
[_window makeKeyAndVisible];//可以运行
正常运行,或者使用点方法:
[self.window makeKeyAndVisible];//可以运行
在实现部分(@implementation)显式的声明实例变量(或者使用@synthesize指令隐形声明的实例变量)是私有的,这就意味着并不能在子类中通过名字直接获取到实例变量。在子类中,只能使用继承的存取方法获取实例变量的值。
基于它们的属性(我理解为基于属性的修饰符),合成方法(@synthesize的方法?)还能做其它的事情,例如,管理内存、复制值等,给实例变量赋值或从实例变量取值就不会做这些事情。这是属性和实例变量的另一个抽象,这种抽象使得当存取实例变量时系统有机会做其它工作。
全局变量、外部全局变量和静态变量
全局变量、外部全局变量
int gGlobalVar = 0;
上面定义了一个全局变量,可以在这个文件的任何位置使用它。
其实这样定义一个全局变量,在其它文件中也可以使用它。所以,上面实际上定义的是一个外部全局变量。
想要在其它文件中是有上面的全局变量,需要在其它文件中这样定义:
extern int gGlobalVar;
这样就能够在其它文件中使用最上面定义的全局变量了。
注意,不能写作 extern int gGlobalVar = 0; 因为使用关键字extern表明这条语句是变量的声明,而不是定义。记住,声明不会引起变量存储空间的分配,二定义会引起变量存储空间的分配。
static关键字
当我们不希望其它文件使用全局变量时,要使用static关键字。
static int gGlobalVar;
这样,在这条语句之后的方法或函数都能够使用这个变量,而其他文件是不能够使用这个变量的。
我们知道,类方法是不能够访问实例变量的,但有时候又希望类方法可以设定和访问一些变量。所以我们可以声明一些静态变量(外界无法访问,保证了封闭性)。
第四部分
- 枚举数据类型
- typedef语句
- 一次求反运算符
枚举数据类型
枚举类型的定义规则:以enmu开头;之后是枚举类型数据的名称;然后是标识符序列,定义了所有允许给这个类型指派的值(包含在一对花括号内)。
理论上,枚举类型只能赋的值只能为花括号内的标识符序列,但就算违背了这个规则,OC编译器也不会发出警告消息。
- 定义一个枚举类型flag:
enmu flag { false , true };
- 声明两个enum flag类型的变量:
enmu flag end , start;
- 给声明的变量赋值或判断变量的值:
end = true;
if(start ** true)
- 如果希望一个枚举标识符对应一个特定的整数值,那么可以在定义数据类型时给这个标识符指定整数值。列表中随后出现的枚举标识符会被依次赋以整数值,从指定的整数值加1开始:
enmu direction {up,down,left = 10,right};
上面定义了一个枚举数据类型direction。因为up位于序列首位,所以编译器给它赋值为0;down接着up,因此,赋给它的值为1;left指定了值10;所以,right的值为11。
- 枚举标识符可以共享相同的整数值。
enmu boolean {no = 0, false = 0, yes = 1 , true = 1};
- OC编译器实际上将枚举标识符作为整型常量来处理
enmu boolean {no = 0, false = 0, yes = 1 , true = 1};
enmu boolean flag;
flag = yes;
在这里,枚举类型变量flag实际上被是赋值为1,而不是yes这个名字。
- 如果要给枚举类型变量赋予一个整数值,应该使用类型转换运算符。但是就算不使用,编译器也不会有异议。
enmu boolean {no = 0, false = 0, yes = 1 , true = 1};
enmu boolean flag;
flag = (enmu boolean)1;
flag = (enmu boolean)(yes - 1);
枚举类型提供了一种方法,是你能把整数值和有象征意义的名称对应起来。尽量不要依赖枚举值被作为整数这个事实。
定义枚举类型时,可以将一个整数类型和枚举名称对应起来
enmu direction:unsinged short int {up,down,left = 10,right};
- 定义枚举类型时,可以将数据类型的名称省略。
enmu {up,down,left = 10,right} direction;
注意,这里定义的是一个未命名的枚举类型,direction是该枚举类型的变量,而不是数据类型名称。
typedef语句
typedef:为数据类型另外指派一个名称。
使用typedef定义一个一个新类型名,可按照下面步骤:
- 像声明所需类型的变量那样编写一条语句。 int a;
- 通常应该出现声明变量名的地方,将其替换为新的类型名;int Counter;
- 在语句前面加上关键字typedef。
typedef int Counter;
typedef NSNumber *NumberObject;//注意星号
type enum Direction {up,down,left,right} Direction;//为enum Direction指定名称Direction
一次求反运算符
一次求反运算符:~。作用为对数字进行位反转。
例子:如果想对一个数的二进制的最低位设置为0,可以使用求反运算符。
w &= ~1;
这样可以不用保证int类型在不同系统上的不同位数。
第五部分
- 扩展类定义的几种方式
- category(分类)
- 协议和代理
扩展类定义的几种方式
子类、分类、协议
category(分类)
分类提供了一种简单的方式,用它可以将类的定义模块化到相关方法的组或类中。它还提供了扩展现有类定义的简单方式,并且不必访问源码,也无需创建子类。
下面是UIAlertController的一个分类:
#import "Fraction.h"
@interface Fraction (test)
- (void)add:(int)a;
- (void)mul:(int)a;
@end
@interface Fraction (test):告诉编译器你正在为Fraction定义新的分类,而且它的名称为test。注意,此处没有列出Fraction的父类,因为编译器已经从Fraction.h知道了这个内容。并且,你没有向编译器告知实例变量,因为以前定义的接口部分已经这样做了。实际上,如果尝试列出父类或实例变量,将会受到编译器发出的语法错误。
总结:分类不能写父类和实例变量,因为编译器已经通过导入的类知道了这些内容。
可以将所有方法的定义放在一个实现部分(Fraction.m或者Fraction+Test.m)中,也可以分开放。
分类的.h和.m文件基本名称是由类的名称紧接着分类的名称:FractionTest.h和FractionTest.m,或者可以使用加号:Fraction+Test.h和Fraction+Test.m
分类的注意事项
分类可以覆写该类中的一个方法,但不建议这么做。因为覆写这个方法后,再也不能访问原来的方法。因此,必须将原来覆写方法中的所有功能复制到替换的方法里。
如果非要覆写方法,正确的选择应该是创建子类。子类可以通过super调用父类的方法,并添加上子类自己的功能。
记住,通过使用分类添加新方法来扩展类不仅会影响类,同时也会影响它的所有子类。例如,如果为跟对象NSObject添加新方法,就存在潜在的危险性,因为每个NSObject的子类都将继承分类的方法,无论你是否愿意。
协议和代理
协议
- 协议是多个类共享的一个方法列表。
- 协议中列出的方法没有相应的实现,计划有其他人来实现。@protocol、@required
- 协议列出了一组方法,有些是可以选择实现的,有些是必须实现的。
- 注意,协议是无类的,它是无类的。和类名一样,协议名也必须是唯一的。
- 如何定义一个协议:
@protocol 协议名称
//声明一些协议方法
@required //在这之后都是必须实现的方法
@optional //在这之后都是可以选择实现的方法
@end
- 下面是在标准的Foundation头文件NSObject.h中定义NSCopying协议的方式:
//在NSObject.h中
@protocol NSCopying
- (id)copyWithZone:(NSZone *)zone;
@end
- 如何遵守协议,可以写在.h中,也可以在.m中的@interface后的<尖括号>里。
遵守NSCopying协议
//.h文件中
@interface test : NSObject
//.m文件中
@interface test ()
- 可以通过 conformsToProtocol: 判断一个对象是否遵守了某项协议。@protocol指令用于获取一个协议名称,并产生一个Protocol对象。
if([currentObject conformsToProtocol:@protocol(NSCopying)] ** YES)
可以通过 respondsToSelector: 判断一个对象是否实现了某个方法。
if([currentObject conformsToProtocol:@selector(copyWithZone:)] ** YES)
- 定义一个协议时,可以扩展现有协议的定义。
@protocol test //说明test协议也遵守了test11协议
代理
定义了协议的类可以看作是将协议定义的方法代理给了实现它们的类。具体的动作由代理类来承担,响应某些事件或者定义某些参数。