目录
6.1 OC的包装类
OC是面向对象编程语言,但它也是从C语言扩展二来的,而C语言中包括的基本数据类型(如short、int、float、double等)都不是对象,它们也不具备“对象”的特性——没有属性、方法可以被调用.OC提供了NSValue、NSNumber来封装C语言的基本类型,这样就可以让它们具有面向对象的特征。
它们不是包装类
进行iOS开发时,可能会遇到如下三个类型:
NSInteger:大致等于long型整数。
NSUInteger:大致等于unsigned long型整数。
CGFLoat:在64位平台上大致相当于double,在32位平台上大致相当于float。
它们虽有NS、CG前缀,但它们并不是包装类,依然只是基本类型。
NSValue和NSNumber
NSValue和NSNumber都是包装类,其中NSValue是NSNumber的父类,NSValue代表一个更通用的包装类,它可用于包装单个short、int、long、float、char、指针、对象id等数据项,通过该包装类,就可以把short、int、long、float、char、指针等添加到NSArray、NSSet等集合(这些集合要求它们的元素必须是对象)中。
NSNumber是更具体的包装类,主要用于包装C语言的各种数值类型,NSNumber主要包括如下3类方法:
+numberWithXxx:该类方法直接将特定类型的值包装成NSNumber。
-initWithXxx:该实例方法需要先创建一个NSNumber对象,再用一个基本类型的值来初始化NSNumber。
-xxxValue:该实例方法放回该NSNumber对象包装的基本类型的值。
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
//调用类方法将类方法的值包装成NSNumber对象
NSNumber* num = [NSNumber numberWithInt:20];
NSNumber* de = [NSNumber numberWithDouble:3.4];
NSLog(@"%d", [num intValue]);
NSLog(@"%g", [de doubleValue]);
NSNumber* ch = [[NSNumber alloc] initWithChar:'J'];
NSLog(@"%@", ch);
}
return 0;
}
将基本类型的值转换为包装类简单的做法就是调用numberWithXxx: 类方法,调用该方法时传入一个基本类型的值,该方法就会返回包装该值的包装类实例。如果要从包装类实例中获取基本类型的值,则调用xxxValue实例方法即可。
基本类型变量和包装类对象之间的转换关系如图所示
6.2 处理对象
打印对象和description方法
NSLog()函数不仅可以用于输出基本类型的值,也可用于输出OC对象,当使用NSLog()函数输出OC对象时,输出的是该对象的description方法的返回值。也就是说,下面两行代码完全一样:
NSLog(@"%@", p);
NSLog(@"%@", [p description]);
description方法是NSObject类的一个实例方法,所有的OC类都是NSObject类的子类,因此,所有的OC对象都有description方法。
description方法是一个特殊的方法,它是一个“自我描述”方法,该方法通常用于实现这样一个功能:当程序员直接打印该对象时,系统会输出该对象的“自我描述”信息,用以告诉外界该对象具有的状态信息。
NSObject类提供的description方法总是返回<FKPerson: 十六进制的首地址>,这个返回值并不能真正实现“自我描述”功能,因此,用户可以重写NSObject类的description方法。
很多时候,重写description方法可以返回该对象所有令人感兴趣的信息所组成的字符串,格式通常如下:
<类名 [实例变量1 = 值1,实例变量2 = 值2,...]>
例如打印FKApple对象
main.m
#import <Foundation/Foundation.h>
#import "FKapple.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
//便利的对象初始化
FKapple* p = [[FKapple alloc] initWithColor: @"红色" weight: 5.68];
NSLog(@"%@", p);//打印对象
}
return 0;
}
FKApple.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface FKapple : NSObject
@ property (nonatomic, copy) NSString* color;
@ property (nonatomic, assign) double weight;
- (id) initWithColor: (NSString*)color weight: (double) weight;
@end
NS_ASSUME_NONNULL_END
FKApple.m
#import "FKapple.h"
@implementation FKapple
@synthesize color = _color;
@synthesize weight = _weight;
- (id) initWithColor: (NSString*)color weight: (double) weight
{
if (self = [super init]) {
self.color = color;
self.weight = weight;
}
return self;
}
//重写父类的description方法
- (NSString*) description
{
return [NSString stringWithFormat:@"<FKapple[_color = %@, _weight = %f]>", self.color, self.weight];
}
@end
==与isEqual方法
OC程序中测试两个变量是否相等有两种方式:一种是利用 == 运算符,另一种是利用 isEqual: 方法。
当使用 == 来判断两个变量是否相等时,如果两个变量是基本类型的变量,且都是数值型,则只要两个变量的值相等,使用 == 判断就将返回真。对于两个指针型变量,它们必须指向同一个对象(两个指针变量保存的地址相同)时,使用 == 判断才会返回真。
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
int it = 65;
float fl = 65.0f;
NSLog(@"65和65.0f是否相等?:%d", (it == fl));
char ch = 'A';
NSLog(@"65和‘A'是否相等?:%d", (it == ch));
NSString* str1 = [NSString stringWithFormat:@"付闯"];//如果是英文则储存在常量区地址一样
NSString* str2 = [NSString stringWithFormat:@"付闯"];//如果是中文则储存在常量区地址不一样
//NSString* S3 = [[NSString alloc] initWithFormat:@"fuchuang"];
NSLog(@"str1的地址:%p, str2的地址:%p", str1, str2);
NSLog(@"str1和str2是否相等?:%d", (str1 == str2));
NSLog(@"str1和str2是否相等?:%d", [str1 isEqual:str2]);
//NSLog(@"%d", [NSDate new] == [NSString new]);
NSString* s1 = @"iOS";
NSString* s2 = @"iOS";
NSLog(@"s1的地:%p, s2的地址:%p", s1, s2);
NSLog(@"s1和s2是否相等?:%d", (s1 == s2));
NSLog(@"s1和s2是否相等?:%d", [s1 isEqual:s2]);
NSString* s3 = [NSString stringWithFormat:@"iOS"];
NSLog(@"s3的地:%p", s3);
NSLog(@"s1和s3是否相等?:%d", (s1 == s3));
NSLog(@"s1和s3是否相等?:%d", [s1 isEqual:s3]);
}
return 0;
}
常量池保证相同的字符串直接量只有一个,不会产生多个副本,例子中的s1、s2都将指向常量池中的同一个字符串对象,因此,s1、s2保存的地址值完全相同。
使用NSString的stringWithFormat: 类方法创建的字符串对象是运行时创建出来的,它被保存在运行时内存区(即堆内存)内,不会放入常量池,所以s3指针变量中保存的地址与s1、s2指针变量中保存的地址并不相同。
重写isEqual
isEqual:方法是NSObject类提供的一个实例方法,因此所有的指针变量都可调用该方法来判断是否与其他指针变量相等。在默认情况下,NSObject提供的isEqual:方法判断两个对象相等的标准与=运算符没有区别,同样要求两个指针变量指向同一个对象才会返回真。因此,NSObject类提供的isEqeal:方法没有太大的实际意义,如果希望采用自定义的相等标准,则可通过重写isEqual:方法来实现。
NSString已经重写了NSObject的isEqual:方法,NSString的isEqual:方法判断两个字符串相等的标准是:只要两个字符串所包含的字符序列相同,通过isEqual:比较就将返回真;否则将返回假。
main.m
#import <Foundation/Foundation.h>
#import "User.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
User* p1 = [[User alloc] initWithName:@"fu" idstr:@"fu135453"];
User* p2 = [[User alloc] initWithName:@"fuchuang" idstr:@"fu135453"];
User* p3 = [[User alloc] initWithName:@"付闯" idstr:@"Fu135453"];
NSLog(@"p1和p2是否相等?%d", [p1 isEqual: p2]);//输出1
NSLog(@"p1和p2是否相等?%d", [p1 isEqual: p3]);//输出0
}
return 0;
}
User.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface User : NSObject
@property (nonatomic, copy) NSString* name;
@property (nonatomic, copy) NSString* idstr;
- (id)initWithName: (NSString*) name idstr: (NSString*) idstr;
@end
NS_ASSUME_NONNULL_END
User.m
#import "User.h"
@implementation User
@synthesize name = _name;
@synthesize idstr = _idstr;
- (id)initWithName: (NSString*) name idstr: (NSString*) idstr {
if (self = [super init]) {
self.name = name;
self.idstr = idstr;
}
return self;
}//重写isEquel 判断idstr是否相等
- (BOOL)isEqual: (id) other {
if (self == other) {
return YES;//自反性
}
if (other != nil && [other isMemberOfClass:User.class]) {
//另一个对象也必须为User对象
User* target = (User*)other;
//两个对象的idstr也要相同
return [self.idstr isEqual: target.idstr];
}
return NO;
}
@end
通常而言,正确地重写isEqual: 方法应该满足下列条件:
自反性:对任意x,[x isEqual: x]一定返回真。
对称性:对任意x和y,如果[y isEqual: x]返回真,则[x isEqual: y]也返回真。
传递性:对任意x、y、z,如果有[x isEqual: y]返回真,[y isEqual: z]返回真,则[x isEqual: z]一定返回真。
一致性:对任意x和y,如果对象中用于比较的关系属性没有改变,那么无论调用[x isEqual: y]多少次,返回的结果都应该保持一致,要么一直是真,要么一直是假。
对任何不是nil的x,[x isEqual: nil]一定返回假。
6.3 类别与拓展
类别
通过继承,子类可以在父类的基础上添加新的方法,甚至重写父类已有的方法。
有些时候,使用继承并不是最好的选择,例如通过[NSNumber numberWithInt: 5]方法所生成的NSNumber对象其实只是NSNumber子类的实例。这样即使为NSNumber派生子类也没有任何意义,派生的子类对NSNumber现有的子类并没有任何影响,此时就需要借助类别来实现。
OC的动态特征允许使用类别为现有的类添加新方法,并且不需要创建子类,不需要访问原有类的源代码。通过使用类别即可动态地为现有的类添加新方法,而且可以将类定义模块化地分布到多个相关文件中。
类别同样由接口和实现部分组成,接口部分的语法格式如下:
@interface 已有类 (类别名)
//方法定义
...
@end
虽然这个语法格式很像在定义类,但在类名后有一个圆括号,而且圆括号中带一个类别名。
定义类别的语法与定义类的语法存在如下差异:
- 定义类时使用的类名必须是该项目中没有的类,而定义类别时使用的类名必须是已有的类。
- 定义类别时必须使用圆括号来包括类名。
- 类别中通常只定义方法
main.m
#import <Foundation/Foundation.h>
#import "NSNumber+fk.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSNumber* mynum = [NSNumber numberWithInt:3];
NSNumber* fc = [mynum add:2.4];
NSLog(@"%f", [fc doubleValue]);
NSLog(@"%@", fc);
}
return 0;
}
NSNumber+fk.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSNumber (fk)
- (NSNumber*) add: (double) num2;
@end
NS_ASSUME_NONNULL_END
NSNumber+fk.m
#import "NSNumber+fk.h"
@implementation NSNumber (fk)
- (NSNumber*) add: (double) num2 {
return [NSNumber numberWithDouble:([self intValue] + num2)];
}
@end
上面为NSNumber定义了fk类,接下来只要在程序中导入NSNumber+fk.h头文件,并在主函数使用NSNumber类,该类的实例就会具有add: 、subtract: 、multiply: 和 divide: 方法。这就实现了对原有NSNumber类的动态扩展。
虽然类别可以重写原有类中的方法,但通常不建议这么做,如果需要重写原有类方法,最好的建议是通过原有类派生子类,然后在子类中重写父类原有的方法。
注意:
通过类别为指定类添加新方法后,这个新方法不仅会影响当前类,还会影响它的全部子类,每个子类都会获取类别扩展方法。
可以根据需要为一个类定义多个类别,不同的类别都可对原有的类增加方法定义。
类别通常有如下3种用法:
对类进行模块化设计
调用私有方法
实现非正式协议
使用类别来调用私有方法
除了使用performSelector: 方法来动态调用那些私有方法之外,还可以通过类别来定义前向引用,从而实现对私有方法的调用。
FKItem.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface FKItem : NSObject
@property (nonatomic, assign) double price;
- (void)info;
@end
NS_ASSUME_NONNULL_END
FKItem.m
#import "FKItem.h"
@implementation FKItem
- (void)info{
NSLog(@"fuchuang");
}
- (double)Discount: (double) discount{
return self.price * discount;
}
@end
main.m
为了可以正常调用calDiscount: 方法,可以在main函数前增加类别:
#import <Foundation/Foundation.h>
#import "FKItem.h"
@interface FKItem (fk)
- (double)Discount: (double) discount;
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
FKItem* item = [[FKItem alloc] init];
item.price = 100;
NSLog(@"物品打折后的价格为%g", [item Discount:0.75]);
}
return 0;
}
拓展
扩展与类别类似,扩展相当于一个未命名的分类。但就用法来看,类别通常有单独的.h和.m文件,扩展则用于临时对某个类的接口进行扩展。类实现部分同时实现类接口部分定义的方法和扩展中定义的方法。
在定义类的扩展时,可以额外增加实例变量,也可以通过@property来合成属性,但在定义类的类别时,则不允许额外定义实例变量,也不能用@property来合成属性。
FKCar.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface FKCar : NSObject
@property (nonatomic, copy) NSString* brand;
@property (nonatomic, copy) NSString* model;
- (void)drive;
@end
NS_ASSUME_NONNULL_END
扩展:
新定义了一个color属性,新增了drive方法
#import "FKCar.h"
NS_ASSUME_NONNULL_BEGIN
@interface FKCar ()
@property (nonatomic, copy) NSString* color;
- (void)drive: (NSString*) owner;
@end
NS_ASSUME_NONNULL_END
FKCar.m
不仅需要实现接口部分定义的所有方法,还需要实现扩展中定义的方法。
#import "FKCar+drive.h"
@implementation FKCar
@synthesize brand;
@synthesize model;
@synthesize color;
- (void)drive {
NSLog(@"%@汽车正在路上奔驰", self);//打印对象
}
- (void)drive: (NSString*) owner{
NSLog(@"%@正在驾驶%@汽车在路上奔驰", owner, self);
}
//重写处理对象的方法来打印对象
- (NSString*) description {
return [NSString stringWithFormat:@"<FKCar[brand = %@, model = %@, color = %@]>", self.brand, self.model, self.color];
//返回的是字符串类型
}
@end
main.m
#import <Foundation/Foundation.h>
#import "FKCar+drive.h"
#import "FKCar.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
FKCar* car = [[FKCar alloc] init];
//使用点方法为car对象的属性赋值
car.model = @"bmw";
car.brand = @"666";
car.color = @"black";
[car drive];
[car drive:@"fc"];
}
return 0;
}
在上面程序中,可以使用点语法来访问color属性,这个color属性是在扩展部分定义的,除此之外,也可以调用drive: 方法,这个方法也是在扩展部分定义的。
注意事项
使用类别与扩展可以覆写该类中的另一个方法,但是,覆写一个方法后,再也不能访问原来的方法。如果确实需要覆写方法,正确的选择可能会是创建子类,子类中覆写了方法,依然可以通过super关键字来引用父类的方法。
如果一个方法定义在多个分类中,该语句不会指定使用哪个分类。
扩展不仅会影响这个类,同时也会影响它的所有子类,也就是为一个类扩展新方法后,所有子类都会继承这个方法。
6.4协议与委托
协议的作用类似于接口,用于定义多个类应该遵守的规范。
规范、协议与接口
同一个类的内部状态数据、各种方法的实现细节完全相同,类是一种具体的实现体。而协议则定义了一种规范,协议定义某一批类所需要遵守的规范,它不关心类的内部状态数据,也不关心类里方法的实现细节,它只规定这批类中必须提供某些方法,提供这些方法的类就可满足实际需要。
协议不提供任何实现。协议体现的是规范和实现分离的设计哲学。
Objective-C中协议的作用就相当于其他语言中接口的作用。
协议定义的是多个类共同的公共行为规范,这意味着协议里通常是定义一组共用方法,但不会为这些方法提供实现,方法的实现则交给类去完成。
使用类别实现非正式协议
之前介绍过,类别可以实现非正式协议,这种类别以NSObject为基础,为NSObject创建类别,创建类别时即可指定该类别应该新增的方法。
当某个类实现NSObject的该类别时,就需要实现该类别下的所有方法,这种基于NSObject定义的类别即可认为是非正式协议。
非正式协议实际上是一个分类,列出了一组方法但并没有实现它们。非正式协议通常是为根类定义的,有时,非正式协议也称为抽象协议。
NSObject+Eatable.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (Eatable)
- (void) taste;
@end
NS_ASSUME_NONNULL_END
NSObject+Eatable.m
#import "NSObject+Eatable.h"
@implementation NSObject (Eatable)
/*- (void)taste {
NSLog(@"苹果很好吃,,,");
}*/
@end
FK Apple.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface FKApple : NSObject
@end
NS_ASSUME_NONNULL_END
FKApple.m
#import "FKApple.h"
@implementation FKApple
在子类实现父类拓展的方法
- (void)taste {
NSLog(@"苹果很好吃");
}
@end
main.m
#import <Foundation/Foundation.h>
#import "NSObject+Eatable.h"
#import "FKApple.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
FKApple* p = [[FKApple alloc] init];
[p taste];
}
return 0;
}
上面在NSObject的Eatable类别中定义了一个taste方法,接下来所有继承NSObjective类的子类都会自动带有该方法,子类可以根据自己的需要决定是否实现该方法。Eatable类别作为一个非正式协议使用,相当于定义了一个规范,因此,遵守该协议的子类通常都会实现这个方法。
最后需要指出的是,对于实现非正式协议的类而言,OC并不强制实现该协议中的所有方法。如果该类没有实现非正式协议中的某个方法,那么程序运行时调用该方法,就会引发unrecognized selector错误。
正式协议
和定义类不同,正式协议不再使用@interface、@implementation关键字,而是使用@protocal关键字。定义正式协议的基本语法格式如下:
@protocol 协议名 <父协议1 , 父协议2> {
零到多个方法定义...
}
说明:
协议名应于类名采用相同的命名规则。
一个协议可以有多个直接父协议,但协议只能继承协议,不能继承类。
协议中定义的方法只有方法签名,没有方法实现:协议中包含的方法既可是类方法,也可是实例方法。
协议里所有的方法都具有公开的访问权限。
FKOutput.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@protocol FKoutput <NSObject>
@optional
- (void)output;//该协议的实现类可选择实现output方法
@required
- (void)addDate:(NSString*) msg;//该协议的实现类必须实现output方法
@end
NS_ASSUME_NONNULL_END
FKProductable.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@protocol FKProductable <NSObject>
- (NSDate*) getProduceTime;
@end
NS_ASSUME_NONNULL_END
FKPrintable.h
#import <Foundation/Foundation.h>
#import "FKProductable.h"
#import "FKoutput.h"
NS_ASSUME_NONNULL_BEGIN
@protocol KKPrintable <FKProductable, FKoutput>
- (NSString*) printColor;
@end
NS_ASSUME_NONNULL_END
接下来定义一个打印协议,该协议同时继承上面的两个协议
协议的继承和类继承不一样,协议完全支持多继承,即一个协议可以有多个直接的父协议。和类继承相似,子协议继承某个父协议,将会获得父协议里定义的所有方法。
遵守(实现)协议
在类定义的接口部分可指定该类继承的父类,以及遵守的协议,语法格式如下:
@interface 类名 : 父类 <协议1 , 协议2...>
FKPrinter.h
#import <Foundation/Foundation.h>
#import "KKPrintable.h"
NS_ASSUME_NONNULL_BEGIN
//继承NSObject, 遵守FKPrint协议
@interface FKPrinter : NSObject <KKPrintable>
@end
NS_ASSUME_NONNULL_END
FKPrinter.m
#import "FKPrinter.h"
#define MAXLINE 10
@implementation FKPrinter
{
NSString* printDate[MAXLINE]; //需要缓存的打印数据
int dateNum; //需要打印的作业数
}
//实现了FKPintable协议和他两个父协议的方法,
- (void)output {
while (dateNum > 0) {
NSLog(@"打印机使用%@打印;%@", self.printColor, printDate[0]);
dateNum--;
for (int i = 0; i < dateNum; i++) {
printDate[i] = printDate[i + 1];
}
}
}
- (void)addDate:(NSString*) msg {
if (dateNum > MAXLINE) {
NSLog(@"输出队列已满,添加失败");
} else {
printDate[dateNum++] = msg;
}
}
- (NSString*) printColor {
return @"red";
}
- (NSDate*) getProduceTime {
return [[NSDate alloc] init];
}
@end
main.m
#import <Foundation/Foundation.h>
#import "FKPrinter.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
FKPrinter* printer = [[FKPrinter alloc] init];
[printer addDate:@"fu"];
[printer addDate:@"chuang"];
[printer output];
[printer addDate:@"19"];
[printer addDate:@"666"];
[printer output];
//用协议定义变量,那么这些变量只能调用该协议中声明的方法
//创建一个FKPrinter对象, 当成FKProductable使用
NSObject<FKProductable>* p = [[FKPrinter alloc] init];
NSLog(@"%@", p.getProduceTime);
//创建一个FKPrinter对象, 当成FKOutput使用
id<FKoutput> s = [[FKPrinter alloc] init];
[s addDate:@"付闯"];
[s addDate:@"19"];
[s output];
}
return 0;
}
上面的Printer类实现了Printable协议,并且也实现了Output 和 Productable 两个父协议中的所有方法。如果实现类实现了协议中的所有方法,那么程序就可以调用该实现类所实现的方法。
程序创建了一个Pointer对象,该对象包含上面三个协议的方法,因此可以调用。
如果程序使用协议来定义变量,那么这些变量只能调用该协议中声明的方法。
使用协议定义变量的语法:
NSObject<协议1 , 协议2 ...>* 变量;
id<协议1 , 协议2 ...> 变量;
正式协议与非正式协议的比较:
非正式协议通过为NSObject创建类别来实现;而正式协议则直接使用@protocol创建。
遵守非正式协议通过继承带特定类别的NSObject来实现;而遵守正式协议则有专门的OC语法
遵守非正式协议通过继承带特定类别的NSObject来实现;而遵守正式协议则必须实现协议中定义的方法。
为了弥补正式协议必须实现协议的所有方法造成的灵活性不足,OC 2.0新增了@optional、@required两个关键字:
@optional:位于该关键字之后、@optional或@end之前声明的方法是可选的。
@required:位于该关键字之后、@required或@end之前声明的方法是必需的,实现类必须实现这些方法。
@protocol Output
//定义协议的方法
@optional
-(void) output;
@required
-(void) addData: (NSString*) msg;
@end
通过在正式协议中使用@optional、@required关键字,正式协议可以完全代替非正式协议的功能
委托
协议体现的 是一种规范, 定义协议的类可以把协议定义的方法委托给实现协议的类,这样可以让类定义具有跟好的通用性质,因为具体的动作交给协议的实现类去实现。
举例来说,当我们在iOS应用中开发一个表格时,需要使用UItableView对象,但这个对象只能实现最通用的表格行为,表格对象将数据处理等动作委托给实现UITableViewDataSourse协议的对象负责处理。