文章目录
接口与APi设计
使用前缀避免命名空间冲突
OC没有其他语言的那种内置的命名空间机制,所以我们需要设法去避免潜在的命名冲突。
比无法链接更严重的问题是运行期又载入了含有重名类的程序库,此时,动态加载器就遇到了重名符号错误,可能会使得程序崩溃
我们为所有名称都加上适当前缀会大幅度减小出现命名冲突的几率。苹果采用双字符前缀,我们采用三字符前缀。
提供全能初始化方法
可以位对象提供必要信息以便其能完成工作的初始化方法叫做全能初始化方法。

正如上述代码所展示的,initWithTimeIntercalSinceReferenceDate:是全能初始化方法。其余的初始化方法都要调用他,只有在全能初始化方法中才会存储内部数据,这样当底层数据存储机制改变时,只需要修改这里的代码就行。
如果子类的全能初始化方法与超类方法的名称不同,那么总应覆写超类的全能初始化方法。
全能初始化方法的调用链一定要维系,并且如果子类的全能初始化方法与超类方法的名称不同,那么总应覆写超类的全能初始化方法
实现description方法
如果我们直接输出对象方法的结果如下:
objc
<Person: 0x60000022b220>
我们可以通过重写description方法实现输出对象具体信息:
objc
- (NSString *)description {
return [NSString stringWithFormat:@"%@: %p, %@", [self class], self, @{@"firstName":_firstName, @"name":_name}];
}
输出结果:
Person: 0x6000002045c0, {
firstName = 222;
name = pop;
}
objc
- (NSString *)debugDescription;//使用控制台输出信息的方法,这里不做具体介绍
尽量使用不可变对象
设计类时,应当尽量使用属性来封装数据,而在使用属性时则可将其生命为只读。
objc
@property (nonatomic, strong, readonly)NSString* flag;
有时候我们想修改封装在对象内部的数据,但是却不想这些数据被外部修改,通常我们选择在对象内部将readonly属性重新生命为readwrite。如果这个属性生命为nonatomic,那么可能产生竞态条件。必要时我们可以通过派发队列等手段将所有数据的存取操作都设置为同步操作。
将属性在分类中重新声明,属性的其他特质必须保持不变。
这样声明的属性在对象的外部仍然能通过键值编码技术设置这些属性。
objc
[pointOfInterest setValue:@"abc" forKey:@"identifier"];
KVC会在类中查找setIdentifier方法,并借此修改属性。这样等同于绕过了本地所提供的API。
在定义类的公共API时,对象内部的各种colletion的属性设置也需要注意,给外界提供collection的返回方法时,尽量返回不可变类型,如果内部有需要设置为可变类型,那么在外部调用方法返回时,应该传入内部属性拷贝。具体我们可以看下面这个例子:
objc
#import <Foundation/Foundation.h>
@interface EOCPerson : NSObject
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSSet *friends;
- (id)initWithFirstName:(NSString *)firstName andLastName:(NSString *)lastName;
- (void)addFriend:(EOCPerson *)person;
- (void)removeFriend:(EOCPerson *)person;
@end
//.m文件
#import "EOCPerson.h"
@interface EOCPerson ()
@property (nonatomic, copy, readwrite) NSString *firstName;
@property (nonatomic, copy, readwrite) NSString *lastName;
@property (nonatomic, strong) NSMutableSet *internalFriends; // 内部持有的可变集合
@end
@implementation EOCPerson
- (NSSet *)friends {
return [_internalFriends copy]; // 返回不可变的 friends 集合
}
- (void)addFriend:(EOCPerson *)person {
[_internalFriends addObject:person]; // 添加好友
}
- (void)removeFriend:(EOCPerson *)person {
[_internalFriends removeObject:person]; // 移除好友
}
- (id)initWithFirstName:(NSString *)firstName andLastName:(NSString *)lastName {
if ((self = [super init])) {
_firstName = [firstName copy];
_lastName = [lastName copy];
_internalFriends = [NSMutableSet new];
}
return self;
}
@end
所以我们在编写代码时,尽量不要将可变的collection作为属性公开,而是应该提供相关方法来修改对象中的可变collection
使用清晰而协调的命名方式
-
方法与变量名使用小驼峰,类名大驼峰并且需要两三个前缀字母
-
方法名中不使用缩略后的类型名称
为私有方法名加前缀
为私有类方法名添加前缀有利于修改方法或方法签名,用p_做前缀挺好的
理解Objective-C错误模型
自动引用计数在默认情况下不是"异常安全的",就是说如果抛出异常,那么本应该在作用域末尾释放的对象不会自动释放了。
如果想生成异常安全的代码,可以通过编译器标志来实现,不过这将会引入一些额外的代码,在不抛出异常时,也照样要执行这部分代码,需要打开的编译器标志是-fobjc-arc-excepptions,我们在使用前需要权衡一下代码效率如何。
即使我们不用ARC也照样很难在抛出异常时不会导致内存泄漏的代码
OC现在的异常处理只适用于极其严重的错误,并且在异常抛出之后程序也就应该退出了。OC中没有办法标识抽象类,为了实现类似功能,最好的办法就是那些子类必须覆写的超类方法中抛出异常,这样的话,只要被使用就会抛出异常。
例如我们拥有一个抽象基类,用户不应该直接使用这个基类,而是继承使用其中的某个子类,在这种情况下,如果有人直接使用了这个抽象类,我们就可以抛出异常来提醒:
objc
// 抽象基类方法
- (void)someMethod {
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"This method must be overridden by a subclass" userInfo:nil];
}
这样做确保了以下几点:
- 强制继承:开发者如果直接使用抽象基类而没有继承的话,程序会通过异常通知其错误
- 提前捕获错误:通过抛出异常,能够及时捕获潜在的设计错误,避免后续逻辑
我们需要注意,异常不应当用于普通的错误处理,因为抛出异常和捕获异常的过程比常规的过程更加昂贵和复杂,用来处理严重的无法恢复的错误情况合适
在我们实际使用中,更多是使用NSError来实现错误信息的输出与传递,其是Objective-C中用于错误处理的标准类,主要组成部分如下:
- Error Domain(错误范围,字符串类型)
- 代表错误的发生范围,即产生错误的根源
- 通常使用特定的全局变量来表示,如
NSURLErrorDomain:专门表示URL加载、网络请求、URL解析等相关错误,NSCocoaErrorDomain:表示Cocoa框架内部的错误 - 例如当URL解析失败时,会使用NSURLErrorDomain来表示该错误来源于URL处理子系统
- Error Code(错误码,整数类型)
- 用于指明在某个错误范围内发生的具体错误,同一个范围内可能有不同的错误情况,通常使用enum定义。
- 在HTTP请求错误中,会返回对应的状态码,如404(Not Found)、500(Internal Server Error)等错误信息
- User Info(用户信息,NSDictionary类型)
- 用于存储有关错误的额外信息,便于调试处理。
- 常见键值对:
NSLocalizedDescriptionKey错误的本地话描述NSUnderlyingErrorKey表示导致当前错误的另一个错误,可用于构建错误链
我们可以为自己程序库/模块中产生的错误,定义一个专用的 NSError Domain 字符串作为错误范围标识
NSError通常使用委托协议来传递错误,当错误发生时,当前对象会将错误信息经由协议中的某个方法传给其代理对象,我们可以看一下下面这个委托模式下错误传递的例子:
objc
#import <Foundation/Foundation.h>
@interface MyConnectionDelegate : NSObject <NSURLConnectionDelegate>
@end
@implementation MyConnectionDelegate
// 连接失败时回调此方法,并传递 NSError
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
NSLog(@"网络请求失败,错误信息: %@", error.localizedDescription);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 创建请求
NSURL *url = [NSURL URLWithString:@"https://invalid.url"]; // 一个无效URL,强制产生错误
NSURLRequest *request = [NSURLRequest requestWithURL:url];
// 创建代理对象
MyConnectionDelegate *delegate = [[MyConnectionDelegate alloc] init];
// 使用 NSURLConnection 发送请求
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:delegate];
// 运行 RunLoop,以便异步回调能执行
[[NSRunLoop currentRunLoop] run];
}
return 0;
}
我们也可以通过方法的输出参数返回给调用者
objc
- (BOOL)doSomething:(NSError**)error {
if (/*发生错误*/) {
if (error) {
*error = [NSError errorWithDomain:@"com.example.domain" code:1001 userInfo:@{NSLocalizedDescriptionKey: @"发生了错误"}];
}
return NO;
} else {
return YES;
}
}
传递给方法的参数是一个指针,然而指针本身又指向另一个指针,那个指针指向NSError对象。
实际上,在使用ARC时,编译器会将方法签名中的NSError**转换为NSError*__autoreleasing*,这是一个自动释放对象的二级指针,对于这类输出方法来说,他们自己本身应该不具有对于外部对象的生命周期的控制权。
理解NSCopying协议
NSCopying协议只有一个方法:
objc
- (id)copyWithZone:(NSZone* )zone;
以前在开发程序时,会将内存分为不同的区,而对象的创建会在某个区内部,现在只有一个默认区。
我们想覆写copy方法,真正需要实现的是copyWithZone:方法。如果想要某个类实现拷贝功能,只需要该类遵从NSCopying协议,并实现对应方法。
objc
#import <Foundation/Foundation.h>
@interface EOCPerson : NSObject <NSCopying>
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
- (id)initWithFirstName:(NSString *)firstName andLastName:(NSString *)lastName;
@end
objc
#import "EOCPerson.h"
@implementation EOCPerson
- (id)initWithFirstName:(NSString *)firstName andLastName:(NSString *)lastName {
self = [super init];
if (self) {
_firstName = [firstName copy];
_lastName = [lastName copy];
}
return self;
}
- (id)copyWithZone:(NSZone *)zone {
EOCPerson *copy = [[[self class] allocWithZone:zone] initWithFirstName:_firstName andLastName:_lastName];
return copy;
}
@end
上述例子中,我们直接将待拷贝的对象交给全能初始化方法,令其执行所有的初始化工作(通常情况)。有时候我们还需要完成其他的工作,比如对象的数据结构可能并未在初始化方法中设置好,需要另行设置。
NSMutableCopying协议与上剋死,也只定义了一个方法:
-(id)mutableCopyWithZone:(NSZone* )zone
我们在面对容器类的深浅拷贝时,要根据情况而定。

objc
- (instancetype)initWithArray:(NSArray<ObjectType> *)array copyItems:(BOOL)flag;
如果copyItems参数为yes,这个方法会给数组中的每个对象发生copy,这个方法才是容器的一个深拷贝,而采用普通的copy方式只是一个浅拷贝,不会拷贝每一个set中的元素