3.0 封装
3.1 异常处理
先来说说什么是错误?
一般情况下,错误指的是我们写的源代码不符合语法规范,然后编译报错。
什么是bug?
程序可以编译,也可以链接和执行,但是执行的结果并不是我们所预想的那样。
什么是异常?
程序可以编译、链接和执行。但是当执行的时候,处于某种特定条件下时,程序的执行会终止。
如何处理异常?
目的:为了让程序在执行的时候如果发生了异常而不崩溃,继续往下执行。
语法:
@try
{
}
@catch(NSException *ex)
{
}
@finally
{
}
将有可能发生异常的代码放在@try中,
当@try中的代码在执行的时候,如果发生了异常,不会崩溃,而是会立即跳转到@catch中去执行里面的代码;
当@catch的代码执行完毕之后,结束@try……@catch往下执行;
如果@try中的代码在执行的时候,没有发生异常,就会略过@catch中的代码往下执行。
注意:
当@try中的代码在执行的时候发生了异常,@try中的发生异常的代码之后的代码不会执行,会立即跳转至@catch中的代码执行。
@catch中的代码只有在@try中的代码发生异常的时候才会执行,所以在@catch中我们一般会写处理异常的代码。
@catch的参数NSException *ex通过%@打印出ex指向的对象的值,就可以知道发生异常的原因。
@try……@catch后面还可以跟一个@finally,@finally中的代码无论@try中是否发生了异常,都会被执行。
@try……@catch并不是万能的,不是所有的运行时错误都可以处理的,尤其是C语言的代码运行错误。
其实,@try……@catch在实际开发过程中,并不是经常使用的,要避免异常我们最常用的方式还是用逻辑判断。
这里只是介绍一下异常处理的一种应用方法。
3.2 类方法
OC中的方法分为两种:
对象方法/实例方法:
我们之前学习的方法就叫做对象方法。如果想要调用对象方法就必须要先创建对象,通过对象名来调用。
类方法:
类方法的调用不依赖于对象,如果要调用类方法,不需要创建对象,而是直接通过类名来调用。
那么类方法如何声明呢?
我们知道对象方法的声明是使用一个“-”来引出,例如:
-(void)sayHi;
而类方法的声明则是使用“+”引出的,其他都是和对象方法一样的。例如:
+(void)sayHi;
类方法的实现和对象方法的实现也是一样的。
那么类方法的调用呢?
刚才我们已经说了,类方法不依赖于对象,所以在调用的时候不需要和对象方法那样先创建对象,而是直接使用类名来调用。
语法格式:
[类名 类方法名];
类方法的特点:
节约空间:因为调用类方法不需要创建对象,这样就节约了空间;
提高效率:因为调用类方法不需要创建对象,直接找到类,直接执行类中的类方法。
注意:
在类方法中不能直接访问属性。
原因是:
属性是在对象创建的时候,跟随着对象一起创建在对象之中。类第一次被访问的时候,会做类加载,是把类的代码存储在代码段。
因为属性只有在对象创建的时候才会创建在对象志之中,而类方法在执行的时候,有可能还没有对象,当没有对象的时候也就没有属性,所以在类方法中无法直接访问属性。
虽然不能直接访问属性,但是我们可以在类方法中创建一个对象,这个时候就可以访问这个对象的属性了。
同理,在类方法中也不能通过self直接访问对象方法。
反之,在对象方法中是可以调用类方法的。
什么时候可以将方法定义为类方法呢?
如果方法不需要直接访问属性,也不需要直接调用其他的对象方法,那么我们就可以将这个方法定义为类方法。
关于类方法的规范:
如果我们写一个类,那么就要求为这个类提供一个和类名同名的类方法,这个方法创建一个最纯洁(所有的属性都是默认值)的对象返回。
例如有一个类:
@interface Person : NSObject
{
NSString *_name;
int _age;
}
+(Person *)person;
@end
@implementation Person
+(Person *)person
{
Person *p1 = [Person new];
return p1;
}
@end
苹果和第三方写的类都遵守这个规范。
如果你希望创建的对象的属性的值由调用者指定,那么就为这个类方法带参数。
例如:
@interface Person : NSObject
{
NSString *_name;
int _age;
}
+(Person *)personWithName:(NSString *)name andAge:(int)age;
@end
@implementation Person
+(Person *)personWithName:(NSString *)name andAge:(int)age
{
Person *p1 = [Person new];
p1->_name = name;
p1->_age = age;
return p1;
}
@end
苹果和第三方写的类也都遵守这个规范。
3.3 NSString
在基础知识中我们说NSString是一个数据类型,但是其实NSString是Foundation框架中的一个类。我们可以在Xcode中右击NSString选择Jump to definition来查看它的定义,你会发现它就是一个类。
所以,OC中的字符串本质上是用NSString对象来存储的。因此,我们也可以使用标准的创建对象的方法来创建一个NSString对象。
例如:
NSString *str1 = [NSString new];
NSString *str2 = [NSString string];
这种方式创建的字符串是空字符串@“”。
但是这种标准的方式实在是比较繁琐,作为OC中最常用的一个类,系统采用了一种更为简单的方式来创建NSString对象。那就是我们前面介绍的方法:
NSString *str = @“Jack”;
@“Jack”是一个NSString对象,str的值是这个对象的地址。
这里我们补充一句有关%@和%p的内容:
%p指代的是指针变量的值
%@指代的是指针变量指向的对象
下面介绍一些NSString常用的几个类方法:
+(instancetype)stringWithUTF8String:(const char*)nullTerminatedCString;
这个类方法中:
instancetype作为返回值,代表返回的是当前这个类的对象。
这个类方法的作用是:将C语言的字符串转换为OC字符串对象。
+(instancetype)stringWithFormat:(NSString *)format,…
这个类方法使用频率非常高。
它的作用是:拼接一个字符串对象,使用变量或者其他数据拼接成OC字符串。
例子:
int age = 19;
NSString *name = @“Jack”;
NSString *str = [NSString stringWithFormat:@“My name is %@, I’m %d years old.”,name,age];
NSString的几个常用对象方法:
如果想要计算一个字符串的字符个数,有这样一个对象方法:
length方法,返回值为NSUInteger,其实就是unsigned long类型,可以处理中文。
例子:
NSString *str = @“Rose”;
NSUInteger len = [str length];
NSLog(@“len = %lu”,len);
得到的输出结果是len = 4。
得到字符串中指定第几个字符:
-(unichar)characterAtIndex:(NSUInteger)index;
返回值是unichar,其实就是unsigned short类型。
例子:
NSString *str = @“rose”;
unichar ch = [str characterAtIndex:2];
NSLog(@“ch = %C”,ch);
输出的结果是第3个字符,也就是s。
这里注意:unichar是两个字节,要输出这种类型的字符要用%C,大写的C,小写的c将无法输出中文。
判断两个字符串的内容是否相同:
这里先声明一下,判断两个字符串是否相同,不要使用==来判断,如果这么做会遇到判断错误的问题。
应该使用:
-(BOOL)isEqualToString:(NSString *)aString;
例子:
NSString *str1 = @“Jack”;
NSString *str2 = @“jack”;
if ([str1 isEqualToString:str2])
{
NSLog(@“它们是一样的!”);
}
else
{
NSLog(@“它们是不一样的!”);
}
比较字符串的大小:
-(NSComparisonResult)compare:(NSString *)string;
返回值NSComparisonResult是一个枚举,其值有三个,分别是-1,0,1,代表代表小于、等于和大于。所以我们也可以使用int类型来接收这个结果。
例子:
NSString *str5 = @"Ellen";
NSString *str6 = @"Jack";
NSComparisonResult result = [str5 compare:str6];
NSLog(@"result = %ld",(long)result);
3.4 匿名对象
我们之前创建对象的方法是创建一个指针,指向这个新的对象。
那么什么是匿名对象呢?
顾名思义,匿名对象就是没有名字的对象。如果我们创建了一个对象,没有用一个指针存储这个对象的地址,也就是没有任何指针指向这个对象,那么这个对象就叫做匿名对象。
创建匿名对象的方法很简单,如下:
[类名 new];
匿名对象怎么用呢?
因为new实际上是一个类方法,这个方法做的事情就是创建对象,返回值是创建的对象的地址,所以我们可以直接使用这个地址。
[Person new]->_name = @“Jack”;
[[Person new] sayHi];
注意:
匿名对象只能使用一次。
每次创建匿名对象都是不同的对象。
匿名对象有什么用?
如果某个对象的成员只会被我们使用一次,用完之后这个对象再也不需要了,那么就可以使用匿名对象。
如果方法的参数是一个对象,而调用者为这个参数赋值的对象就是专门来给这个方法传递的,并且这个对象调用者后面不会再使用,那么这个时候就可以直接为方法传递一个匿名对象。
3.5 属性的封装
面向对象的三大特征是什么?
封装
类就是一种高级别的封装,类将数据和行为封装为了一个整体。
好处:
屏蔽内部的事项,外界不需要知道内部是如何实现的,只需要知道这个对象有什么用。
方便操作
后期的维护十分的便利
继承
多态
继承和多态,我们在后面的内容里会介绍。
本节主要介绍封装。
那么,什么是属性的封装呢?
我们在为对象的属性赋值的时候,语法上其实只要数据的类型是属性的类型就可以了,无论多大多小的值都是可以的。但是,有时候一些值虽然符合属性的类型,但是情理上是不合适的。
比如说一个Person类的对象有一个int类型的属性_age表示一个人的年龄,为这个对象赋值的时候_age只要符合int类型的要求理论上都是可以的,但是一个人的年龄是不可以有1000岁的,一般来说都是在0~150之间。
这个时候,我们希望能够为这个数据做一个逻辑验证。如果为属性赋值的值在一个逻辑范围之间的,我们就把这个值赋值给这个属性,否则我们就进行默认处理。
如何实现这个需求呢?
首先,不能使用@public。因为一旦使用@public就意味着外界可以直接访问对象的这个属性,外界一旦可以直接访问这个属性就可以任意的为这个属性赋值,所以必须要将这个功能关掉。
为类提供一个方法,这个方法专门为这个属性赋值。这个方法我们叫做setter。
这个方法一定是一个对象方法。
这个方法没有返回值。
这个方法的名称必须以set开头,后面跟上去掉下划线首字母大写的属性名。
这个方法一定是有参数的,参数的类型和属性的类型一致,参数的名称和属性的名称一致(去掉下划线)。
在方法的实现种,判断传入的数据是否符合逻辑,如果符合逻辑则赋值,否则做默认处理。
外界想要为对象的属性赋值,那么就调用这个对象的setter方法,将要赋值的数据传入给这个方法。方法会对这个数据进行验证,如果符合验证就会把这个值赋值给这个属性,否则就做默认处理。
下面举例说明属性的封装如何使用代码实现:
假设有一个Person类,有两个属性,分别是_name和_age,其中_name要求长度不得小于2个字符,而_age则要求在0~150之间。
首先,我们建立这个Person类:
@interface Person : NSObject
{
NSString *_name;
int _age;
}
-(void)setName:(NSString *)name;
-(void)setAge:(int)age;
@end
@implementation Person
-(void)setName:(NSString *)name
{
NSUInteger len = [name length];
if (len >= 2)
{
_name = name;
}
else
{
_name = @"无名";
}
}
-(void)setAge:(int)age
{
if (age >= 0 && age <= 150)
{
_age = age;
}
else
{
_age = 18;
}
}
@end
如果我们在main函数中写入如下代码:
Person *p1 = [Person new];
[p1 setName:@"我"];
[p1 setAge:200];
我们会得到p1这个对象的名字是“无名”,岁数是18。
以上介绍的就是属性的封装,但是我们会发现一个问题,如果去掉了@public我们在main函数中就无法直接读取对象的属性值了。那要怎么办呢?
这个时候,我们需要再写一个方法,专门用来返回属性的值。这个方法我们叫做getter。
首先,这个方法一定是一个对象方法,因为这个方法做的事情是读取对象属性的值。
这个方法一定有返回值,返回值的类型和属性的类型一致。
这个方法的名称直接就是属性的名称(去掉下划线)。
这个方法没有参数
这个方法的实现是直接将属性的值返回。
这个时候,如果外界希望得到属性的值,那么就只需要调用这个getter方法就可以了。
举例说明:
在刚才那个例子里,我们可以添加这样两个方法:
-(NSString *)name;
-(int)age;
-(NSString *)name
{
return _name;
}
-(int)age
{
return _age;
}
这样在main函数中就可以使用这个方法来输出和读取这几个对象属性的值了。
例如:
NSLog(@"我的名字叫%@,我今年%d岁了。",[p1 name],[p1 age]);
下面来谈谈封装的规范:
只要属性需要被外界访问,就要为这个属性封装setter和getter,哪怕对这个属性没有逻辑要求验证。
如果属性只在类的内部访问,那么就不需要为其封装setter和getter。
只读封装与只写封装:
只读封装:为属性封装的时候,只提供getter,不提供setter。
只写封装:为属性封装的时候,只提供setter,不提供getter。
3.6 类与类之间的关系
类与类之间的关系分为以下几种主要关系:
组合关系
一个类是由多个类组合起来的,比如:计算机类,是由主板类、内存类、CPU类、硬盘类……等组合起来的,主板、内存、硬盘作为计算机类的属性,那么这个时候,计算机对象和主板、内存、硬盘的关系就是组合关系。
依赖关系
一个类的方法的参数是另外一个类,那么我们就说它们之间的关系是依赖关系。
比如B类是A类方法的参数,我们就说A类依赖于B类。
这里提到几个概念:
耦合度:当修改一个类的时候,对另外一个类的影响程度。
高耦合:一个类修改了,另外一个类就崩溃了,这是高耦合。
低耦合:当修改一个类的时候,对另外一个类的影响很小或者甚至没有影响。
高内聚:一个类仅仅做自己的相关的事情。
单一职责原则:一个类只做自己的事情,别人的事情给别人做。
关联关系
关联体现的是两个类之间语义级别的一种强依赖关系。
比如我和我的朋友,这种关系比依赖更强、不存在依赖关系的偶然性、关系也不是临时性的,一般是长期性的,而且双方的关系一般是平等的。关联可以是单向、双向的。
表现在代码层面,为被关联类B以类的属性形式出现在关联类A中,也可能是关联类A引用了一个类型为被关联类B的全局变量。
继承关系
继承关系将在后面的章节描述。
3.7 案例——士兵突击
我们来利用一个实例来说明本章节学习的内容:
利用面向对象,模拟如下流程:
士兵 开枪 射出子弹
这个流程中有三个类:
士兵类:
属性:姓名、兵种、有枪
行为:开火的方法
枪类:
属性:型号、射程、弹夹
行为:射击
这里的弹夹也是一个类:
属性:最大容量、现在弹夹内的子弹数量
那么代码可以这么写:
先从小范围的类开始,弹夹类:
Clip.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Clip : NSObject
{
int _maxCapcity;
int _bulletNum;
}
-(void)setMaxCapcity:(int)maxCapcity;
-(int)maxCapcity;
-(void)setBulletNum:(int)bulletNum;
-(int)bulletNum;
@end
NS_ASSUME_NONNULL_END
Clip.m
#import "Clip.h"
@implementation Clip
-(void)setMaxCapcity:(int)maxCapcity
{
_maxCapcity = maxCapcity;
}
-(int)maxCapcity
{
return _maxCapcity;
}
-(void)setBulletNum:(int)bulletNum
{
if (bulletNum >= 0 && bulletNum <= _maxCapcity)
{
_bulletNum = bulletNum;
}
else
{
_bulletNum = _maxCapcity;
}
}
-(int)bulletNum
{
return _bulletNum;
}
@end
接着是枪类:
Gun.h
#import <Foundation/Foundation.h>
#import "Clip.h"
NS_ASSUME_NONNULL_BEGIN
@interface Gun : NSObject
{
NSString *_model;
Clip *_clip;
int _firingRange;
}
-(void)setModel:(NSString *)model;
-(NSString *)model;
-(void)setFiringRange:(int)firingRange;
-(int)firingRange;
-(void)setClip:(Clip *)clip;
-(Clip *)clip;
-(void)shoot;
@end
NS_ASSUME_NONNULL_END
Gun.m
#import "Gun.h"
@implementation Gun
-(void)setModel:(NSString *)model
{
_model = model;
}
-(NSString *)model
{
return _model;
}
-(void)setFiringRange:(int)firingRange
{
_firingRange = firingRange;
}
-(int)firingRange
{
return _firingRange;
}
-(void)setClip:(Clip *)clip
{
_clip = clip;
}
-(Clip *)clip
{
return _clip;
}
-(void)shoot
{
if ([_clip bulletNum] > 0)
{
NSLog(@"突突突……");
int leftBulletNum = [_clip bulletNum];
leftBulletNum --;
[_clip setBulletNum:leftBulletNum];
NSLog(@"剩余子弹:%d",leftBulletNum);
}
else
{
NSLog(@"子弹没有了……");
}
}
@end
然后是士兵类:
Soldier.h
#import <Foundation/Foundation.h>
#import "Gun.h"
NS_ASSUME_NONNULL_BEGIN
@interface Soldier : NSObject
{
NSString *_name;
NSString *_type;
Gun *_gun;
}
-(void)setName:(NSString *)name;
-(NSString *)name;
-(void)setType:(NSString *)type;
-(NSString *)type;
-(void)setGun:(Gun *)gun;
-(Gun *)gun;
-(void)fire;
@end
NS_ASSUME_NONNULL_END
Soldier.m
#import "Soldier.h"
@implementation Soldier
-(void)setName:(NSString *)name
{
_name = name;
}
-(NSString *)name
{
return _name;
}
-(void)setType:(NSString *)type
{
_type = type;
}
-(NSString *)type
{
return _type;
}
-(void)setGun:(Gun *)gun
{
_gun = gun;
}
-(Gun *)gun
{
return _gun;
}
-(void)fire
{
NSLog(@"FIRE!...");
[_gun shoot];
}
@end
最后是main.m
#import <Foundation/Foundation.h>
#import "Soldier.h"
int main(int argc, const char * argv[])
{
@autoreleasepool
{
Clip *clip1 = [Clip new];
[clip1 setMaxCapcity:30];
[clip1 setBulletNum:9];
Gun *gun1 = [Gun new];
[gun1 setModel:@"M1"];
[gun1 setClip:clip1];
Soldier *soldier1 = [Soldier new];
[soldier1 setName:@"Jack"];
[soldier1 setType:@"Level 1"];
[soldier1 setGun:gun1];
for (int i = 0; i < 10; i++)
{
[soldier1 fire];
}
}
return 0;
}
好了,大家把这些代码输入到Xcode中看一下效果吧!本案例涵盖了本章节几乎所有的知识点。