跳转到主要内容

【iOS】OC语言学习——面向对象(下)

目录 一、OC的包装类 1.1 以下不是包装类: 1.2 以下是包装类: 二、处理对象 2.1 处理对象和description方法 一、NSLog(@"%@", obj) 做了什么? 二、默认行为 三、如何自定义打印内容? 输出: 四、打印集合对象(NSArray、NSDict

原创文章2025iOS 底层Objective-C面向对象【iOS】OC语言学习——面向对象(下)
作者 TommyWu
本文 4,776 字,阅读预计需要 24 分钟
目录展开
封面图片: 【iOS】OC语言学习——面向对象(下)

目录

一、OC 的包装类

1.1 以下不是包装类:

1.2 以下是包装类:

二、处理对象

2.1 处理对象和 description 方法

一、NSLog (@”%@”, obj) 做了什么?

二、默认行为

三、如何自定义打印内容?

输出:

四、打印集合对象(NSArray、NSDictionary)

2.2 == 和 isEqual 方法

2.2.1 == 方法

2.2.2 isEqual 方法

三、类别与拓展

3.1 类别

3.1.1 利用类别进行模块化设计

3.1.2 使用类别来调用私有方法

3.2 拓展

四、协议(protocol)与委托

4.1 协议

4.1.1 使用类别实现非正式协议

4.1.2 正式协议的定义

4.1.3 遵守(实现)协议

4.1.4 正式协议与非正式协议的差异

4.1.5 协议与委托(delegate)

一、OC 的包装类

OC 提供了 NSValue、NSNumber 来封装 C 语言基本类型(short、int、float 等)。

在 Objective-C 中,** 包装类(Wrapper Classes)** 是用来把基本数据类型(如 int、float、char 等)“包装” 为对象的类。因为 Objective-C 是面向对象的语言,有时候我们需要把基本类型当作对象使用,比如:

  • 放入 NSArray、NSDictionary 这样的集合中(这些集合只能存放对象);
  • 使用对象方法对数值进行操作;
  • 与 Foundation 框架接口交互。

1.1 以下不是包装类:

        1、NSInteger:大致等于 long 型整数

        2、NSUInteger:大致等于 unsigned long 型整数         3、CGFLoat:在 64 位平台相当于 double,在 32 位平台相当于 float

        以上的类型只是基本类型。为了更好的兼容不同的平台,当程序需要定义整形变量的时候,建议使用NSInteger,NSUInteger;当程序需要定义浮点型变量的时候,建议使用CGFLoat。

1.2 以下是包装类:

        1、NSValue是 NSNumber 的父类,它代表一个更通用的包装类,可以包装 int、short、long、float、char、指针、对象 id 等数据项。并将它们添加到 NSArray、NSSet 等集合中去。

        2、NSNumber是更具体的包装类,用于包装 c 语言的各种数值类型。它有如下三类方法:            ** - [x] +numberWithXxx:直接将特定类型的值包装成 NSNumber             - [x] -initWithXxx:该实例方法需要先创建一个 NSNumber 对象,再用一个基本类型的值来初始化 NSNumber             - [x] -xxxValue:该实例方法返回该 NSNumber 对象包装的基本类型的值 **         如上方法的 Xxx 是 Int,Char,Double,string 等各种数据类型。

NSNumber 的 compare: 返回 NSComparisonResult 枚举值(NSOrderedAscending、NSOrderedSame 或 NSOrderedDescending),不是对象;应按枚举语义比较数值。

        基本类型变量和包装类对象之间的转换关系可以理解为:基本类型变量通过调用 numberWithXxx:类方法来转换并返回包装类对象;包装类对象通过调用 xxxValue 来获取基本类型的值。

#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSNumber *num = [NSNumber numberWithInt:66];
NSNumber *de = [NSNumber numberWithDouble:7.7];
NSLog(@"%d",[num intValue]);
NSLog(@"%g",[de doubleValue]);
NSNumber *ch = [[NSNumber alloc] initWithChar:'t'];
NSLog(@"%@",ch);
}
return 0;
}

二、处理对象

2.1 处理对象和 description 方法

在 Objective-C 中,打印对象(NSLog (@”%@”, obj)) 实际上是调用对象的 -description 方法。这个方法决定了你在控制台看到的输出内容。

一、NSLog (@”%@”, obj) 做了什么?

当你写:

NSLog(@"%@", obj);

它相当于:

NSLog(@"%@", [obj description]);

也就是说:

        NSLog 并不会直接打印对象地址;

  • 它调用了 obj 的 -description 方法,获取一个 NSString* 类型的描述字符串来打印。

二、默认行为

如果你没有重写 -description,那么会输出类似:

<ClassName: 0x10060ae50>

这是 NSObject 默认的格式,表示 “类名 + 内存地址”。

三、如何自定义打印内容?

你可以重写 -description 方法来自定义输出内容:

示例:

@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@end
@implementation Person
- (NSString *)description {
return [NSString stringWithFormat:@"Person: name=%@, age=%d", self.name, self.age];
}
@end

使用:

Person *p = [[Person alloc] init];
p.name = @"Tom";
p.age = 20;
NSLog(@"%@", p);
#### 输出:

Person: name=Tom, age=20

四、打印集合对象(NSArray、NSDictionary)

集合类如 NSArray、NSDictionary、NSSet,当你 NSLog 打印它们时,它们也会调用内部所有对象的 -description 方法。

NSArray *arr = @[p];
NSLog(@"%@", arr);

如果你没有给 p 写 -description,你就会看到一串地址;

如果写了,就会输出里面每个对象的自定义内容。

#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@end
@implementation Person
//重写 description 方法
- (NSString *)description {
return [NSString stringWithFormat:@"Person: name=%@, age=%d", self.name, self.age];
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init];
p.name = @"Tom";
p.age = 20;
// 打印对象
NSLog(@"%@", p); // 自动调用 [p description]
}
return 0;
}

不重写 VS 重写后

<Person: 0x600000e812e0>
Person: name=Tom, age=20
### 2.2 == 和 isEqual方法
oc中测试两个变量事都相等的方式有两个,分别是:==方法和isEqual方法
#### 2.2.1 ==方法
当用==方法时,若️①两个变量是基本类型的变量,️②两个变量都是数值型的变量(不一定要求数据类型严格相等),️③两个变量的值相等。则==判断返回真,否则返回假。
        而对于指针类型的变量,则要两个指针指向同一个对象,则==返回真,否则返回假。
        当使用==的两个类没有继承关系时,编译器会提示警告。
**@“hello”和[NSString stringWithFormat:@“hello”]的区别:**
相同内容的字符串字面量是否共享对象或地址不是公开保证;比较字符串内容应使用 isEqualToString:,不要依赖 ==。
stringWithFormat: 返回的具体类、是否分配普通堆对象及对象地址也可能受内容、系统版本和运行时优化影响。
以下代码演示了==的用法:
```objective-c
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
int it = 65;
int fl = 65.0f;
char ch = 'A';
NSString *str1 = @"hello";
NSString *str2 = @"hello";
NSString *str3 = @"byebye";
NSLog(@"%d",(it==fl)); //结果为1
NSLog(@"%d",(fl == ch)); //结果为1
NSLog(@"%d",(str1 == str2)); //结果为1
NSLog(@"%d",(str2 == str3)); //结果为0
//常量池
NSString *p1 = @"朱斌";
NSString *p2 = @"朱斌";
NSLog(@"p1地址:%p,p2地址:%p",p1,p2);
NSLog(@"%d",(p1 == p2)); //结果为1
NSString *p3 = [NSString stringWithFormat:@"朱斌"];
NSString *p4 = [NSString stringWithFormat:@"朱斌"];
NSLog(@"p3地址:%p",p3);
NSLog(@"p4地址:%p",p4);
NSLog(@"%d",(p1 == p3)); //结果为0
NSLog(@"%d",(p4 == p3)); //结果为0
NSString *p5 = [NSString stringWithFormat:@"zbchi"];
NSString *p6 = [NSString stringWithFormat:@"zbchi"];
NSLog(@"p5地址:%p",p5);
NSLog(@"p6地址:%p",p6);
NSLog(@"%d",(p5 == p6)); //结果为1
}
return 0;
}
#### 2.2.2 isEqual方法
isEqual比较的是对象的内容。
isEqual默认实现是比较地址(跟 == 一样),但很多系统类(如NSString,NSArray,NSNumber)都 **重写该方法 **来比较内容。所以这个时候输出的是** 内容相等**
```objective-c
#import "FKPerson.h"
@implementation FKPerson
- (id) initWithName: (NSString*) name idStr: (NSString*) idStr {
if(self = [super init]) {
self.name = name;
self.idStr = idStr;
}
return self;
}
- (BOOL) isEqual:(id) other {
//如果两个对象指针相等,为同一个对象
if(self == other) {
return YES;
}
//当other不为nil且它为FKPerson的实例时
if(other != nil && [other isMemberOfClass:FKPerson.class]) {
FKPerson* target = (FKPerson*)other;
//并且要判断当前对象的idStr和target对象的idStr相等才可以判断两个对象相等
return [self.idStr isEqual: target.idStr];
}
return NO;
}
@end

三、类别与拓展

在 oc 中,类别和拓展都是对类进行的 “补充” 机制。

3.1 类别

类别是 oc 中用于給已有方法添加方法的一种机制,不能添加成员变量

类别的定义:

命名规则:在接口文件部分的文件命名是 “类名 + 类别名.h” 在实现部分的文件命名是 “类名 + 类别名.m” 的形式。

类别的接口部分的声明和类的定义十分相似,但类别不继承父类,只需要在已有类的类名后面加一个括号,写入类别名,然后再在下面定义方法。

@interface ClassName (CategoryName)
- (void)newMethod;
@end
@implementation ClassName (CategoryName)
- (void)newMethod {
NSLog(@"Category method called");
}
@end

特点:

Category 可声明属性,但不能新增实例变量;属性的 getter/setter 实现需由开发者提供,常见存储方式是关联对象。

方法是运行时动态添加的

Category 可以实现与原类同名的方法,使消息查找优先命中分类实现;原类实现不会被删除,调用顺序也不是可依赖的覆盖契约。

多个 Category 实现同名方法时,具体命中顺序受镜像加载、链接及 Runtime 附加顺序影响,不应断言 “最后编译的类别一定生效”。

FKPerson.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface FKPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
- (void)test1;
@end
NS_ASSUME_NONNULL_END
//FKPerson.m
#import "FKPerson.h"
@implementation FKPerson
- (void)test1 {
NSLog(@"test1");
}
@end
//FKPerson+Test2.h
#import "FKPerson.h"
NS_ASSUME_NONNULL_BEGIN
@interface FKPerson (Test2)
- (void)dda;
@end
NS_ASSUME_NONNULL_END
//FKPerson+Test2.m
#import "FKPerson+Test2.h"
@implementation FKPerson (Test2)
- (void)dda {
NSLog(@"test2");
}
@end
//NSNumber+FK
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSNumber (FK)
- (NSNumber *)add:(double)num2;
- (NSNumber *)subtract:(double)num2;
- (NSNumber *)multiply:(double)num2;
- (NSNumber *)divide:(double)num2;
@end
NS_ASSUME_NONNULL_END
//NSNumber+FK.m
#import "NSNumber+FK.h"
@implementation NSNumber (FK)
- (NSNumber *)add:(double)num2 {
return @(self.doubleValue + num2);
}
- (NSNumber *)subtract:(double)num2 {
return @(self.doubleValue - num2);
}
- (NSNumber *)multiply:(double)num2 {
return @(self.doubleValue * num2);
}
- (NSNumber *)divide:(double)num2 {
return @(self.doubleValue / num2);
}
@end
//main
#import <Foundation/Foundation.h>
#import "FKPerson.h"
#import "FKPerson+Test2.h"
#import "NSNumber+FK.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 测试 FKPerson 类和分类
FKPerson *person = [[FKPerson alloc] init];
person.name = @"Tom";
person.age = 18;
[person test1]; // 输出 test1
[person dda]; // 输出 test2
// 测试 NSNumber 分类
NSNumber *num1 = @10;
NSLog(@"Add: %@", [[num1 add:5] stringValue]); // 15
NSLog(@"Sub: %@", [[num1 subtract:3] stringValue]); // 7
NSLog(@"Mul: %@", [[num1 multiply:2] stringValue]); // 20
NSLog(@"Div: %@", [[num1 divide:2] stringValue]); // 5
}
return 0;
}
#### 3.1.1 利用类别进行模块化设计
        类的实现部分不能分布到多个.m文件中去,因此当某个类非常大时,会导致那个类实现所在的文件非常大,以至于维护起来非常困难。因此如果需要将一个较大的类分模块设计,使用类别是一个不错的选择。通过类别可以对类实现按模块分布到不同的*.m文件中,从而提高项目后期的可维护性。
#### 3.1.2 使用类别来调用私有方法
        在前面的学习中我们知道,在实现部分定义的方法相当于私有方法,通常不允许调用。但是实际上私有方法仍然有办法调用。比如这一小节要学的,通过类别来定义前向引用,从而实现对私有方法的调用。
我们用代码来演示如何调用:
首先,我们定义一个FKItem类的接口部分,只声明了一个info方法
```objective-c
#import <Foundation/Foundation.h>
@interface FKItem : NSObject
@property (nonatomic,assign) double price;
- (void) info;
@end

在实现部分新添加一个方法,相当于私有方法 

#import "FKItem.h"
@implementation FKItem
@synthesize price;
- (void) info {
NSLog(@"这是一个普通的方法")
}
//新增的私有方法
- (double) calDiscount: (double) discount {
return self.price *discount;
}
@end

我们在主函数中在调用这个私有方法的时候,会报错说没有这个方法。我们可以在 main()函数下新建一个类别,然后再在类别中声明 calDiscount 方法,这时就可以了

#import <Foundation/Foundation.h>
#import "FKItem.h"
@interface FKItem (fk)
- (double) calDiscount: (double)diacount;
@end
int main(int argc,char *argv[]) {
@autoreleasepool {
FKItem *item = [[FKItem alloc] init];
item.price = 109;
[item info];
NSLog(@"物品打折的价格是:%g",[item calDiscount:.75]);
}
}
### 3.2拓展
拓展与类别相似,拓展相当于匿名类别,只能在类的实现文件(.m 文件)中声明,语法如下:
```objective-c
// MyClass.m
@interface MyClass () // 没有名字
@property (nonatomic, strong) NSString *secret; // 私有属性
- (void)privateMethod; // 私有方法
@end
@implementation MyClass
// 实现方法
@end

 从语法上来看,扩展相当于定义一个匿名的类别,但从用法来看,类别一般是有特定的.h 和.m 文件,扩展则用于临时对某个类的接口进行扩展,类实现部分同时实现类接口部分定义的方法和扩展中定义的方法。

类扩展可在主类编译时声明私有属性;Category 可声明属性元数据,但不能新增实例变量,也不会自动合成访问器实现。

#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

上面只是定义了一个 FKCar 接口,在该接口中定义了两个属性和一个方法,接下来对该类进行拓展

#import <Foundation/Foundation.h>
#import "FKCar.h"
NS_ASSUME_NONNULL_BEGIN
@interface FKCar ()
@property (nonatomic,copy) NSString *color;
- (void) drive: (NSString*) owner;
@end
NS_ASSUME_NONNULL_END

接下来是 FKCar 的实现部分:

#import "FKCar.h"
#import "FKCar+drive.h"
@implementation FKCar
@synthesize brand;
@synthesize model;
@synthesize color;
- (void) drive {
NSLog(@"汽车正在路上跑");
}
- (void) drive: (NSString*) owner {
NSLog(@"%@正驾驶着%@汽车在路上跑",owner,self);
}
- (NSString*) description {
return [NSString stringWithFormat:@"<FK[brand = %@,model = %@,color = %@",self.brand,self.model,self.color];
}
@end
#import <Foundation/Foundation.h>
#import "FKCar+drive.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
FKCar *car = [[FKCar alloc] init];
car.brand = @"宝马";
car.model = @"X5";
car.color = @"黑色";
[car drive];
[car drive: @"孙悟空"];
}
return 0;
}

类扩展与 Category 都使用 @interface 补充既有类的声明;Category 可为既有类增加方法并在运行时附加,类扩展通常用于同一类的私有声明。它们对实例变量布局的能力不同。

Category 可以声明属性元数据,但不会自动合成后备 ivar 或 getter/setter 实现;类扩展可声明私有属性,并在所属类实现中按编译器合成规则生成访问器和存储。

四、协议(protocol)与委托

4.1 协议

        类是一种具体实现体,而协议则是定义了一种规范,定义了某一批类所需要遵守的规范。协议不提供任何实现,它体现的是规范和实现分离的设计哲学。

        让规范和实现分离正是协议的好处,是一种松耦合的设计。

4.1.1 使用类别实现非正式协议

        这也是类别的作用之一。

NSObject 分类可用于声明非正式协议式的约定,但不会强制其他类实现这些方法;若需要编译期协议检查,应使用正式 @protocol。

以下用代码来演示非正式协议:

首先我们创建一个 NSObject 的类别,名为 EaTable,并且为其定义一个 taste 方法:

4.1.2 正式协议的定义

        定义正式协议的时候,不再使用 @interface 和 @implementation 关键字了,而是使用 @protocol 关键字,定义正式协议的语法如下:

@protocol ProtocolName <ParentProtocol1, ParentProtocol2>
- (void)methodName;
@end

注意:

        1、协议名应与类名采用相同的命名规则。即协议名应该由多个有意义的单词连接而成,每个单词首字母大写。

        2、一个协议可以有多个直接父协议,但协议只能继承协议,不能继承类。

        3、协议中定义的方法只有方法签名,没有方法实现,协议中包含的方法即可以是类方法也可以是实例方法。

        4、协议中所有方法都是公开的访问权限。

以下是三个定义正式协议的代码:

#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@protocol FKOutput
- (void) output;
- (void) addData(String msg);
@end
NS_ASSUME_NONNULL_END

上面定义了一个 FKOutput 协议,这个协议定义了两个方法,分别表示添加数据和输出数据。

#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@protocol FKProoductable
- (NSData*) getProductTime;
@end
NS_ASSUME_NONNULL_END

以上定义了一个 FKProductable 协议,其中定义了一个 getProductTime 方法,用来返回产品的生产时间。

#import <Foundation/Foundation.h>
#import "FKOutput.h"
#import "FKProductable.h"
NS_ASSUME_NONNULL_BEGIN
@protocol FKPrintable <FKOutput,FKProductable>
- (NSString*) printColor;
@end
NS_ASSUME_NONNULL_END

上面这个是一个打印机协议,该协议同时继承以上两个协议。

        协议的继承和类的继承不一样,协议完全支持多继承,即一个协议可以有多个直接的父协议。和类继承相似,子协议继承某个父协议,将会获得父协议中的所有方法。

        一个协议继承多个父协议时, 多个父协议排在 <> 中间,多个协议口见以(,)隔开。

4.1.3 遵守(实现)协议

        在类定义的接口部分可以指定该类继承的父类,以及遵守的协议,语法如下:

@interface 类名: 父类<协议1,协议2...>

        由上面的语法格式可以看出,一个类可以同时遵守多个协议。

        如果程序需要使用协议来定义变量,有如下两种语法:

        1、NSObject <协议 1, 协议 2…>* 变量;

2、id <协议 1, 协议 2…> 变量;

        通过上面的语法格式定义的变量,它们的编译时的类型仅仅只是所遵守的协议类型,因此只能调用该协议中定义的方法。

声明一个协议

@protocol MyDelegate <NSObject>
@required
- (void)didFinishTask;
@optional
- (void)didStartTask;
@end

如何让类遵守协议? 在类的声明中使用尖括号 <协议名> 来表示遵守

@interface Worker : NSObject <MyDelegate>
@end

** 然后在实现中写出协议方法 **

@implementation Worker
- (void)didFinishTask {
NSLog(@"Task finished!");
}
@end

如果你没有实现 @required 方法,** 编译器会报警告 **

使用协议的类如何调用这些方法?

@interface Manager : NSObject
@property (nonatomic, weak) id<MyDelegate> delegate;
- (void)doWork;
@end
@implementation Manager
- (void)doWork {
NSLog(@"Manager is working...");
if ([self.delegate respondsToSelector:@selector(didStartTask)]) {
[self.delegate didStartTask];
}
// 工作完成
if ([self.delegate respondsToSelector:@selector(didFinishTask)]) {
[self.delegate didFinishTask];
}
}
@end
#### 4.1.4 正式协议与非正式协议的差异
        1、非正式协议通过为NSObject创建类别来实现,而正式协议直接使用@protocol创建;
        2、遵守非正式协议通过继承带特定类别的NSObject来实现,而遵守正式协议则有专门的OC语法来实现;
正式协议中标记为 @required 的方法属于遵循者应实现的契约;@optional 方法可以不实现。
        在OC中还有两个关键字:
        1、**@optional**:位于该关键字只后、@required或@end之前声明的方法是可选的,实现类可选择是否实现这些方法。
        2、**@required**:位于该关键字之后、@optional或@end之前声明的方法是必需的,实现类必需实现这些方法。
        通过在正式协议中使用以上两个关键字,正式协议完全可以代替非正式协议的功能。
#### 4.1.5 协议与委托(delegate)
        定义协议的类可以把协议定义的方法委托给实现协议的类,这样可以让类定义具有更好的通用性质。
        更通用的,当应用程序启动时,应用程序启动的开始加载、加载完成等系列事件,都是委托给相应的代理对象完成的。
---
原文发布于 CSDN:[【OC】OC语言学习——面向对象(下)](https://blog.csdn.net/2402_86720949/article/details/147772892)

接着阅读