Effective Objective-C 2.0 读书笔记—— 接口与API设计
Effective Objective-C 2.0 读书笔记—— 接口与API设计
文章目录
- Effective Objective-C 2.0 读书笔记—— 接口与API设计
- 1. 用前缀避免命名空间冲突
- 2.提供"全能初始化方法"
- 3.实现description方法
- 4.尽量使用不可变对象
- 5.理解Objective -C错误模型
1. 用前缀避免命名空间冲突
正如书上所说OC没有其他语言之中内置的命名空间机制,所以我们在对方法进行命名时,就需要设法避免潜在的命名冲突。我们先了解一下命名空间冲突是什么东西。
假设有两个不同的库或模块,它们各自定义了一个名为 MyClass
的类:
// 第一个库
@interface MyClass : NSObject
// ...
@end
// 第二个库
@interface MyClass : NSObject
// ...
@end
当这两个库被同时引入同一个项目之中,那么编译器就会发现MyClass的定义重复,进而导致编译阶段发生错误。
那么避免这个问题的唯一办法就是变相实现命名空间,Apple宣称其保留使用所有 “两字母前缀” (two-letter prefix)的权利,所以一般来说我们自己写的类的前缀为三个字母。假设你所在的公司叫做Effective Widgets,那么就可以在所有应用程序都会用到的那部分代码中使用EWS 作前缀。
不仅是类名,应用程序中的所有名称都应加前缀。如果要为既有类新增“分类” (category),那么一定要给“分类” 及“分类” 中的方法加上前缀。除了以上这些我们算是比较熟知的内容会产生冲突时,书中也说到了,那就是类的实现文件中所用的 纯C函数及全局变量。
假设我们在两个不同的源文件中都有一个名为 completion
的函数。它们可能都在各自的 .m
文件中定义,例如:
// EOCSoundPlayer.m
void completion(SystemSoundID ssID, void *clientData) {
EOCSoundPlayer *player = (__bridge EOCSoundPlayer*)clientData;
if ([player.delegate respondsToSelector:@selector(soundPlayerDidFinish:)]) {
[player.delegate soundPlayerDidFinish:player];
}
}
// AnotherFile.m
void completion(SystemSoundID ssID, void *clientData) {
// 另一个处理
}
由于C语言函数并没有所谓类的归属关系,只要链接这两个文件,编译器就会发现两个相同的completion函数,符号相同所以不知道调用哪一个。
解决这个问题除了我们刚刚说到的使用命名空间,即添加前缀的方式来解决之外,也可以使用静态函数:
- 如果某个函数只在单个文件中使用,你可以将该函数声明为
static
,这样它的符号仅对当前源文件可见,其他文件无法访问它。
static void completion(SystemSoundID ssID, void *clientData) {
// 仅对当前文件可见
}
另外,当我们在编写第三方库时,如果引用了其他第三方库,就要把其他的第三方库的前缀也进行修改,那一份
第 三方库代码都加上你自己的前缀。例如,Application 你准备发布的程序库叫做EOCLibrary,其中引 入 了 名 为 XYZLibrary的第三方库 , 那么就应该把XYZLibrary 中的所有名字都冠以 EOC。于是,应用程序就可以随意使用它自已直接引人的那个XYZLi br ary 库 了,而不必担心与EOCLibrary 里的这个XYZLibrary相冲突
2.提供"全能初始化方法"
所有对象均要初始化。在初始化时,有些对象可能无须开发者向其提供额外信息, 不过一般来说还是要提供的。通常情况下,对象若不知道必要的信息,则无法完成其工作。我们把这种可为对象提供必要信息以便其能完成工作的初始化方法叫做“全能初始化方法” (designated initializer)
如果创建实例的方法不止一种,我们还是需要选中一个作为全能初始化方法,书中使用NSDate举例
- (id)init;
- (id)initWithString:(NSString *)string;
- (id)initWithTimeIntervalSinceNow:(NSTimeInterval)seconds;
- (id)initWithTimeInterval:(NSTimeInterval)seconds sinceDate:(NSDate *)refDate;
- (id)initWithTimeIntervalSinceReferenceDate:(NSTimeInterval)seconds;
- (id)initWithTimeIntervalSince1970:(NSTimeInterval)seconds;
在官方的文档所述的一样,在上面几个初始化方法中,initWithTimeInt ervalSinceRef erenceDate:
是全能初始化方法。也就是说,其余的初始化方法都要调用它。于是,只有在全 能初始化方法中,才会存储内部数据。这样的话,当底层数据存储机制改变时,只需修改此方法的代码就好,无须改动其他初始化方法。
编写一个表示矩形的类,- (instancetype)initWithWidth:(CGFloat)width height:(CGFloat)height;
就作为全能初始方法
#import <Foundation/Foundation.h>
@interface Rectangle : NSObject
@property (nonatomic, assign) CGFloat width;
@property (nonatomic, assign) CGFloat height;
// 全能初始化方法
- (instancetype)init;
- (instancetype)initWithWidth:(CGFloat)width height:(CGFloat)height;
- (instancetype)initWithRect:(CGRect)rect;
@end
.m文件实现如下
#import "Rectangle.h"
@implementation Rectangle
// 默认初始化方法
- (instancetype)init {
self = [super init];
if (self) {
return [self initWithWidth:5.0f andHeight :10.0f];
}
}
// 通过宽度和高度初始化矩形——全能初始化方法
- (instancetype)initWithWidth:(CGFloat)width andHeight:(CGFloat)height {
self = [super init];
if (self) {
_width = width;
_height = height;
}
return self;
}
// 通过正方形的边长初始化矩形(宽度和高度相同)
// 通过CGRect初始化矩形
- (instancetype)initWithRect:(CGRect)rect {
self = [super init];
if (self) {
return [self initWithWidth:rect.size.width andHeight :rect.size.height];
}
return self;
}
@end
我们首先定义了- (instancetype)initWithWidth:(CGFloat)width height:(CGFloat)height;
的方法作为全能的初始化方法,然后就在剩下的初始化方法之中调用我们的全能初始化方法,当底层数据存储机制改变时,只需修改此 方法的代码就好,无须改动其他初始化方法。
接着我们写一个正方形的类
#import "EOCRectangle.h"
@interface EOCSquare : EOCRectangle
- (id) initWithDimension: (float) dimension;
@end
@implementation EOCSquare
- (id) initWithDimension: (float) dimension (
return [super initWithWidth:dimension andHeight:dimension) ; }
@end
由于是继承于矩形类,我们在自定义正方形的初始化方法时,也要注意到我们也有可能调用父类的初始化方法,为了避免出现未知的问题,我们需要重写父类之中的- (instancetype)initWithWidth:(CGFloat)width andHeight:(CGFloat)height
方法
- (id) initWithWidth: (float) width andHeight: (float) height {
float dimension = MAX (width, height) ;
return [self initWithDimension:dimension] ;
}
有时我们不想覆写超类的全能初始化方法不想令 initWithWidth:andHeight:
方法以其两参数中较大者作边长来初始化EOCSquare对象,我们认为这是方法调用者自己犯了错误。在这种情况下,常用的办法是覆写超类的全能 初始化方法并 于其中抛出异常:
-(id) initWithWidth: (float)width andHeight: (float) height {
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason: @"Must use initWithDimension: instead." userInfo:nil];
}
不过,在Objective-C程序中,只有当发生严重错误时,才应该抛出异常,所以,初始化方法抛出异常乃是不得已之举,表明实例真的没办法初始化了。
3.实现description方法
description方法在平时应用的很多,这里不做多做赘述,书中作者讲了几个小技巧
当一个类有许多属性需要被打印时,我们可以使用NSDictionary来实现进行打印,例如
- (NSString*)description {
return [NSString stringWithFormat:@"<%@: %p, %@>",
[self class], // 类名
self, // 当前对象的内存地址
@{ @"title": _title,
@"latitude":@(_latitude),
@"longitude":@(_longitude)}
];
}
打印出来结果如下
location = <EOCLocation: 0x7f98f2e01d20, {
latitude = "51.506";
longitude = 0;
title = London;
}>
4.尽量使用不可变对象
这个点我在通过CoreLocation Framework深入了解MVC架构之中有过了解,我们在设置一个类持有的属性时,需要注意到类外部尽量不能直接操作类之中的核心属性,举一个例子:
NSMutableSet *set = [NSMutableSet new] ;
NSMutableArray *arrayA = [@[@1, @2]mutableCopy];
[set addObject:arrayA];
NSLog(@"set = %@", set);
// Output: set = {((1,2)) }
我们知道NSMutableSet的性质,当我们加入一个一模一样的元素时,set 里仍然只有一个对象,因为刚才要加入的那个数组对象和set 中已有的数组对象相等,所以set 并不会改变。我向set添加一个数组B,出现以下结果
程序是通过创建时这个数组的哈希值来判断两者是否相同
NSMutableArray *arrayB = [@[@1, @2]mutableCopy];
[set addobject :arrayB];
NSLog (@"set = %@", set) ;
// Output: set = ( ( (1,2))
但是我们如果绕一个弯,那么情况又会变得不同
NSMutableArray*arrayC= [@[@1] mutableCopy];
[set addObject:arrayC];
NSLog (@"set = %@", set) ;
// Output: set = {((1), (1,2))}
我们改变arrayC的内容,令其和最早加入set 的那个数组相等
[arrayC addObject: @2];
NSLog (@"set = %@", set) ;
// Output: set = {((1,2), (1,2) )}
用这个例子举例说明,轻易的使得属性可以被修改,可能会产生未知的错误,而且不易于项目的维护。
那么我们要做的其实就是让这些属性在外部访问的时候是只读(readonly),就可以保证我们的类之中的数据都是互相协调的。
那在类中,我们可能需要对属性进行修改,那我们就在.m之中将属性重新命名为可读写,举个例子,在.h文件当中
@property (nonatomic, readonly) NSString *identifier;
@property (nonatomic, readonly) NSString *title;
@property (nonatomic, readonly) float latitude;
@property (nonatomic, readonly) float longitude;
这些属性在类的外部只能被访问而不能修改,那么在.m文件当中,我们再赋予这个属性可以读写
@interface EOCPointOfInterest ()
@property (nonatomic, copy, readwrite) NSString *identifier;
@property (nonatomic, copy, readwrite) NSString *title;
@property (nonatomic, assign, readwrite) float latitude;
@property (nonatomic, assign, readwrite) float longitude;
@end
虽说这样在一定程度上可以使得外部不能直接修改这些属性,但我们仍然能够使用KVC的相关方法来进行修改,由于KVC是通过直接在类中查找并修改属性,所以直接避免了调用类本身提供的API。
另外,当我们在类之中带有数组这一类collection
的时候,一般来说,内部就是一个可以灵活添加的collection
,而外部返回的时候他们的不可变版本,即内部collection
的拷贝
再举书中的例子
#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
这个类通过 readonly
属性保证了 firstName
和 lastName
不能被外部修改,只能在初始化时被赋值。同时,朋友的集合通过 addFriend:
和 removeFriend:
方法进行管理,但外部只能访问不可变的 NSSet
类型的集合。这样可以保持封装性和安全性。
5.理解Objective -C错误模型
当前很多种编程语言都有“异常” (exception)机制,Objective-C也不例外。OC特有的 “ 自动引用计数 ”,在默认情况下不是 “ 异常安全的 ” (e x c e p t i o n s a f e )。具体来说,这意味着 : 如果抛出异常 ,那么本应在作用域未尾释放的对象现在却不会自动释放了。
如果想生成**“异常安全”**的代码,可以通过设置编译器的标志 -fobjc-arc-exceptions
来实现。
不过,这将引入一些额外的代码,即使在不抛出异常的情况下,这部分代码仍然会执行。因此,使用该选项需要权衡代码执行效率和异常处理的安全性。
在 Objective-C 中,异常机制主要用于处理极其严重的错误,而不是用于常规的错误处理。这与其他语言不同,Objective-C 没有类似于 abstract class
的概念来标识某个类为抽象类。因此,为了实现类似的功能,我们可以通过在父类的关键方法中抛出异常,来确保子类必须实现该方法。
例如,如果你有一个抽象基类,应该确保用户不能直接实例化这个类,而是通过子类来使用。如果有人错误地尝试直接使用这个基类并调用必须被子类重写的方法,你可以通过抛出异常来提醒开发者:
// 抽象基类方法
- (void)someMethod {
@throw [NSException exceptionWithName:NSInvalidArgumentException
reason:@"This method must be overridden by a subclass"
userInfo:nil];
}
在上述例子中,someMethod
是一个抽象方法,应该在子类中实现。如果有人直接调用了这个基类的 someMethod
,程序将抛出一个异常,说明该方法必须由子类来实现。
这种做法确保了以下几点:
- 强制继承:开发者如果直接使用抽象基类而没有继承,程序会通过异常通知其错误。
- 提前捕获错误:通过抛出异常,你能够及时捕获潜在的设计错误,避免后续的逻辑错误。
需要注意的是,异常不应当用于普通的错误处理,因为抛出异常和捕获异常的过程比常规的条件判断更为昂贵和复杂。在生产代码中,异常应当仅用于严重的、无法恢复的错误情况。
在我们的实际应用之中,我们更多的是使用NSError来进行错误报告
NSError
是 Objective-C 中用于错误处理的标准类,它由以下三个核心部分组成:
- Error Domain(错误范围,类型:字符串)
- 代表错误的发生范围,即产生错误的根源。
- 通常使用特定的全局变量来定义,比如:
NSURLErrorDomain
(表示 URL 处理相关的错误)。NSCocoaErrorDomain
(表示 Cocoa 框架内部的错误)。
- 例如,当 URL 解析失败时,会使用
NSURLErrorDomain
来表示该错误来源于 URL 处理子系统。
- Error Code(错误码,类型:整数)
- 用于指明在某个错误范围内发生的具体错误。
- 同一错误范围内,可能有多个不同的错误情况,通常使用
enum
定义。 - 例如,在
NSURLErrorDomain
下:NSURLErrorNotConnectedToInternet
(表示网络未连接)。NSURLErrorTimedOut
(表示请求超时)。
- 在 HTTP 请求错误中,错误码可能会直接对应 HTTP 状态码,如 404(Not Found)、500(Internal Server Error)等。
- User Info(用户信息,类型:NSDictionary)
- 存储有关错误的额外信息,便于调试和错误处理。
- 常见的键值对:
NSLocalizedDescriptionKey
:错误的本地化描述(例如:“请求超时”)。NSUnderlyingErrorKey
:表示导致当前错误的另一个错误,可用于构建“错误链”(Chain of Errors)。
NSError 的第一种常见用法是通过委托协议来传递此错误。有错误发生时,当前对象会把错误信息经由协议中的某个方法传给其委托对象 (delegate)。例如, NSURLConnection 在其委托协议NSURLConnectionDelegate 之中就定义了如下方法: - (void) connection: (NSURLConnection *) connection didFail WithError: (NSError *)error
。虽然 NSURLConnection
已被 NSURLSession
取代,但它仍然可以用于理解 委托模式下的错误传递。
#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;
}
还有另一种方法是经由方法的 “输出参数” 返回给调用者,
- (BOOL) doSomething: (NSError**)error
- (BOOL)doSomething:(NSError **)error {
// 执行可能会出错的操作
if (/* 发生错误 */) {
if (error) {
// 如果传入的 error 参数不为空,则通过 *error 传递错误
*error = [NSError errorWithDomain:@"com.example.domain"
code:1001
userInfo:@{NSLocalizedDescriptionKey: @"发生了一个错误"}];
}
// 返回 NO,表示操作失败
return NO;
} else {
// 返回 YES,表示操作成功
return YES;
}
}
对于错误码,我们可以自己设置枚举量,规范错误的类型。