Effective Objective-C 2.0 读书笔记—— 接口与API设计

ops/2025/2/8 18:35:27/

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相冲突

image-20250202152943881

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 属性保证了 firstNamelastName 不能被外部修改,只能在初始化时被赋值。同时,朋友的集合通过 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:NSInvalidArgumentExceptionreason:@"This method must be overridden by a subclass"userInfo:nil];
}

在上述例子中,someMethod 是一个抽象方法,应该在子类中实现。如果有人直接调用了这个基类的 someMethod,程序将抛出一个异常,说明该方法必须由子类来实现。

这种做法确保了以下几点:

  1. 强制继承:开发者如果直接使用抽象基类而没有继承,程序会通过异常通知其错误。
  2. 提前捕获错误:通过抛出异常,你能够及时捕获潜在的设计错误,避免后续的逻辑错误。

需要注意的是,异常不应当用于普通的错误处理,因为抛出异常和捕获异常的过程比常规的条件判断更为昂贵和复杂。在生产代码中,异常应当仅用于严重的、无法恢复的错误情况。

在我们的实际应用之中,我们更多的是使用NSError来进行错误报告

NSError 是 Objective-C 中用于错误处理的标准类,它由以下三个核心部分组成:

  1. Error Domain(错误范围,类型:字符串)
    • 代表错误的发生范围,即产生错误的根源。
    • 通常使用特定的全局变量来定义,比如:
      • NSURLErrorDomain(表示 URL 处理相关的错误)。
      • NSCocoaErrorDomain(表示 Cocoa 框架内部的错误)。
    • 例如,当 URL 解析失败时,会使用 NSURLErrorDomain 来表示该错误来源于 URL 处理子系统。
  2. Error Code(错误码,类型:整数)
    • 用于指明在某个错误范围内发生的具体错误。
    • 同一错误范围内,可能有多个不同的错误情况,通常使用 enum 定义。
    • 例如,在NSURLErrorDomain下:
      • NSURLErrorNotConnectedToInternet(表示网络未连接)。
      • NSURLErrorTimedOut(表示请求超时)。
    • 在 HTTP 请求错误中,错误码可能会直接对应 HTTP 状态码,如 404(Not Found)、500(Internal Server Error)等。
  3. 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);
}@endint 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:1001userInfo:@{NSLocalizedDescriptionKey: @"发生了一个错误"}];}// 返回 NO,表示操作失败return NO;} else {// 返回 YES,表示操作成功return YES;}
}

对于错误码,我们可以自己设置枚举量,规范错误的类型。


http://www.ppmy.cn/ops/156779.html

相关文章

BGP边界网关协议(Border Gateway Protocol)选路、属性(一)

一、简介 当BGP收到到达同一目的地的多条路由时&#xff0c;会根据选路规则选择出最优路由&#xff0c;然后将最优路由下发到IP路由表&#xff0c;指导数据流量转发。在交换机的实现中&#xff0c;当到达同一目的地存在多条路由时&#xff0c;BGP选路的概要过程 注&#xff1a;…

优化深度神经网络

训练集、开发集(验证集)、测试集 偏差与方差 正则化 L2正则 Dropout 随机丢弃部分神经元输入&#xff0c;经常用于计算机视觉的神经网络内&#xff0c;因为通常没有足够的训练数据&#xff0c;很容易出现过拟合的问题 数据增强 训练集规一化 可以使其图像更均匀&#xff0c;…

DeepSeek-R1 云环境搭建部署流程

DeepSeek横空出世&#xff0c;在国际AI圈备受关注&#xff0c;作为个人开发者&#xff0c;AI的应用可以有效地提高个人开发效率。除此之外&#xff0c;DeepSeek的思考过程、思考能力是开放的&#xff0c;这对我们对结果调优有很好的帮助效果。 DeepSeek是一个基于人工智能技术…

C语言:函数栈帧的创建和销毁

目录 1.什么是函数栈帧2.理解函数栈帧能解决什么问题3.函数栈帧的创建和销毁的过程解析3.1 什么是栈3.2 认识相关寄存器和汇编指令3.3 解析函数栈帧的创建和销毁过程3.3.1 准备环境3.3.2 函数的调用堆栈3.3.3 转到反汇编3.3.4 函数栈帧的创建和销毁 1.什么是函数栈帧 在写C语言…

动态规划LeetCode-121.买卖股票的最佳时机1

给定一个数组 prices &#xff0c;它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。 你只能选择 某一天 买入这只股票&#xff0c;并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。 返回你可以从这笔交易中获取的最大利润。…

【数据采集】基于Selenium采集豆瓣电影Top250的详细数据

基于Selenium采集豆瓣电影Top250的详细数据 Selenium官网:https://www.selenium.dev/blog/ 豆瓣电影Top250官网:https://movie.douban.com/top250 写在前面 实验目标:基于Selenium框架采集豆瓣电影Top250的详细数据。 电脑系统:Windows 使用软件:PyCharm、Navicat 技术需求…

机器学习day7

自定义数据集 使用pytorch框架实现逻辑回归并保存模型&#xff0c;然后保存模型后再加载模型进行预测&#xff0c;对预测结果计算精确度和召回率及F1分数 代码 import numpy as np import torch import torch.nn as nn import torch.optim as optimizer import matplotlib.pyp…

【Python】第一弹---解锁编程新世界:深入理解计算机基础与Python入门指南

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】【C详解】【Linux系统编程】【MySQL】【Python】 目录 1、计算机基础概念 1.1、什么是计算机 1.2、什么是编程 1.3、编程语言有哪些 2、Python 背景知识 2.…