前言
本篇文章介绍OC的基本概念
类
定义类
一个OC类包含两部分的定义
- @interface部分
- @implementation部分
@interface部分包含类的公开信息,即能和类的使用者共享的信息
@implementation部分包含类的私有信息,比如实例变量和代码
@interface部分
我们如果想定义一个类,首先得定义一个类名,然后需要告诉编译器该类的父类是什么,然后我们可能需要定义一些方法或者属性,来表明当前该类能够做什么。而这些东西就需要在@interface部分定义,下面给出@interface部分的语法
@interface 类名:父类名称
//这里边定义类的接口和属性
@end
@interface部分通常放在头文件里边,可以被多个文件引用
@implementation
@implementation部分通常放在.m文件里边,包含声明在@interface部分的函数的实现代码,并且可以定义类对象使用的变量,@implementation部分的语法如下:
@implementation 类名
{
//这里存放类对象的变量
}
//这里存放函数的实现
@end
创建一个类实例
创建一个类实例一般分为三步,比如创建类A的实例
A* a; //声明一个类对象
a=[A alloc]; //为类对象分配存储空间
a=[a init]; //初始化类对象
方法的定义
定义方法模版如下:
// 无参数的情况
// -:方法类型: 实例方法是-
// 类方法是+
// void:返回值类型
// methodName:方法名
-(void) methodName
// 一个参数的情况
// pType:参数类型
// pName:参数名称
-(void) methodName:(pType)pName
// 多个参数的情况
// 多个参数时,都是使用pDes:(pType)pName这种模式
// 依次往后排列
-(void) methodName:(pType)pName pDes:(pType)pName
类方法的调用
Objective-C采用特定的语法对类和实例应用方法
[ClassOrInstance method]
在这条语句中,左方括号后要紧跟类的名称或者该类的实例名称,它后面可以是一个或多个空格,空格后面是将要执行的方法。最后,使用右方括号和分号来终止。
类方法和实例方法
在OC类中,使用-
开头定义的方法称为实例方法,实例方法能够对类的实例执行一些操作。使用+
开头定义的方法称为类方法,类方法对类本身执行一些操作,比如创建类的实例。
方法参数
方法的实参其实是方法的局部变量
对于基本类型,作为方法参数,传递的是一个副本
但是对于对象,作为方法参数,传递的是对象的指针或者引用,所以可以在方法中修改传入对象的状态
基本数据类型
id数据类型可以存储任何类型的对象
注意:在NSLog方法中,打印int类型使用%i,打印id类型使用%p
如果在一组嵌套的循环内使用break;仅退出最内层的循环
实例变量
实例变量是OC类的基本成员,实例变量会在生成类实例的时候占用内存,这点和方法还不一样,在OC中直接定义实例变量的方法有两种:
- 在interface中定义
- 在implementation中定义
先看一下在interface中的定义:
@interface A : NSObject
{
int a1;
}
在implementation中定义:
@implementation A
{
int b;
}
两种定义方式都有两点需要注意:
- 实例变量的定义必须在大括号中。
- 实例变量定义时不能赋初值,默认值为0
两种定义的区别如下:
- interface中定义的实例变量可以被当前类和子类访问,就相当于C++中的protected访问修饰符一样
- implementation中定义的实例是私有的,只能在当前类内部访问。
- 两者定义的实例变量都不能被类实例的用户访问
属性
访问器
因为类的用户无法直接访问在类中定义的实例变量,为了实例变量对用户可写,需要定义设值方法
。为了实例变量对用户可读,需要定义取值方法
。这两类方法合称访问器。
属性的定义
如果我们在接口部分使用@property声明一个变量,这种变量用正式一点的描述叫做属性
,其实属性只是一个概念上的描述,编译器会把当前声明的属性变成一个实例变量的声明和两个方法
,一个设值方法,一个取值方法。
注意:属性只能够在接口部分定义
编译器对属性的处理
编译器自动处理的部分遵循下面的规则:
-
如果在实现部分没有使用
@synthesize 属性名
这种指令,编译器生成的实例变量的名称会在属性名称前面添加_ -
如果在实现部分使用
@synthesize 属性名
这种指令,编译器生成的实力变量和属性名称一样 -
如果编译器根据属性生成了一个实例变量,但是在实现部分又定义了一个同名的实例变量,则只会保留一个
看下面的代码@interface A : NSObject @property int numA; @property int numB; -(void) print; @end @implementation A int numA; int numB; @synthesize numA; -(void) print { numA = 40; numB = 30; [self setNumA:10]; [self setNumB:20]; NSLog(@"numA=%i, numB=%i",numA,numB); } @end #import <Foundation/Foundation.h> int main(int argc, const char * argv[]) { @autoreleasepool { A* a = [[A alloc] init]; [a print]; } return 0; }
因为在A的接口部分定义了属性numA和numB,但是在实现部分调用了@synthesize numA;所以:
- numA属性会生成实例变量numA和访问器方法
- numB属性会生成实例变量_numB和访问器方法
所以,在实现部分声明的实例变量numB和_numB没有任何关系,并且
[self setNumB:20]
设置的是_numB的值,所以最终的输出为:numA=10, numB=30
访问属性
OC允许使用.运算符快速访问属性,比如对于上面代码中的
[self setNumB:20];
可以写成
self.numB = 20;
注意:实例变量是不可以用点运算符来访问的
,否则,对于上面的代码,我们不确定是给实例变量numB赋值呢还是使用setNumB给实例变量_numB赋值,所以,对实例变量使用.运算符是不允许的。
实际上,使用点运算符访问属性底层会转换成对访问器的调用。
从编码规范的角度考虑,我们一般使用点运算符来访问属性,但是对于一般函数的访问还是会使用方括号的形式。
属性的注意事项
- 属性名称不要以new、alloc、copy、init这些词开头
- 在默认情况下,合成的设值函数只是简单的复制对象指针,而不是对象本身。
属性的总结
- 类对象的用户无法访问类的实例变量
- 因为类对象的用户无法访问类的实例变量,所以OC定义了属性,属性的目的是为了类对象用户访问类的实例变量
- 属性会在底层转化成实例变量和两个方法:设值方法和取值方法,这两个方法都是公开的,类对象的用户通过这两个方法访问实例变量
- 类对象的用户还可以通过点运算符+属性这种简单的方式来访问或者设置实例变量的值
动态绑定
id类型
id数据类型是一种通用的对象类型。也就是说,id可以用来存储属于任何类的对象。
注意:使用id来声明对象时不需要添加星号*
id的这个特性决定了id可以根据不同的情况指向不同的类型,然后执行具有相同函数名的函数,具体执行哪个类对象的函数要到运行期才能知道,id的这个特性之所以能够实现,是因为OC系统会一直跟踪对象所属的类。
下面用一个例子演示一下:
@interface A1 : NSObject
-(void)print;
@end
@interface A2 : NSObject
-(void)print;
@end
@interface A3 : NSObject
-(void)print;
@end
@implementation A1
-(void)print{NSLog(@"print A1");}
@end
@implementation A2
-(void)print{NSLog(@"print A2");}
@end
@implementation A3
-(void)print{NSLog(@"print A3");}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
int index;
id a;
while (true) {
printf("input 1,2 or 3:");
scanf("%i",&index);
switch(index)
{
case 1:
a = [[A1 alloc] init];
break;
case 2:
a = [[A2 alloc] init];
break;
case 3:
a = [[A3 alloc] init];
break;
default:
exit(0);
}
[a print];
}
}
return 0;
}
上述例子根据输入的值会调用不同类对象的print函数。
id类型的使用
- id类型可以调用的方法,必须是在id动态绑定的类对象中的某一个对象定义的方法,就像上面的例子,如果三个类A1、A2、A3都没有定义print,编译会报错。至少得有一个定义了print方法,编译就能通过,但是执行时如果找不到方法,会崩溃,所以,一定要确保id类型调用的方法,在各个类对象都有实现。
- 不能对id变量使用点运算符,否则编译器会报错
id类型和静态类型
首先,将一个变量定义为特定类的对象时,使用的是静态类型。“静态”指的是对存储在变量中对象的类型进行显式声明。这样,存储在这种形态中的对象的类是预定义的,也就是静态的。使用静态类型时,编译器尽可能确保变量的用法在程序中始终保持一致。编译器能够通过检查来确定应用于对象的方法是由 该类定的还是由该类继承的,否则它将显示警告消息。
然而,如果检查是在运行时执行的,为什么还要关心静态类型呢?
关心静态类型是因为它能更好地在程序编译阶段而不是在运行时指出错误。如果把它留到运行时,那么在错误发生时,你甚至都可能不在现场。假如程序投 入到生产环境,一些倒霉的用户在运行程序时会意外地发现,特定的对象不能够识别某个方法,从而引发程序崩溃。
另外,使用静态类型能够提高程序的可读性。
动态类型的方法
以下方法是NSObject提供的用于处理动态类型的方法列表:
- [A class]/[a class]
解释:根据类名或者类实例对象获取到类对象 - -(BOOL) isKindOfClass:class-object
解释:当前实例是不是class-object类或者他的子类的实例,该方法的调用者应该是一个类的实例
实例:id a = [[A1 alloc] init]; [a isKindOfClass:[A1 class]];
- -(BOOL) isMemberOfClass:class-object
解释:当前实例是不是class-object类的实例,该方法的调用者应该是一个类的实例
实例:id a = [[A1 alloc] init]; [a isMemberOfClass:[A1 class]];
- -(BOOL)respondsToSelector: Selector
解释:当前实例能否响应Selector指定的方法,该方法的调用者应该是一个类的实例
实例:id a = [[A1 alloc] init]; [a respondsToSelector:@selector(printA)];
- +(BOOL)instanceRespondsToSelector: Selector
解释:当前类能否响应Selector指定的方法,该方法的调用者应该是一个类对象而不是实例
实例:[A1 instancesRespondToSelector:@selector(printA)];
- +(BOOL) isSubclassOfClass:class-object
解释:当前类是不是class-object类的子类,该方法的调用者应该是一个类对象而不是实例
实例:[A1 isSubclassOfClass:[A2 class]];
- -(id)performSelector: selector
解释:应用selector指定的方法,该方法的调用者应该是一个类的实例
实例:id a = [[A1 alloc] init]; [a performSelector:@selector(printA)];
- -(id)performSelector: selector withObject: object1
解释:应用selector指定的方法,该方法的调用者应该是一个类的实例 - -(id)performSelector: selector withObject: object1 withObject: object2
解释:应用selector指定的方法,该方法的调用者应该是一个类的实例
异常处理
在OC中,可以使用如下代码捕获异常
@try {
//code1
} @catch (NSException *exception) {
//code2
} @finally {
//code3
}
有几点需要说明:
- finally不是必须的,可以没有finally
- 如果有finally
- 如果没有抛出异常,则在执行完try之后会执行finally的代码
- 如果抛出异常,则在抛出异常后会执行catch的代码,执行完catch的代码会执行finally的代码
如果catch中抛出了异常,则如果有finally,在抛出异常之前会先执行finally的代码