文章目录
前言
我们在构建程序应用时,如果决定重用代码,那么我们在编写接口时就会将其设计成易于复用的形式。这需要用到Objective-C语言的常见编程范式
提示:以下是本篇文章正文内容,下面案例可供参考
用前缀避免命名空间冲突
Objective-C没有其他语言那样子的内置命名空间机制。所以在起名的时候要设法避免潜在的命名冲突。如果发生命名冲突,那么应用程序的链接过程就会出错。
应用程序中的两份代码都各自实现了EOCTheClass的类,导致EOCTheClass所对应的类符号和“元类”符号各自定义了两次。
在运行期再入了含有重名类的程序库更糟糕,这个时候,“动态加载器”就遭遇了“重名符号错误”,很可能导致整个应用程序崩溃。
- 避免此问题的唯一解决办法就是变相实现命名空间:为所有的名称都加上适当前缀。即便加了前缀,也难保不会出现命名冲突,但是概率会小很多
- 使用Cocoa创建应用程序时要注意,Apple官方宣称其保留使用所有“两字母前缀”的权利,所以自己选用的前缀都应该是三个字母的。
- 不仅仅是类名,应用程序中的所有名称都应该加前缀。如果要为既有类新增“分类”,那么一定要给“分类”及“分类”中的方法加上前缀。在类的实现文件中所用的纯C函数及全局变量,这些名称是要算做“顶级符号”的。
- 如果使用第三方库编写自己的代码,并准备将其发布为程序库供他人开发应用程序,那么一定注意符号重复问题。我们的程序库所包含的那个第三方库也许还会为应用程序本身所引入,若是如此,那就很容易出现重复符号错误,这时应该给第三方代码库都加上自己的前缀。
- 虽说逐个改名是很麻烦的事情,不过如果向避免命名冲突,还是得去做。应用程序不直接引入XYZLibrary,改用EOCLibrary不行吗?但是应用程序可能还会引入另外一个ABCLibrary的库,这个库又包含了XYZLibrary库。此时如果我们和ABCLibrary库都没有加前缀,应用程序还会出现重复符号错误。还有如果我们的库里所用的XZYLibrary是X版本的,而应用程序却需要使用Y版本的某些功能,所以它必须自己在引入一份。
提供“全能初始化方法”
所有的对象都得初始化。初始化时一般要提供一些额外信息,UITableViewCell初始化时就要知名样式及标识符,标识符能够区分不同类型的单元格。这种对象的创建吃成本较高,我们在创建的时候可以依照标识符来复用,以提升程序效率。我们把这种可以为对象提供必要信息宜宾啊其能完成工作的初始化方法叫做“全能初始化方法”
- 如果创建类实例的方法不止一种,那么这个类就有多个初始化方法。但是仍然要在其中选择一个座位全能初始化方法,令其他的方法都来调用它。
- (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
- 在上面几个初始化方法中,initWithTimeIntervalSinceReferenceDate:是全能初始化方法。其余的初始化方法都要调用它,于是只有在全能初始化方法中才会存储内部数据。当底层数据存储机制改变时,只需修改此方法代码就好,无需改动其他初始化方法。
// EOCRectangle.h
// text12.12
//
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface EOCRectangle : NSObject
@property (nonatomic, assign, readonly) float width;
@property (nonatomic, assign, readonly) float height;
@end
NS_ASSUME_NONNULL_END
- (id)initWithWidth:(float)width andHeight:(float)height{
if (self = [super init]){
_width = width;
_height = height;
}
- 如果有人使用[[EocRectangle alloc]init]的方法创建会如何?
- (id)initWithWidth:(float)width andHeight:(float)height{
if (self = [super init]){
_width = width;
_height = height;
}
return self;
}
- (id)init{
return [self initWithWidth:5.0f andHeight:10.0f];
}
- (id)init{
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Must use initWithWidth:andHeight: instead." userInfo:nil]
}
- 设置默认值的那个init方法调用了全能初始化方法。如果采用这个版本来覆写,那么也可以直接在其代码中设置_width和_height实例变量的值。
- 如果类的底层存储方式变了,init与全能初始化方法设置数据所用的代码就都要修改。如果类的初始化方法有很多种,而且待初始化的数据比较复杂,那么这样就特别麻烦。很容易就会忘记修改某个初始化方法,从而导致各个初始化方法之间互相不一致
// EOCRectangle.h
// text12.12
//
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface EOCRectangle : NSObject
@property (nonatomic, assign, readonly) float width;
@property (nonatomic, assign, readonly) float height;
- (id)initWithWidth:(float)width andHeight:(float)height;
@end
NS_ASSUME_NONNULL_END
// EOCRectangle.m
// text12.12
//
//
#import "EOCRectangle.h"
@implementation EOCRectangle
- (id)initWithWidth:(float)width andHeight:(float)height{
if (self = [super init]){
_width = width;
_height = height;
}
return self;
}
//- (id)init{
// return [self initWithWidth:5.0f andHeight:10.0f];
//}
//- (id)init{
// @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Must use initWithWidth:andHeight: instead." userInfo:nil]
//}
@end
// EOCSquare.h
// text12.12
//
//
#import "EOCRectangle.h"
NS_ASSUME_NONNULL_BEGIN
@interface EOCSquare : EOCRectangle
- (id)initWithDimension:(float)dismensionl;
@end
NS_ASSUME_NONNULL_END
// EOCSquare.m
// text12.12
//
//
#import "EOCSquare.h"
@implementation EOCSquare
- (id)initWithDimension:(float)dismensionl{
return [super initWithWidth:dismensionl andHeight:dismensionl];
}
@end
- EOCSquare类的全能初始化方法,调用了超类的全能初始化方法。EOCRectangle类的实现代码也调用了其超类的全能初始化方法。全能初始化方法的使用链一定要维系。如果子类的全能初始化方法与超类方法的名称不同,那么总应该覆写超类的全能初始化方法
- (id)initWithWidth:(float)width andHeight:(float)height{
float dismensionl = MAX(width, height);
return [self initWithDimension:dismensionl];
}
- (id)initWithDimension:(float)dismensionl{
return [super initWithWidth:dismensionl andHeight:dismensionl];
}
- 覆写了这个方法后,即便使用init来初始化EOCSquare对象,也能照常工作。
- 有时候可能需要编写多个全能初始化方法,如果某对象的实例有两种完全不同的创建方式,必须分开处理,那么就会出现这种情况。
- 每个子类的全能初始化方法都应该调用其超类的对应方法,并逐层向上。
- 如果超类的出生方法不适用于子类,那么覆写这个方法并在其中抛出异常。
实现description方法
调试代码时,经常需要打印并查看对象信息。一种办法是把对象的全部属性输出到日志里,最常见的还是直接打印
NSLog(@“object = %@”, object);
- 在构建需要打印到日志的字符串时,object对象会收到description消息。该方法所返回的描述信息将取代“格式字符串”里的“%@”。
- 如果在自定义的类上这么做,会输出
- 要想输出更为有用的方法需要覆写description方法并将描述此对象的字符串返回。在新的实现description方法中,也应该像磨人的实现那样,打印出类的名字和指针地址,这些内容有时候会用到。
- 有一个简单的方法,可以在description方法中输出很多互不相同的信息。借助NSDictionary的description方法输出。在自定义的description方法中,把待打印的信息放到字典里面,然后将字典对象的description方法所输出的内容包含在字符串里并返回,这样就可以实现精简的信息输出方式
// ECOLocation.h
// text。12.14
//
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface ECOLocation : NSObject
@property (nonatomic, copy, readonly) NSString* title;
@property (nonatomic, assign, readonly) float latitude;
@property (nonatomic, assign, readonly) float longitude;
- (id)initWithTitle:(NSString*)title latitude:(float)latitude longitude:(float)longitude;
@end
NS_ASSUME_NONNULL_END
// ECOLocation.m
// text。12.14
//
//
#import "ECOLocation.h"
@implementation ECOLocation
- (id)initWithTitle:(NSString *)title latitude:(float)latitude longitude:(float)longitude{
if (self = [super init]){
_title = [title copy];
_latitude = latitude;
_longitude = longitude;
}
NSLog(@"<%@: %p, %@>",[self class], self, @{@"title":_title, @"latitude":@(_latitude),@"longitude":@(_longitude)});
return self;
}
- (NSString*)description{
return [NSString stringWithFormat:@"<%@: %p, %@>",[self class], self, @{@"title":_title, @"latitude":@(_latitude),@"longitude":@(_longitude)}];
}
@end
- 这样比仅仅输出指针和类名有用多了,而且对象中的每条属性都能打印的很好。也可以在格式字符串中直接为每个实例变量保留好位置,然后逐个打印。
- 使用NSDictionary来实现此功能可以领代码更易维护,如果还要向类中新增属性,并且要在description方法中打印,那么修改字典内容即可。
- NSObject中的debugDescription方法也要注意,这个方法是在调试器中以控制台命令打印对象时调用的。在NSObject类的默认实现中,此方法只是直接调用了description。
- 如果想在调试时打印出尽可能详细的对象描述信息,则应该实现debugDescription方法
尽量使用不可变对象
设计类的时候,应充分运用属性来封装数据。而在使用属性的时候,则可以将其声明为“只读”。默认情况下是“可读也可写“。这样设计出来的类都是”可变的“。只是一般情况下我们需要的类未必需要改变。
有的对象没有必要修改其内容,即使修改了,新数据也不会推送回服务器。如果把可变对象mutable object放入collection之后又修改其内容,很容易破坏set内部结构,失去其固有的语义。
具体到变成实践中,一个尽量把对外公布的属性设置为只读,而且只在确有必要时才讲属性对外公布。
// EOCPointOfInterest.h
// text。12.14
//
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface EOCPointOfInterest : NSObject
@property (nonatomic, copy, readonly) NSString* identifier;
@property (nonatomic, copy, readonly) NSString* title;
@property (nonatomic, assign, readonly) float latitude;
@property (nonatomic, assign, readonly) float longitude;
- (id)initWIthIdentifier:(NSString*)identifier title :(NSString*)title latitude:(float)latitude longitude:(float)longitude;
@end
NS_ASSUME_NONNULL_END
- 我们讲属性设置为readonly时,如果是这改变这些属性值,编译的时候就会报错。属性值可以读出,但是无法写入。这样的话在使用对象的时候就可以保证其底层数据不会改变。对象本身的数据结构也就不可能出现不一致的现象。
- 有时候kennel向修改封装在对象内部的数据,但是却不想令这些数据为外人所改动。这种情况下,通常是在对象内部将readonly重新声明为readwrite。如果该属性是nonatomic的,可能会产生“竞争条件”(竞态条件)。在对象内部写入属性时,对象外的观察者也许正读取该属性。如果向避免这个问题,我们可以在必要时通过“派发队列”等手段,将(包括对象内部的)所有数据存取操作都设为同步操作。
- 重新声明readwrite这一操作在class- continuation中完成,在公共接口中声明的属性可以于此处重新声明,属性的其他特质必须保持不变,readonly可拓展为readwrite。
// EOCPointOfInterest.h
// text。12.14
//
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface EOCPointOfInterest : NSObject
@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
NS_ASSUME_NONNULL_END
- 现在只能于EOCPointOfInterest实现代码内部来设置这些属性值了。仍能通过“键值编码”技术设置这些属性
- 有些吃宵夜直接用类型信息查询功能查处属性所对应的实例变量在内存布局中的偏移量,以此设置这个实例变量的值,这样做比绕过公共的API还不合规范。从技术上讲,某个类即使没有对外公布“设置方法”,也依然可以想办法修改对应属性
- 在定义公共的API时,还要注意一些东西:对象里表示各种collection的那些属性到底应该怎样设置?
// EOCOPerson.h
// text。12.14
//
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface EOCOPerson : 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:(EOCOPerson *)person;
- (void)removeFriend:(EOCOPerson *)person;
@end
NS_ASSUME_NONNULL_END
// EOCOPerson.m
// text。12.14
//
//
#import "EOCOPerson.h"
@implementation EOCOPerson{
NSMutableSet* _internalFriends;
}
- (NSSet*)friends{
return [_internalFriends copy];
}
- (void)addFriend:(EOCOPerson *)person{
[_internalFriends addObject:person];
}
- (void)removeFriend:(EOCOPerson *)person{
[_internalFriends removeObject:person];
}
- (id)initWithFirstName:(NSString *)firstName andLastName:(NSString *)lastName{
if (self = [super init]){
_firstName = firstName;
_lastName = lastName;
_internalFriends = [NSMutableSet new];
}
return self;
}
@end
- 也可以利用NSMutableSet的方法来实现friends属性,令该类的用户不借助“addFriend”与“removeFriend”方法直接操作属性。但是这样通过分解耦数据的方法很容易出bug
- 不要在返回的对象上查询类型确定其是否可变。不宜从底层直接修改对象中的数据。
使用清晰而协调的命名方式
类,方法,变量的命名是Objective-C编程的重要环节。名称中一般都带有“in”,“for”,“with”等介词。
- 方法与变量名使用了“驼峰命名法”,以小字母开头,其后每个单词首字母大写。类名也使用驼峰命名法,只是首字母大写,前面通常有两三个前缀字母。
方法命名
//
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface EOCRectangle : NSObject
@property (nonatomic, assign, readonly) float width;
@property (nonatomic, assign, readonly) float height;
- (id)initWithWidth:(float)width andHeight:(float)height;
@end
NS_ASSUME_NONNULL_END
- 绕过我们使用这样的命名方法,我们可以很清晰的了解到各个参数的意义。
- 长名字令代码可读性很高,不要吝惜长方法名,吧方法名起的稍微长一些,可以保证能准确传达出发染发所执行的任务。
方法名 | 缘由 |
---|---|
+string(工厂方法(factory method)) | 用于创建新的空字符串,方法名清晰的描述了返回值的类型 |
+stringWithString(工厂方法) | 根据某字符串创建出与之内容相同的新字符串。与创建空字符串所用的那个工厂方法一样,方法名的第一个单词也指明了返回类型 |
+localizedStringWithFormat:(工厂方法) | 根据特定格式创建出新的本地字符串。返回值仍为字符串,只不过是一种经过本地化处理的特殊字符串 |
-lowercaseString | 把字符串中的大写字母都转化为小写,不会修改接收此消息的字符串本身,而是创建一个新的字符串 |
-intValue | 将字符串解析位置整数。返回值为int |
-length | 获取字符串长度 |
-lengthOfBytesUsingEcoding: | 若字符串是以给定的编码格式(ASCll,UTF8,UTF6)来编码的,返回其子节数组的长度 |
-getCharacters:range: | 获取给定范围内的字符 |
- (void)getCharacters:(unichar*)buffer range:(NSRange)arange | 首个参数buffer指向一个足够大的数组,容纳所请求范围内的那些自负 |
-hasPrefix | 判断本字符串是否以另一个字符串开头。返回值是Boolean类型 |
isEqualToString: | 判断两字符串是否相等。返回值为Boolean类型 |
- 如果方法返回值是新创建的,那么方法名的首个词应该是返回值的类型,除非前面还有修饰语。
- 应该把表示参数的名词放在参数前面
- 如果方法要在当前对象上执行操作,那么应该包含动词,如果操作时还需要参数,应该在这个动词后边加上一个或者多个名次。
- 不要使用str这样的简称
- Boolean属性一个加is前缀。如果方法返回非属性的Boolean值,一个根据其功能,选用has或者is当前缀
- 将get这个前缀留给那些借由“输出参数”来保存返回值的方法。
类与协议的命名
应该为类和协议的名称加上前缀,以避免命名空间冲突。一个像给方法起名那样吧词句组织好,使其从左到右较为通顺。
- 命名方式一个协调一致,如果要从其他框架中继承子类,那么无比遵守其命名规则。若要创建自定义的委托协议,其名称中应该包含委托协议的发起方的名称,后面再跟上Delegate
- 起名时应该遵守OC命名规范
- 方法名要言简意赅,从左到右读起来应该像日常生活中的句子
- 方法名里不要使用缩略后的类型名称
- 方法名风格要和直接代码或所要生成的框架相符合
为私有方法名加前缀
一个类所做的事情通常都要比从外面看到的多。编写类的实现的代码时,经常要写一些只在内部使用的方法。
- 为私有方法名加前缀便于修改方法名或者方法签名。赋予哥哥方法来说,修改其名称或者签名前要三思,因为公共的API不可以随便改动。如果改了,那么使用这个类的所有开发者都必须更新代码。
- 对于内部方法,若要修改其名称或者签名,只需同时修改本类内部的相关代码,不会影响到外表的API。用前缀把私有方法标注起来,很容易看出哪些方法名苦也随意修改,哪些不能轻举妄动
- 具体使用何种前缀可以自行决定,最好包含下划线合字母p。其余部分按照驼峰命名法即可。
- 私有方法不能出现在接口中。看你要在class-continuation分类里声明私有方法。OC语言没有办法将方法标为私有。每个对象都可以响应任意消息。而且可以在运行期检视某个对象所能直接响应的消息,根据给定的消息查处其对应方法。这一工作要在运行期才能完成。所以OC中没有那种约束方法调用的机制可以用以限定谁能调用此方法,能在哪个对象上调用此方法以及何时能调用此方法。
- 不要单用一个下划线做私有方法的前缀,这种方法是预留给苹果公司的
理解Objective-C错误模型
当前很多种编程语言都有“异常”机制,Objective-C也不例外
- ARC,在默认情况下不是“异常安全的”。具体来说意味着如果抛出异常,本应该在作用区域未必释放的对象现在却不会自动释放了。如果想生成“异常安全”的代码,可以通过设置编译器的标志来实现。即使不用ARC,也很难写出在抛出异常时不会导致内存泄漏的代码。如果有段代码先创建好了某个资源,使用完了之后在将其释放,可是在释放资源之前如果抛出异常了,该资源就不会被释放。
- OC采用的办法是:只在极其罕见的情况下抛出异常,异常抛出之后,无需考虑恢复问题,而且程序此时应该退出。
- 异常值一个应用于极其严重的错误。出现不那么严重的错误时,OC语言所采用的变成范式为:令方法返回nil/0,或者使用NSError,表示其中有错误发生
Error domain(错误范围,类型为字符串) | 错误发生的范围,也就是产生错误的根源。通常用一个全局变量来定义。 |
---|---|
Error code (错误码,类型为整数) | 独有的错误码,用以知名在某个范围内具体发货啥呢跪了何种错误。某个特定范围内可能会发生一系列相关错误,这些错误码通常用enum来定义 |
User info(用户信息,类型为字典) | 有关此错误的额外信息,其中或许包含一段“本地化的描述”,或许还含有导致该错误发生的另外一个错误,经由此种信息,可讲相关错误串成一条:“错误链” |
- 设计API时,NSError的第一种常见用法是通过委托协议来传递此错误。有错误发生时,当前对象会把错误经由协议中的某个方法传给其委托对象。
- NSError的另外一种常见用法是:经由方法的“输出参数”返回给调用者。
理解NSCopying协议
使用对象时经常要拷贝它,如果想令自己的类支持拷贝操作,要实现NSCopying协议。
-(id)copyWithZone:(NSSZone*)zone
- copu 方法由NSObject实现,该方法只是以“默认区”为参数来调用“copyWithZone”。我们总想覆写copy方法,其实真正需要实现的是“copyWithZone”方法
- 若想使某个类支持拷贝功能,只需声明该类遵从NSCopying协议,并实现其中的那个方法即可。
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface EOCOPerson : NSObject
<NSCopying>
@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:(EOCOPerson *)person;
//- (void)removeFriend:(EOCOPerson *)person;
@end
NS_ASSUME_NONNULL_END
- (id)copyWithZone:(NSZone *)zone{
EOCOPerson* copy = [[[self class]allocWithZone:zone] initWithFirstName:_firstName andLastName:_lastName];
return copy;
}
- 我们直接把待拷贝的对象交给“全能初始化方法”,令其执行所有的初始化工作。
// EOCOPerson.h
// text。12.14
//
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface EOCOPerson : NSObject
<NSCopying>
@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:(EOCOPerson *)person;
- (void)removeFriend:(EOCOPerson *)person;
@end
NS_ASSUME_NONNULL_END
// EOCOPerson.m
// text。12.14
//
//
#import "EOCOPerson.h"
@implementation EOCOPerson{
NSMutableSet* _internalFriends;
}
- (id)copyWithZone:(NSZone *)zone{
EOCOPerson* copy = [[[self class]allocWithZone:zone] initWithFirstName:_firstName andLastName:_lastName];
copy->_internalFriends = [_internalFriends mutableCopy];
return copy;
}
- (NSSet*)friends{
return [_internalFriends copy];
}
- (void)addFriend:(EOCOPerson *)person{
[_internalFriends addObject:person];
}
- (void)removeFriend:(EOCOPerson *)person{
[_internalFriends removeObject:person];
}
- (id)initWithFirstName:(NSString *)firstName andLastName:(NSString *)lastName{
if (self = [super init]){
_firstName = firstName;
_lastName = lastName;
_internalFriends = [NSMutableSet new];
}
return self;
}
@end
- 这次的代码把本对象的_internalFriends复制了一份,令copy对象的_internalFriends实例变量指向这个复制过的set。不拷贝这个变量,直接令两个对象共享一个可变的set的话,给原来的对象操作后,拷贝的对象也会发狠恶搞改变
- 如果初始化方法可能要设置一个复杂的内部数据结构,在拷贝后的对象中,这个数据结构立刻就要用其他的数据来覆写。
- mutableCopy方法来自另外一个叫做“NSMutableCopying"的协议,定义了一个方法:-(id)mutableCopyWithZone:(NSZone*)zone
- 可变对象上调用copy方法会返回另外一个不可变的类的实例。这样做是为了能在可变版本和不可变版本之间自由切换。copy返回的对象和当前对象类型一致。immutableCopy和mutableCopy返回当前对象的不可变版本和可变版本。
- 可以通过查询类型信息判断拷贝的实例对象是否可变。
- 深拷贝:在拷贝对象自身时,将其自身底层数据也一并复制。Foundation框架中的clooection类在默认情况下都是浅拷贝。只拷贝容器对象本身,不拷贝其中数据
- -(id)initWIthSet:(NSArray*)array copyItems:(BOOL)copyItems
- 没有专门定义深拷贝的协议,所以其具体执行方式由每个类来决定,你只需确定自己所写的类是否要深拷贝。