我们如果决定重用代码,那么我们在编写接口时就会将其设计成易于复用的形式。这就要用到OC语言中常见的编程范式(paradigm)。
一、用前缀避免命名冲突
OC没有其它语言那种内置的命名空间(namespace)机制。鉴于此,我们在起名时要设法避免潜在的命名冲突,否则就很容易重名了。如果发生了命名冲突(naming clash),那么应用程序的 链接过程就会出错,因为其中出现了重复符号。
当SomeClass
对应的类符号和“元类”符号各被定义了两次,就会发生错误。比无法链接更糟糕的情况是,在运行期载入了含有重名类的程序库。此时,“动态加载库”(dynamic loader)就遭遇了“重名符号错误”(duplicate symbol error),很可能会令整个应用程序崩溃。
避免此问题的唯一办法就是变相实现命名空间:为所有名称都加上适当前缀。但是,即便加了前缀,也难保不出现命名冲突,但是其几率会小很多。
使用Cocoa创建应用程序时一定要注意,Apple宣称其保留使用所有“两字母前缀”(two-letter prefix)的权利,所以我们自己选用的前缀应该是三个字母的。
不仅仅是类名,应用程序中的所有名称都应该加前缀。如果要为既有类新增“分类”(category),那么一定要给“分类”及“分类”中的方法加上前缀。开发者可能会忽视另外一个容易引发命名冲突的地方,那就是类的实现文件中所用的纯C函数及全局变量,这个问题必须要引起注意。在编译好的目标文件中,这些文件是要算做“顶级符号”(top–level symbol)的。
如果要用第三方库编写自己的代码,并准备将其再发布为程序库供他人开发应用程序所用,那么尤其要注意重复符号问题。若应用程序自身和其所用的程序库都引入了同名的第三方库,则后者应该加前缀以避免命名冲突。
二、提供全能初始化方法
所有对象均要初始化,在初始化时,有些对象可能无须开发者向其提供额外的信息,不过一般来说还是需要的。通常情况下,对象若不知道必要的信息,则无法完成其工作。我们把可以为对象提供必要信息以便其能完成工作的初始化方法叫做“全能初始化方法”(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
正如该类的文档描述的那样,在上面几个初始化方法中,“initWithTimeIntervalSinceReferenceDate:
”是全能初始化方法。也就是说,其余的初始化方法都要调用它。于是,只有在全能初始化方法中,才会存储内部数据。这样的话,当底层数据存储机制改变时,只需修改此方法的代码就好了,无需改动其他初始化方法。例:
编写一个表示矩形的类:
@interface Rectangle : NSObject
@property (nonatomic, assign, readonly) float width;
@property (nonatomic, assign, readonly) float height;
- (id)initWithWidth:(float)width andHeight:(float)height;
@end
注意,我们把属性设为了只读,这样一来,外界就无法设置Rectangle
的属性了,可以提供以下方法:
- (id)initWithWidth:(float)width andHeight:(float)height {
if (self = [super init]) {
_width = width;
_height = height;
}
return self;
}
但是,如果有人使用[[Rectangle alloc] init]
来创建矩形会如何呢?这是合乎规定的,会将其高度与宽度设置为0。但是我们一般希望能够使用我们来默认的值,或者是抛出异常,指明本类实例必须使用“全能初始化方法”来初始化。可以使用下面的方法来初始化:
//using default values
- (id)init {
return [self initWithWidth:5.0f andHeight:10.0f];
}
//throw an exception
- (id)init {
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Must use initWithWidth:andHeight: instead" userInfo:nil];
}
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Must use initWithWidth:andHeight: instead'
@interface Square : Rectangle
- (id)initWithDimension:(float)dimension;
@end
@implementation Square
- (id)initWithDimension:(float)dimension {
return [super initWithWidth:dimension andHeight:dimension];
}
@end
全能初始化方法的调用链一定要维系。调用者可能会使用“init
”或者“initWithWidth:andHeight:
”来初始化Square
对象。这样可能导致创建出“宽度”与“高度”不想等的正方形。于是就引出一个问题:如果子类的全能初始化方法与超类方法的名称不同,那么总应该覆写超类的全能初始化方法。在这个例子中,这样改写:
- (id)initWithWidth:(float)width andHeight:(float)height {
float dimension = MAX(width, height);
return [self initWithDimension:dimension];
}
有时我们不想覆写超类的全能初始化方法,因为那样做可能没有道理。我们也可以覆写超类的全能初始化方法并使其抛出异常:
- (id)initWithWidth:(float)width andHeight:(float)height {
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Must use initWithDimension: instead" userInfo:nil];
}
这样做看起来似乎显得突兀,不过有时却是必须的,因为那种情况下创建出来的对象,其内部数据有可能相互不一致(inconsistent internal data)。如果这么做了,那么在这个例子中,调用init
方法也会抛出异常,因为init
方法也得调用“initWithWidth:andHeight:
”。此时可以覆写init
方法。
不过,在OC程序中,只有当发生严重错误时,才应该抛出异常。
有时候可能需要编写多个全能初始化方法,比如,如果某对象的实例有两种完全不同的创建方式,必须分开处理,那么就会出现这种情况。以NSCoding
为例,此协议提供了“序列化机制”(serialization mechanism),对象可以依次指明其自身的编码(encode)和解码(decode)方式。NSCoding
协议定义了下面这个初始化方法,遵从该协议者都应该实现此方法:
- (id)initWithCoder:(NSCoder*)decoder;
我们在实现此方法时一般不调用平常所使用的那个全能初始化方法,因为该方法要通过“解码器”(decoder)将对象数据解压缩,所以和普通的初始化方法不同。而且,如果超类也实现了NSCoding
,那么还需调用超类的“initWithCoder:
”方法。于是,子类中有不止一个初始化方法调用了超类的初始化方法,因此,严格地说,在这种情况下出现了两个全能初始化方法。具体到Rectangle上:
@interface Rectangle : NSObject<NSCoding>
@property (nonatomic, assign, readonly) float width;
@property (nonatomic, assign, readonly) float height;
- (id)initWithWidth:(float)width andHeight:(float)height;
@end
@implementation Rectangle
//Designated initializer
- (id)initWithWidth:(float)width andHeight:(float)height {
if (self = [super init]) {
_width = width;
_height = height;
}
return self;
}
//Superclass's designated initializer
- (id)init {
return [self initWithWidth:5.0f andHeight:10.0f];
}
//Initializer from NSCoding
- (id)initWithCoder:(NSCoder*)decoder {
//Call through to super's designated initializer
if (self = [super init]) {
_width = [decoder decodeFloatForKey:@"width"];
_height = [decoder decodeFloatForKey:@"height"];
}
return self;
}
@end
注意,NSCoding
协议的初始化方法没有调用本类的全能初始化方法,而是调用了超类的相关方法。然而,若是超类也实现了NSCoding,则需要改为调用超类的“initWithCoder:
”初始化方法。例如,在此情况下,Square类就要这么写:
@interface Square : Rectangle
- (id)initWithDimension:(float)dimension;
@end
@implementation Square
//Designated initializer
- (id)initWithDimension:(float)dimension {
return [super initWithWidth:dimension andHeight:dimension];
}
//Superclass's designated initializer
- (id)initWithWidth:(float)width andHeight:(float)height {
float dimension = MAX(width, height);
return [self initWithDimension:dimension];
}
//Initializer from NSCoding
- (id)initWithCoder:(NSCoder *)coder {
if (self = [super initWithCoder:coder]) {
//Square 的特定初始化
}
return self;
}
@end
每个子类的全能初始化方法都应该调用其超类的对应方法,并逐层向上。
三、实现description方法
NSLog(@"object = %@", object);
在构建需要打印到日志的字符串时,object
对象会收到description
消息,该方法所返回的描述信息将取代“格式字符串”(format string)里的“%@”。
对于一个数组:
NSArray *object = @[@"String", @123];
NSLog(@"object = %@", object);
输出:
object = (
String,
123
)
object = <FKApple: 0x100709ba0>
为了显示地址外的更多信息,我们可以覆写description方法:
相关内容在此有稍有介绍:OC处理对象。
在新实现的description
方法中也应该像默认的那样,打印出名字和指针地址,因为这些内容有时也会用到。
有个简单的方法,可以在description
中输出很多互不相同的信息,那就是借助NSDictionary
类的description
方法。此方法输出信息的格式如下:
{
key: value;
foo: bar;
}
在自定义的description
方法中,把带打印的信息放到字典里面,然后将字典对象的description
方法所输出的内容包含在字符串里面返回,这样就可以实现精简的信息输出方式了。例:一个表示地点名称和坐标的类。
@interface Location : 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
@implementation Location
- (id)initWithTitle:(NSString *)title latitude:(float)latitude longitude:(float)longitude {
if (self = [super init]) {
_title = [title copy];
_latitude = latitude;
_longitude = longitude;
}
return self;
}
@end
要是这个类的description方法能够打印出地名和经纬度就好了。我们可以用下面的方法编写description
方法,用NSDictionary
来实现此功能:
- (NSString*)description {
return [NSString stringWithFormat:@"<%@: %p, %@>",
[self class], self,
@{@"title":_title,
@"latitude":@(_latitude),
@"longitude":@(_longitude),
}];
}
输出为:
loc = <Location: 0x1007090c0, {
latitude = "55.2";
longitude = "34.98";
title = "Some Place";
}>
NSObject
协议中还有个方法要注意,那就是debugDescription
。它和description
的区别在于:debugDescription
方法是开发者在调试器(debugger)中以控制台命令打印对象时才调用的。在NSObject类的默认实现中,此方法只是直接调用了description
。这样,通过重写这两个方法就可以区分描述信息与调试器po命令打印的信息了。若想在调试时打印出更详尽的对象描述信息,应实现debugDescription
方法。
四、尽量使用不可变对象
设计类的时候,应充分应用属性来封装数据。而在使用属性时,则可以将其声明为“只读”(readonly)。默认情况下,属性是“既可读又可写的”(read–write),这样设计出来的类都是“可变的”(mutable)。不过,一般情况下我们要建模的数据未必需要改变。
在编程实践中,应该尽量把对外公布的属性设为只读,而且只有在确有必要时才将属性对外公布。
讲一个类做成不可变类,需要把所有属性都声明成readonly:
@interface LocationOfInterest : 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
虽然这些属性都没有设置方法(setter),但我们还是应该在文档里指明实现所用的内存管理语义,这样的话,以后想把它变成可读写的属性时就会简单一些。
有时可能想修改封装在对象内部的数据,但是却不想令这些数据为外人所改动。这种情况下,通常做法是在对象内部重新声明为readwrite。当然,如果该属性是nonatomic的,那么这样做可能会产生“竞争条件”(read condition)。在对象内部写入某属性时,对象外的观察者也许正在读取该属性。若想避免此问题,我们可以在必要时通过“派发队列”(dispatch queue)等手段,将(包括对象内部的)所有数据存储操作都设为同步操作。
将属性在对象内部重新声明为readwrite这一操作可在“class-continuation分类”中完成,在公共接口中声明的属性可于此处重新声明,属性的其他特质必须保持不变,而readonly可以扩展为readwrite。
例:
#import "LocationOfInterest.h"
@interface LocationOfInterest ()
@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
@implementation LocationOfInterest
/*...*/
@end
现在,只能于其实现代码内部设置这些属性值了。更准确的说,在对象外部,仍然能够通过“键值编码”(Key-Value Coding, KVC)技术设置这些属性值,比如:
[locationOfInterest setValue:@"abc" forKey:@"identifier"];
这样做可以改动identifier
属性,因为KVC会在类里查找“setIdentifier:”方法,并借此修改此属性。不过,这样做相当于违规绕过了本类所提供的API。
此外,还要注意对象内的各种collection
应该设成可变的还是不可变的。若属性在功能上要使用可变的来实现,可以提供一个readonly
属性供外界使用,该属性将返回不可变的set
,而此set
则是内部那个可变set
的一个拷贝,比如:
// Person.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
@property (nonatomic, copy, readonly) NSString *name;
@property (nonatomic, strong, readonly) NSSet *friends;
- (id)initWithName:(NSString*)name;
- (void)addFriend:(Person*)person;
- (void)removeFriend:(Person*)person;
@end
NS_ASSUME_NONNULL_END
// Person.m
#import "Person.h"
@implementation Person {
NSMutableSet *_internalFriends;
}
- (NSSet*)friends {
return [_internalFriends copy];
}
- (void)addFriend:(Person *)person {
[_internalFriends addObject:person];
}
- (void)removeFriend:(Person *)person {
[_internalFriends removeObject:person];
}
- (id)initWithName:(NSString *)name {
if (self = [super init]) {
_name = name;
_internalFriends = [[NSMutableSet alloc] init];
}
return self;
}
@end
五、使用清晰而协调的命名方式
类、方法以及变量的命名是OC编程的重要环节。方法与变量名使用“驼峰式大小写命名法”(camel casing)。类名也用驼峰命名法,不过其首字母大写,而且前面通常还有两三个前缀字母。按照驼峰命名法写出来的代码更容易为其他OC开发者所接受。
方法命名
- (id)initWithSize:(float)width :(float)height;
- (id)initWithWidth:(float)width andHeight:(float)height;
观察上下两个方法名,下面的命名方式要好得多。把方法名起的稍微长一点,可以保证其能准确传达出方法所执行的任务。然而方法名也不能长的太过分了,应该尽量言简意赅。
给方法命名的注意事项可以总结成下面几条规则:
- 如果方法的返回值是新创建的,那么方法名的首个词应是返回值的类型,除非前面还有修饰语,如
localizedString
。属性的存取方法不遵循这种命名方式,因为一般认为这些方法不会创建新对象,即使有时返回内部对象的一份拷贝,我们也认为那相当于原有的对象。这些存取方法应该按照其对应的属性来命名。 - 应该把表示参数类型的名词放在参数前面。
- 如果方法要在当前对象上执行操作,那么就应该包含动词;若执行操作时还需要参数,则应该在动词后面加上一个或者多个名词。
- 不要使用str这种简称,应该用string这样的全称。
- Boolen属性应该加is前缀。如果某方法返回非属性的Boolen值,那么应该根据其功能,选用has或者is当前缀。
- 将get这个前缀留给那些借由“输出参数”来保存返回值的方法。
类与协议的命名
应该为类与协议的名称加上前缀,以避免命名空间冲突,而且应该像给方法起名那样把语句组织好,使其从左至右读起来比较通顺。最重要的一点就是,命名方式应该协调一致,而且如果要从其他框架中继承子类,那么务必遵循其命名惯例。
六、为私有方法名加前缀
一个类所做的事情通常要比从外面看到的多。编写类的代码时,经常要写一些只在内部使用的方法。我们应该为这种方法的名称加上某些前缀,这有助于调试,因为可以很容易把公共方法和私有方法区别开。
为私有方法名加前缀还有个原因,就是便于修改方法名或者方法签名。对于公共方法来说,修改其代码或签名前要三思,因为类的API不便随意改动。对内部方法来说,修改其名称或者签名,不会影响到面向外界的API。用前缀把私有方法标起来,就可以很容易能看出哪些方法可以随意修改,哪些不应轻易改动。
具体使用何种前缀可根据个人喜好,其中最好包含下划线与字母p。p表示“private”(私有的),而下划线可以把它和真正的方法名区隔开。下划线后面的部分按照常用的驼峰命名法来命名即可。例如:
@interface SomeObject : NSObject
- (void)publicMethod;
@end
@implementation SomeObject
- (void)publicMethod {
/*...*/
}
- (void)p_privateMethod {
/*...*/
}
与公共方法不同,私有方法不出现在接口定义中,私有方法一般只在实现的时候声明。
OC没有办法将方法标为私有,如前面提到的,每个对象都可以响应任何消息,而且可以在运行期检视某个对象所能直接相应的的消息。根据给定的消息查出其对应的方法,这一工作要在运行期才能完成。
苹果公司喜欢单用一个下划线作为私有方法的前缀,鉴于此,苹果公司在文档里说,开发者不应该单用一个下划线做前缀。
七、理解Objective-C错误模型
当前很多编程语言都有“异常”(exception)机制,OC也不例外。
首先要注意的是,“自动引用计数”(Automatic Reference Counting, ARC)在默认情况下不是“异常安全的”(exception safe)。具体来说,这意味着:如果抛出异常,那么本应在作用域末尾释放的对象现在不会自动释放了。如果想生成“异常安全”的代码,可以通过设置编译器的标志来实现,不过这将引入一些额外代码,在不抛出异常时,也照样要执行这部分代码。需要打开的编译器标志叫做-fobjc-arc-exceptions
。
即使不用ARC,也很难写出在抛出异常时不会导致内存泄露的代码。比如说,设有段代码先创建好了某个资源,使用完之后再将其释放。可是在释放资源之前如果抛出异常了,那么该资源就不会被释放了:
id someResource = /*...*/;
if (/*check for error*/) {
@throw [NSException exceptionWithName:@"ExceptionName" reason:@"There was an error" userInfo:nil];
}
[someResource doSomething];
[someResource release];
在抛出异常前先释放someResource
,这样做当然能解决此问题,不过要是待释放的资源有很多,而且代码的执行路径更为复杂的话,那么释放资源的代码就容易写的很乱。此外,代码中加入了新的资源之后,开发者经常会忘记在抛出异常前先把它释放掉。
OC语言现在采用的方法是:只在极其罕见的情况下抛出异常,异常抛出之后,无须考虑恢复问题,而且应用程序此时也应该退出。也就是说,不用再编写复杂的“异常安全”的代码了。
异常只应该用于极其严重的错误。比如有人直接使用了一个抽象基类,那么可以考虑抛出异常。OC没有办法将某个类标识为“抽象类”。想达成类似的效果,最好的办法是在那些子类必须覆写的超类方法里抛出异常。这样的话,只要有人直接创建抽象基类的实例并使用它,即会抛出异常:
- (void)mustOverrideMethod {
NSString *reason = [NSString stringWithFormat:@"%@ must be overriden", NSStringFromSelector(_cmd)];
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:reason userInfo:nil];
}
异常只用来处理严重错误(fatal error,致命错误);对于“不那么严重的错误”(nonfatal error,非致命错误),OC语言所采用的编程范式为:令方法返回nil/0,或是使用NSError,以表明其中有错误发生。如:
- (id)initWithValue:(id)value {
if (self = [super init]) {
if (/*Value means instance can't be created*/) {
self = nil;
} else {
//Initialize instance
}
}
return self;
}
这种情况下,如果if
语句发现无法用传入的参数值来初始化当前实例,那么就把self
设置成nil
,这样的话,整个方法的返回值也就是nil
了。调用者发现初始化方法并没有2把实例创建好,于是便可以知道其中发生了错误。
NSError
的用法更加灵活,因为经由此对象,我们可以把导致错误的原因回报给调用者。NSError
对象里封装了三条消息:
- Error domain(错误范围,类型为字符串)产生错误的根源,通常用一个特有的全局变量来定义。
- Error code(错误码,类型为整数)独有的错误代码,用以指明在某个范围内具体发生了何种错误。某个特定范围可能会发生一系列相关错误,这些错误通常采用
enum
定义。 - User info(用户信息,类型为字典)有关错误的额外信息,其中或许包含一段“本地化描述”(localized description),或许还含有导致该错误发生的另外一个错误,经由此种信息,可将相关错误串成一条“错误链”(chain of errors)。
设计API时,NSError的第一种常见做法是通过委托协议来传递此错误。有错误发生时,当前对象会把错误信息经由协议中的某个方法传递给其委托对象(delegate)。如,NSURLConnection在其委托协议NSURLConnectionDelegate中就定义了如下方法:
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError*)error
当NSURLConnection出错之后(比如与远程服务器连接操作超时了),就会调用此方法以处理相关错误。这个委托方法未必非得实现不可:是不是必须处理此错误,可交由NSURLConnection
类的用户判断。这比抛出异常要好,因为调用者至少可以自己决定NSURLConnection
是否回报此错误。
NSError的另外一种常见用法是:经由方法的“输出函数”返回给调用者。比如:
- (BOOL)doSomething:(NSError**)error
传递方法的参数是个指针,而该指针本身又指向另一个指针,那个指针指向NSError
对象。或者也可以把它当作一个直接指向NSError对象的指针。这样一来,此方法不仅能有普通的返回值,而且还能经由“输出参数”把NSError对象回传给调用者。用法如下:
NSError *error = nil;
BOOL ret = [object doSomething:&error];
if (error) {
//There is an error
}
这种返回Boolen值的方法,若不关注具体错误信息,那么可直接判断这个Boolen值;若是关注具体错误,那就可以检查经由“输出参数”所返回的错误对象。在不想知道具体错误时,可以给error
参数传入nil
:
BOOL ret = [object doSomething:nil];
if (ret) {
//There is an error
}
实际上,在使用ARC时,编译器会把方法签名中的NSError**
转换成NSError* __autoreleasing* ,也就是说,指针所指的对象会在方法执行完毕后自动释放。这个对象必须自动释放,因为“doSomething:”方法不能保证其调用者可以把此方法中创建出来的NSError
释放掉,所以必须加入autorelease
。这就与大部分方法(以new、alloc、copy、mutableCopy开头的方法不在此列)的返回值所具备的语义相同了。
该方法通过下列代码把NSError
对象传递到“输出参数”中:
- (BOOL)doSomething:(NSError**)error {
//Do something that may cause an error
if (/*There was an error*/) {
if (error) {
//Pass the 'error' through the out-parameter
*error = [NSError errorWithDomain:domain code:code userInfo:userInfo];
}
return NO;//Indicate failure
} else {
return YES;//Indicate success
}
}
这段代码以*error
语法为error
参数“解引用”(dereference),也就是说,error
所指的那个指针现在要指向一个新的NSError
对象了。在解引用之前,必须先保证error
参数不是nil
,因为空指针解引用会导致“段错误”(segmentation fault)并使程序崩溃。
NSError
对象里的“错误范围”(domin)、“错误码”(code)、“用户信息”(user information)等部分应该按照具体的错误情况填入适当内容。错误范围应该被定义成NSString的全局变量,而错误码则定义成枚举类型为佳。如:
//ABCSomething.h
extern NSString *const ABCErrorDomin;
typedef NS_ENUM (NSUInteger, ABCError) {
ABCErrorUnknown = -1;
ABCErrorInternalInconsistency = 100;
ABCErrorGeneralFault = 105;
ABCErrorBadInput = 500;
}
//ABCSomething.m
NSString *const ABCErrorDomin = @"ABCErrorDomain";
最好能为自己程序库中所发生的错误指定一个专用的“错误范围”字符串,使用此字符串创建NSError
对象,并将其返回给库的使用者,这样就可以确信:该错误肯定是由你的程序库所回报的。用枚举表示错误码也是明智之举,因为这些枚举不仅解释了错误码的含义,而且还加了个有意义的名字。
八、理解NSCopying协议
使用对象时经常需要拷贝它。在OC中,此操作通过copy
方法完成。如果想令自己的类支持拷贝操作,那就要实现NSCopying协议,该协议只有一个方法:
- (id)copyWithZone:(NSZone*)zone
为何会出现NSZone
呢?因为以前开发程序时,会据此把内存分成不同的“区”(zone),而对象会创建在某个区里面。现在不需要了,每个程序只有一个区:“默认区”(default zone)。所以说,尽管必须实现这个方法,但不必担心zone
参数。
copy
方法由NSObject
实现,该方法只是以“默认区”为参数来调用“copyWithZone:”。我们总是想覆写copy
方法,其实真正需要实现的是“copyWithZone:”方法,这个问题一定要注意。
若想使某个类支持拷贝功能,只需声明该类遵从NSCopying协议,并实现其中的那个方法即可。例:
// Car.h
#import <Foundation/Foundation.h>
@interface Car : NSObject
@property (nonatomic, strong) NSMutableString* brand;
@property (nonatomic, strong) NSMutableString* type;
@property (nonatomic, assign) int price;
- (id)initWithBrand:(NSMutableString*)brand type:(NSMutableString*)type andPrice:(int)price
@end
// Car.m
#import "Car.h"
@implementation Car
-(id)copyWithZone: (NSZone*) zone {
NSLog(@"执行copyWithZone:");
Car* carCopy = [[[self class] allocWithZone: zone] initWithBrand:_brand type:_type andPrice:_price];
return carCopy;
}
@end
本例中直接把待拷贝对象交给“全能初始化”(designated initializer),令其执行所有初始化方法。有时候,可能要完成一些其他的操作,比如类对象中的数据结构可能并未在初始化方法中设置好,需要另行设置。这种情况下,就要把其他的数据一并拷贝过来。如:
//Person.h
@interface Person : NSObject <NSCopying>
@property (nonatomic, copy, readonly) NSString *name;
@property (nonatomic, strong, readonly) NSSet *friends;
- (id)initWithName:(NSString*)name;
- (void)addFriend:(Person*)person;
- (void)removeFriend:(Person*)person;
@end
// Person.m
#import "Person.h"
@implementation Person {
NSMutableSet *_friends;
}
- (NSSet*)friends {
return [_friends copy];
}
- (void)addFriend:(Person *)person {
[_friends addObject:person];
}
- (void)removeFriend:(Person *)person {
[_friends removeObject:person];
}
- (id)initWithName:(NSString *)name {
if (self = [super init]) {
_name = name;
_friends = [[NSMutableSet alloc] init];
}
return self;
}
- (id)copyWithZone:(NSZone *)zone {
Person *copy = [[[self class] allocWithZone:zone] initWithName:_name];
copy->_friends = [_friends mutableCopy];
return copy;
}
@end
这次实现的代码多了一些,它把本对象的_friends
实例变量复制了一份,令copy
对象的_friends
实例变量指向这个复制过的set
。注意,这里使用了->
语法,因为_friends
并非属性,只是个在内部使用的实例变量。
这个例子提出了一个有趣的问题,为什么要拷贝_friends
实例变量呢?不拷贝这个变量,直接令两个对象共享一个可变的set
是否更简单些?如果真的那么做了,那么在给原来的对象添加一个新朋友,拷贝过的那个对象居然也神奇地与之为友了。本例中,这显然不是我们想要的结果,然而,如果那个set
是不可变的,则无需复制。
通常情况下,应该像本例这样,采用全能初始化方法来初始化待拷贝的对象,不过有些时候不能这么做,因为全能初始化方法可能会产生一些“副作用”(side effect),这些附加操作对目前要拷贝的对象无益。比如,初始化方法可能要设置一个复杂的数据结构,可是在拷贝后的对象中,这个数据结构立刻就要用其他数据来覆写,所以没必要再设置一遍。
仔细看刚才的“copyWithZone:”方法,会发现,存放朋友对象的那个set
是通过mutableCopy
方法复制的。此方法来自另一个叫做NSMutableCopying
的协议。该协议与NSCopying
类似,也只定义了一个方法,然而方法名不同:
- (id)mutableCopyWithZone:(NSZone*)zone
mutableCopy
这个“辅助方法”(helper)与copy
相似,也是用默认的zone
参数来调“mutableCopyWithZone:”。如果你的类分为可变版本和不可变版本,那么就应该实现NSMutableCopying。若采用此模式,则在可变类中覆写“copyWithZone:”方法时,不要返回可变的拷贝,而应返回一份不可变的版本。无论当前实例是否可变,若需获取其可变版本的拷贝,均应调用mutableCopy
方法。同理,若需要不可变的拷贝,则总应该通过copy
方法来获取。
对于NSArray与NSMutableArray来说,下列关系总是成立:
-[NSMutableArray copy] => NSArray
-[NSArray mutableCopy] => NSMutableArray
此外,还有个微妙的情况:在可变对象上调用copy
方法会返回另外一个不可变类的实例。这样做是为了能在可变版本与不可变版本之间自由切换。要实现此目标,还有个办法,就是提供三个方法:copy
、immutableCopy
、mutableCopy
,其中copy
返回的对象类型与当前对象类型一致,而另外两个方法则分别返回不可变版本与可变版本的拷贝。但是,如果调用者不知道实例是否真的可变,那么这种做法就不太好了。
为了安全,还是只有返回不可变类型的方法与返回可变类型的方法比较好,把拷贝方法叫copy
是因为许多类没有“可变”与“不可变”之分。
此外,在编写拷贝方法时还要决定一个问题:执行“深拷贝”(deep copy)还是“浅拷贝”(shallow copy)。深拷贝的意思是:在拷贝对象自身时,将其底层数据也一并复制过去。
Foundation框架中的collection
类默认执行浅拷贝。如果有必要的话,自定义类中也可以增加一个执行深拷贝的方法。以NSSet
为例,该类提供下面这个初始化方法,用以执行深拷贝:
- (instancetype)initWithSet:(NSSet *)set copyItems:(BOOL)flag;
若flag
设置为YES,则该方法向数组中的每个元素发送copy
消息,用拷贝好的元素创建新的set
,并返回给调用者。
前面那个例子中,若想深拷贝,则要编写一个专供深拷贝的方法:
- (id)deepCopy {
Person *copy = [[[self class] alloc] initWithName:_name];
copy->_friends = [[NSMutableSet alloc] initWithSet:_friends copyItems:YES];
return copy;
}
有关深浅拷贝的内容,还可以看看OC Foundation框架 对象复制。
原文地址:https://blog.csdn.net/weixin_52192405/article/details/122441050
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.7code.cn/show_10001.html
如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱:suwngjj01@126.com进行投诉反馈,一经查实,立即删除!