Objective-C编程深入解析
1. 实例方法
在面向对象编程中,核心是将责任转移到对象上。我们可以要求对象执行的任务就是它们的实例方法。最常见的实例方法用于直接访问和修改对象的实例变量,技术上称为访问器和修改器,Apple更常用的说法是setter和getter。
1.1 Getter和Setter的定义
- Getter :在Objective - C中,getter的名称和返回类型与它访问的实例变量相同,并且不接受任何参数。例如:
- (NSString *)name;
{
return name;
}
-
Setter
:setter在名称前加上
set前缀,接受一个与实例变量类型相同的参数,并且不返回任何值。由于内存管理的原因,setter稍微复杂一些,示例代码如下:
- (void)setName:(NSString *)aName;
{
if (name == aName)
return;
[name release];
name = [aName retain];
}
1.2 Setter参数命名注意事项
Setter的参数不能与实例变量同名,因为如果同名会导致判断条件
if (name == name)
这样的混淆情况。而在Java中,可以使用
this
关键字来区分,但Objective - C没有这个特性。
1.3 封装的重要性
虽然直接访问变量看起来更简单,但这违反了封装原则。封装是面向对象编程的重要部分,它要求隐藏实现细节,这样可以防止对象之间的纠缠,保护程序员免受实现更改的影响。例如,对于
age
的setter:
// 简单版本
- (void)setAge:(NSUInteger)anAge;
{
age = anAge;
}
// 增加验证逻辑版本
- (void)setAge:(NSUInteger)anAge;
{
if (anAge > 0 && anAge < 100)
age = anAge;
}
如果遵循封装原则,只需修改setter方法;如果直接设置变量,则需要进行大量的实现工作。
2. 协议
Objective - C采用单继承模型,为了克服单继承的限制并增加灵活性,类可以通过遵循任意数量的协议来继承额外的接口。
2.1 协议的定义和使用
协议类似于类,但不包含变量或提供实现,只是定义一个接口,遵循该协议的类需要实现这些接口。例如,Foundation定义了
NSCopying
协议,如果
BMPerson
类实现了
copy
方法,可以在类声明中添加该协议:
@interface BMPerson : NSObject <NSCopying>
实际上,调用
- (id)copy
时,是调用
- (id)copyWithZone:(NSZone *)zone
,遵循
NSCopying
协议需要实现
copyWithZone:
方法。
2.2 自定义协议
可以自定义协议,例如定义一个提供唯一ID的通用接口:
@protocol BMIdentifying
- (NSString *)uniqueIDString;
@end
如果
BMPerson
实现了
uniqueIDString
,可以在协议列表中添加:
@interface BMPerson : NSObject <NSCopying, BMIdentifying>
还可以在方法签名中使用协议作为类型:
- (void)addIdentifyingObject:(id <BMIdentifying>)object;
调用该方法时,可以进行协议类型的转换:
[listOfIdentifyingObjects addIdentifyingObject:(id <BMIdentifying>)object];
2.3 Cocoa中协议的应用
Cocoa使用正式和非正式协议实现了多种与类无关的模式,如下表所示:
| 模式 | 说明 |
| ---- | ---- |
| 插件 | 通过创建插件架构,程序员可以将应用程序开放给第三方开发,使用协议为插件开发者提供设计自由 |
| 委托和数据源 | 对象的行为和内容需要可配置,委托和数据源模式将决策留给另一个对象,避免大量使用子类化 |
| 通知 | 在某些操作前后调用方法,程序员可以根据通知类的操作同步其他对象的操作 |
| 快速枚举 | Objective - C 2.0引入统一、简洁且高度优化的
for...in
循环,集合通过遵循
NSFastEnumeration
协议实现快速枚举 |
| 键值编码和键值观察 | 除了调用显式方法,还可以通过名称访问对象的属性,并观察属性的变化 |
| 脚本化 | 应用程序需要实现某些额外方法才能进行脚本化,还有一些可选方法处理对象使用和脚本使用之间的不匹配 |
| 可访问性 | 遵循可访问性协议,应用程序不仅可供残疾用户使用,还可参与用户界面脚本编写 |
| 动画 | Cocoa的Core Animation包装通过协议实现,减少对现有框架的更改,扩展开发者自定义类的可用性 |
| 拼写检查 | 通过实现拼写协议,开发者可以将系统级的拼写和语法检查功能集成到自己的应用中 |
| 锁定 | 锁定协议为线程安全类定义标准接口,用于锁定和解锁 |
| 粘贴板操作 | 正式和非正式协议定义了粘贴板操作的各个部分,包括提供数据、接收数据和数据格式 |
在Leopard之前,所有协议方法都是必需的,可选接口通过非正式协议(实际上是
NSObject
的类别)实现。Objective - C 2.0引入了
@optional
和
@required
编译器指令,消除了对非正式协议的需求。
3. Objective - C动态运行时
Objective - C具有很强的动态性,多态性使得不同对象可以以不同方式响应相同的消息。与Java和C++等语言不同,Objective - C在运行时才真正发挥作用,大部分代码在执行前不会被锁定,这赋予了它强大的能力。
3.1 消息传递机制
Objective - C不寻常的消息传递语法是其动态性的核心。例如:
[myObject setColorWithRed:0.4 green:0.3 blue:0.0 alpha:1.0];
myObject
是接收者,
setColorWithRed:0.4 green:0.3 blue:0.0 alpha:1.0
是消息。运行时会将消息拆分为方法签名和参数数组,每个方法签名会解析为一个选择器(
SEL
类型),运行时动态地将选择器与方法实现(
IMP
类型)配对。
3.2 方法名说明
方法的名称是方法声明,包括冒号,但不包括参数名称或类型。例如,
setColorWithRed:0.4 green:0.3 blue:0.0 alpha:1.0
调用的方法名称是
setColorWithRed:green:blue:alpha:
。
下面是消息传递的流程mermaid图:
graph LR
A[发送消息] --> B[运行时拆分消息]
B --> C[解析方法签名为选择器]
C --> D[动态配对选择器和方法实现]
D --> E[执行方法]
4. 类别
在开发中,即使库的设计者考虑到了所有可能的方法,这也不是一个好主意,因为会导致类变得难以使用和性能下降。虽然可以通过子类化来扩展现有类,但有时这会带来很多问题。
4.1 子类化的问题
以
NSString
为例,它提供了
stringByAppendingString
方法,但没有
stringByPrependingString
方法。尝试子类化
NSString
创建
BMString
类:
#import <Cocoa/Cocoa.h>
@interface BMString : NSString
- (NSString *)stringByPrependingString:(NSString *)aString;
@end
@implementation BMString
- (NSString *)stringByPrependingString:(NSString *)aString;
{
return [aString stringByAppendingString:self];
}
@end
但子类化
NSString
存在很多隐藏问题,如继承不完美,
NSString
实际上是一个类簇的接口等,并且使用自定义子类时需要时刻记住使用它。
4.2 类别介绍
类别是在现有类上定义的额外方法,作为一种组织工具,它可以让代码的组织更符合开发者的需求。例如,可以将一个非常大的类拆分成多个文件,为不同功能区域使用单独的头文件。同时,也可以一次性为多个类添加功能,新功能的使用者只需导入新的头文件,旧类可以忽略它。
4.3 类别定义和使用
使用类别为
NSString
添加
stringByPrependingString
方法的示例如下:
#import <Cocoa/Cocoa.h>
@interface NSString (BMString)
- (NSString *)stringByPrependingString:(NSString *)aString;
@end
@implementation NSString (BMString)
- (NSString *)stringByPrependingString:(NSString *)aString;
{
return [aString stringByAppendingString:self];
}
@end
使用时:
NSString *greeting = @", world!";
greeting = [greeting stringByPrependingString:@"Hello"];
NSLog(greeting);
与子类不同,类别可以访问
NSString
类的所有内容,包括私有成员。
类别和协议的对比列表如下:
| 对比项 | 类别 | 协议 |
| ---- | ---- | ---- |
| 功能 | 为现有类添加实现和方法 | 定义接口,让类实现 |
| 灵活性 | 可在已编译的类上添加功能 | 类遵循协议实现接口 |
| 与继承关系 | 不依赖继承 | 与继承共同提供扩展能力 |
5. 方法交换
虽然类别可以为类添加方法,但继承模型提供了类别没有的功能,即子类可以覆盖父类的实现。Objective - C允许我们交换任何类的实现,这就是方法交换(swizzling)。
5.1 方法交换示例
以
NSXMLNode
的
description
方法为例,它输出的XML没有空格,难以阅读。我们可以创建一个类别,定义一个新方法
prettyDescription
,并将其实现与
description
方法的实现进行交换:
#import <Cocoa/Cocoa.h>
#import <objc/runtime.h>
@interface NSXMLNode (BMPrettyDescriptions)
- (NSString *)prettyDescription;
@end
@implementation NSXMLNode (BMPrettyDescriptions)
+ (void)load;
{
Method oldMethod = class_getInstanceMethod(self, @selector(description));
Method newMethod = class_getInstanceMethod(self, @selector(prettyDescription));
method_exchangeImplementations(oldMethod, newMethod);
}
- (NSString *)prettyDescription;
{
return [self XMLStringWithOptions:NSXMLNodePrettyPrint];
}
@end
使用交换后的方法:
NSXMLNode *xmlNode;
// ... load the XML document
NSLog(@"Current node: %@", xmlNode);
这样输出的XML就会有良好的格式。
方法交换的流程mermaid图如下:
graph LR
A[定义新方法] --> B[获取旧方法和新方法]
B --> C[交换方法实现]
C --> D[调用原方法时执行新方法实现]
通过以上这些特性,Objective - C在编程中展现出了强大的灵活性和扩展性,开发者可以根据不同的需求选择合适的方式来实现功能。
6. 内存管理与实例方法
在Objective - C中,内存管理是一个重要的话题,特别是在处理实例方法时。前面提到的setter方法,由于涉及到内存管理,其实现会比getter方法复杂一些。
6.1 Setter内存管理分析
回顾之前的
setName
方法:
- (void)setName:(NSString *)aName;
{
if (name == aName)
return;
[name release];
name = [aName retain];
}
这里的内存管理步骤可以详细解释如下:
1.
检查是否为同一对象
:
if (name == aName)
检查新传入的对象和当前对象是否相同,如果相同则直接返回,避免不必要的内存操作。
2.
释放旧对象
:
[name release]
释放当前对象的引用,减少其引用计数。如果引用计数降为0,则对象会被销毁。
3.
保留新对象
:
name = [aName retain]
将新对象的引用赋值给实例变量,并增加新对象的引用计数,确保对象不会被意外销毁。
6.2 内存管理与封装的关系
内存管理和封装是紧密相关的。如果不使用setter和getter方法,直接访问实例变量,可能会导致内存管理混乱。例如,如果直接修改实例变量而不进行适当的内存管理,可能会导致内存泄漏或野指针问题。而使用setter和getter方法可以将内存管理的逻辑封装在方法内部,提高代码的安全性和可维护性。
下面是一个关于内存管理操作的步骤列表:
| 步骤 | 操作 | 说明 |
| ---- | ---- | ---- |
| 1 | 检查对象是否相同 | 避免不必要的内存操作 |
| 2 | 释放旧对象 | 减少旧对象的引用计数 |
| 3 | 保留新对象 | 增加新对象的引用计数 |
7. 协议的深入应用
协议在Objective - C中有着广泛的应用,除了前面提到的各种模式,还有一些实际的使用场景和注意事项。
7.1 协议的组合使用
在实际开发中,一个类可以遵循多个协议,以实现多种功能。例如,
BMPerson
类可以同时遵循
NSCopying
和
BMIdentifying
协议:
@interface BMPerson : NSObject <NSCopying, BMIdentifying>
这样,
BMPerson
类就需要实现
copyWithZone:
和
uniqueIDString
方法。
7.2 协议作为方法参数类型
使用协议作为方法参数类型可以提高代码的灵活性和可扩展性。例如:
- (void)addIdentifyingObject:(id <BMIdentifying>)object;
这个方法接受一个遵循
BMIdentifying
协议的对象作为参数,调用时可以传入任何实现了该协议的对象:
[listOfIdentifyingObjects addIdentifyingObject:(id <BMIdentifying>)object];
7.3 协议的可选方法
在Objective - C 2.0中引入了
@optional
和
@required
编译器指令,使得协议方法可以分为可选和必需的。例如:
@protocol BMOptionalIdentifying
@required
- (NSString *)requiredIDString;
@optional
- (NSString *)optionalIDString;
@end
遵循该协议的类必须实现
requiredIDString
方法,而
optionalIDString
方法可以选择实现。
以下是一个协议使用的流程mermaid图:
graph LR
A[定义协议] --> B[类遵循协议]
B --> C[实现必需方法]
C --> D{是否有可选方法}
D -- 是 --> E[选择实现可选方法]
D -- 否 --> F[完成协议实现]
E --> F
8. 类别与代码组织
类别在代码组织方面有着独特的优势,它可以让代码更加模块化和易于维护。
8.1 大类别拆分
对于一个非常大的类,可以使用类别将其拆分成多个文件,每个文件负责不同的功能。例如,一个
LargeClass
可以拆分成以下几个类别:
// LargeClass+Feature1.h
@interface LargeClass (Feature1)
- (void)feature1Method;
@end
// LargeClass+Feature1.m
@implementation LargeClass (Feature1)
- (void)feature1Method {
// 实现代码
}
@end
// LargeClass+Feature2.h
@interface LargeClass (Feature2)
- (void)feature2Method;
@end
// LargeClass+Feature2.m
@implementation LargeClass (Feature2)
- (void)feature2Method {
// 实现代码
}
@end
这样,不同功能的代码可以分开管理,提高代码的可读性和可维护性。
8.2 多类功能添加
类别还可以一次性为多个类添加功能。例如,为
NSString
和
NSArray
添加一个通用的日志方法:
// LoggingCategory.h
@interface NSString (Logging)
- (void)logString;
@end
@interface NSArray (Logging)
- (void)logArray;
@end
// LoggingCategory.m
@implementation NSString (Logging)
- (void)logString {
NSLog(@"String: %@", self);
}
@end
@implementation NSArray (Logging)
- (void)logArray {
NSLog(@"Array: %@", self);
}
@end
使用时,只需导入
LoggingCategory.h
头文件,
NSString
和
NSArray
对象就可以调用相应的日志方法。
以下是类别使用的好处列表:
| 好处 | 说明 |
| ---- | ---- |
| 代码模块化 | 将大类拆分成多个文件,便于管理 |
| 功能扩展 | 为现有类添加新功能 |
| 代码复用 | 一次性为多个类添加相同功能 |
9. 方法交换的注意事项
方法交换是一个强大的技术,但也需要谨慎使用,否则可能会导致一些难以调试的问题。
9.1 方法交换的副作用
方法交换会改变原方法的行为,可能会影响到其他依赖原方法的代码。例如,如果在一个广泛使用的类上进行方法交换,可能会导致其他部分的代码出现意外的结果。
9.2 线程安全问题
方法交换操作应该在合适的时机进行,通常在类加载时(如
+ (void)load
方法)进行。如果在多线程环境下进行方法交换,可能会导致线程安全问题。
9.3 备份原方法
在进行方法交换时,建议备份原方法的实现,以便在需要时可以恢复。例如:
#import <Cocoa/Cocoa.h>
#import <objc/runtime.h>
@interface NSXMLNode (BMPrettyDescriptions)
- (NSString *)prettyDescription;
@end
@implementation NSXMLNode (BMPrettyDescriptions)
+ (void)load;
{
Method oldMethod = class_getInstanceMethod(self, @selector(description));
Method newMethod = class_getInstanceMethod(self, @selector(prettyDescription));
IMP originalIMP = method_getImplementation(oldMethod);
method_exchangeImplementations(oldMethod, newMethod);
// 可以保存originalIMP用于恢复
}
- (NSString *)prettyDescription;
{
return [self XMLStringWithOptions:NSXMLNodePrettyPrint];
}
@end
下面是一个方法交换注意事项的表格:
| 注意事项 | 说明 |
| ---- | ---- |
| 副作用 | 可能影响其他依赖原方法的代码 |
| 线程安全 | 在合适的时机进行交换,避免多线程问题 |
| 备份原方法 | 保存原方法实现,以便恢复 |
通过对Objective - C中实例方法、协议、动态运行时、类别和方法交换等特性的深入了解和合理应用,开发者可以更加灵活地编写代码,提高代码的质量和可维护性。同时,在使用这些特性时,需要注意相关的细节和潜在的问题,确保代码的稳定性和可靠性。
超级会员免费看
37

被折叠的 条评论
为什么被折叠?



