目录
- 第三章:接口与API设计
- 第15条:用前缀避免命名空间冲突
- 第16条:提供“全能初始化方法”
- 第17条:实现description方法
- 第18条:尽量使用不可变对象
- 第19条:使用清晰而协调的命名方式
- 第20条:为私有方法名加前缀
- 第21条:理解Objective-C错误模型
- 第22条:理解NSCopying协议
第三章:接口与API设计
在构建应用程序时,可能会将其中的代码用在后续项目或供他人集成在别的项目,因此,很少有那种写完就不再复用的代码。编写接口时就会将其设计成易于复用的形式,这需要用到OC语言中常见的编程范式(paradigm),同时还需了解各种可能碰到的陷阱。
第15条:用前缀避免命名空间冲突
OC没有其他语言那种内置的命名空间(namespace)机制,所以在起名时应注意避免潜在的命名冲突,否则就很容易重名。如果发生命名冲突(namespace clash),那么应用程序的链接过程就会因为出现重复符号而出错。
比如在工程中的两个文件中都实现了名为Person的类,那么就会导致Person所对应的类符号和元类符号各定义了两次,从而导致编译错误:
出现这种情况一般都是把两个相互独立的程序库都引入到了当前项目中,而它们又恰好有重名的类。
比编译器无法链接更糟的情况是,在运行期载入了含有重名类的程序库,此时,“动态加载器”(dynamic loader)就遭遇了“重名符号错误”(duplicate symbol error),从而导致Crash。
避免以上问题的唯一办法就是变相实现命名空间:为所有名称都加上适当前缀。
- 以何前缀命名?
- 假设你所在的公司叫做Effective Widgets,那么就可以在所有应用程序都会用到的那部分代码中使用EWS前缀;如果有些代码只用于名为Effetive Browser的浏览器项目中,那就在这部分代码中使用EWB作前缀;如果有些即便加了前缀,也难保不出现命名冲突,但是几率会小很多。
- 需要注意的是,Apple宣称其保留使用所有 “两字母前缀”(two-letter prefix)的权利,所以你自己选用的前缀应该是三个字母的。假如开发者不遵守这条规则,使用TW这两个字母作为前缀,就会出现问题。iOS 5.0 SDK发布时,包含了Twitter框架,此框架就使用TW作前缀,这就很可能出现重复符号错误。
- 不仅是类名,应用程序中所有名称都应加前缀
- 分类以及分类中的方法加上前缀(🚩详见25条)。
- 类实现文件中所使用的纯C函数及全局变量。因为在编译好的目标文件中,这些名称是要算做 “顶级符号” (top-level symbol)的。
- 若自己开发程序库时用到了第三方库,则应该为其中的名称加上前缀
- 如果你使用了第三方库(以手动管理而非Cocopods管理),并准备将其再发布为程序库供他人开发使用时,尤其注意符号问题。
- 例如,你准备发布的程序库叫做EOCLibrary,其中引入了名为XYZLibrary的第三方库,那么就应该把XYZLibrary中的所有名字都冠以EOC——EOCXYZLibrary。
第16条:提供“全能初始化方法”
可为对象提供必要信息以便其能完成工作的初始化方法叫做 “全能/指定初始化方法”(designated initializer)。
类实例的初始化方法可能不止一种,我们要选定一个作为该类的全能初始化方法,令其他初始化方法均调用此方法。这样当初始化操作有变化时,只需要改动全能初始化方法,无须改动其他初始化方法。
比如,指定矩形类EOCRectangle的初始化方法为:
@implementation EOCRectangle
- (instancetype)initWithWidth:(float)width andHeight:(float)height {
self = [super init];
if (self) {
_width = width;
_height = height;
}
return self;
}
@end
然后像下面这样,参照其中一种版本来避免开发者调用原始的init
方法:
//1. .h文件中使用NS_UNAVAILABLE来禁用该初始化方法
+ (instancetype)new NS_UNAVAILABLE;
- (instancetype)init NS_UNAVAILABLE;
//2. 使用全能初始化方法覆写init
- (instancetype)init {
return [self initWithWidth: 5.0f andHeight: 8.0f];
}
//3. 抛出异常
- (instancetype)init {
@throw [NSException exceptionWithName: NSInternalInconsistencyException reason: @"必须使用initWithWidth:andHeight:代替" userInfo: nil];
}
若现在想创建一正方形类EOCSquare继承于EOCRectangle,指定它的全能初始化方法为:
- (instancetype)initWithDimension:(float)dimension {
return [super initWithWidth: 5.0f andHeight: 8.0f];
}
注意,该方法同样调用了父类的全能初始化方法,全能初始化方法的调用链一定要维系。
这时,调用者可能会使用initWithWidth:andHeight:
或者init
方法来初始化实例,这不是我们所希望的,所以当类继承时,如果子类的全能初始化方法与父类方法的名称不同,那么总应该覆写父类的全能初始化方法:
- (instancetype)initWithWidth:(float)width andHeight:(float)height {
float dimension = MAX(width, height);
return [self initWithDimension: dimension];
}
此时已经不再需要重写init
方法,因为调用到的父类中的init方法中调用的是initWithWidth:andHeight:
方法,而该方法已被子类重写,所以调用的也是子类的实现(即指定的全能初始化方法)。
当然有时我们不想覆写父类的全能初始化方法,而认为是方法调用者自己犯了错误,常用的办法就是在父类的全能初始化方法中抛出异常:
- (instancetype)initWithWidth:(float)width andHeight:(float)height {
@throw [NSException exceptionWithName: NSInternalInconsistencyException reason: @"必须使用initWithDimension:代替" userInfo: nil];
}
此时调用init
方法也会抛出此异常了,那就直接覆写,以合理的默认值来调用当前类的全能初始化方法:
- (instancetype)init {
return [self initWithDimension: 5.0f];
}
不过,在OC程序中,只有当发生严重错误时,才应该抛出异常(🚩详见21条),所以,初始化方法抛出异常仍是不得已之举,表明实例真的没办法初始化了。
如果某对象的实例有两种完全不同的创建方式,必须分开处理,那么就需要编写多个全能初始化方法。以NSCoding
协议为例:
此协议提供了“序列化机制”(serialization mechanism),对象可以此指明其自身的编码(encode)及解码(decode)方式。序列化与反序列化机制可参考这篇文章:【iOS】数据持久化(二)之归档和解档(iOS 13以后)。
MacOS的AppKit与iOS的UIKit这两个UI框架都广泛运用此机制:将对象序列化,并保存至XML格式的“NIB”文件中。这些NIB文件通常用来存放viewController及其视图布局。加载NIB文件时,系统会在解压缩(unarchiving)的过程中解码viewController。
我们在实现initWithCoder:
方法时一般不调用平常所使用的那个全能初始化方法,因为该方法要通过“解码器”(decoder)将对象数据解压缩,所以和普通的初始化方法不同:
// EOCRectangle
- (instancetype)initWithCoder:(NSCoder *)coder {
self = [super init];
if (self) {
_width = [coder decodeFloatForKey: @"width"];
_height = [coder decodeFloatForKey: @"height"];
}
return self;
}
// EOCSquare
- (instancetype)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder: coder];
if (self) {
// EOCSquare的特定初始化方法
}
return self;
}
每个子类的全能初始化方法都应该调用其父类的对应方法,并逐级向上,实现initWithCoder:
时也要这样,这样编写出来的子类才会完全遵守NSCoding协议(fully NSCoding compliant)。如果不这么做,EOCSquare的该方法没有调用其父类的该同名方法,而是调用了自身或父类的其他全能初始化方法,那么父类的initWithCoder:
就没机会执行,也就无法将_width
及_height
这两个实例变量解码了。
第17条:实现description方法
使用NSLog打印对象,就会给对象发送description
消息,该方法返回一个字符串,所以打印对象用%@
。
打印数组对象:
NSArray* object = @[@"A String", @(123)];
NSLog(@"%@", object);
// (
// "A String",
// 123
// )
打印自定义类的对象,输出:<EOCPerson: 0x600003c0c0b0>
。
description
方法定义在NSObject协议里,不过NSObject类也实现了它。因为NSObject不是唯一的根类,所以许多方法都要定义在NSObject协议里。比方说,NSProxy也是一个遵从了NSObject协议的根类,由于description等方法定义在NSObject协议里,因此想NSProxy这种根类及其子类也必须实现它们。
如果我们想打印对象的详细信息,可以在自己的类里重写description
方法,否则打印信息时就会调用NSObject类所实现的默认方法(返回类名和对象的内存地址):
id object = [NSObject new];
NSLog(@"%@", object);
// <NSObject: 0x600003cdc0f0>
这种信息也只有在想判断两指针是否真的指向同一对象时才有用处。
现在重写EOCPerson类的description方法:
- (NSString *)description {
return [NSString stringWithFormat:@"<%@: %p, \"%@ %@\">", [self class], self, _firstName, _lastName];
}
// <EOCPerson: 0x600000e09fa0, "Jacky Wan">
借助字典的description
方法来更好地打印对象属性信息:
- (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}>
这比直接拼接打印属性信息写法更易维护,如果以后新增属性并且要在description
中打印,呢么只需修改字典内容即可。
NSObject协议中还有个方法debugDescription
:
该方法是开发者在调试器(debugger)中以控制台命令打印对象时才调用的(断点,然后使用LLDB命令poprint-object
)。
在NSObject类中的debugDescription
的默认实现时直接调用description
,若不想在代码打印对象时输出太详尽的对象描述信息,而是在调试时才这么做,比如在调试时再打印“类名和对象的内存地址”,那么就在自定义类中实现debugDescription
方法,返回更详尽的信息。比如,NSArray类:
第18条:尽量使用不可变对象
设计类的时候,应充分运用属性来封装数据(🚩详见第6条),尽量把对外公布的属性设为只读readonly
,而且只在确有必要时才将属性对外公布。
比如,一些模型对象通过初始化方法创建,其后无需改动其属性值(比如地图模型),那么属性对外应设只读。这样只要使用方试着修改属性值就会编译错误,对象本身的数据结构也就不可能出现不一致的对象,比如地图模型的经纬度等数据不会发生变动。
在【🚩第8条】中所述,如果把可变对象放入collection后又修改其内容,那么就很容易破坏set
的内部数据结构,使其失去固有的意义,所以要尽量减少对象的可变内容。
把属性设置为readonly
后,该属性就没有了setter
方法,就可以不指定其内存管理语义而采用默认了:
@property (nonatomic, readonly)NSString* title;
虽说如此,我还是应该在文档中里指明实现所用的内存管理语义,这样以后把它变为可读写属性时就会简单一些。
有时我们想在对象内部修改属性,但是只对外只读。这时候就可以在类扩展中将属性重新声明为可读可写readwrite
:
// .h
@property (nonatomic, readonly)NSString* title;
// .m 在extension中将readonly扩展为readwrite
@property (nonatomic, copy, readwrite)NSString* title;
如果该属性是nonatomic
,那么这样做可能会产生“竞争条件”(race condition)。在对象内部写入某属性时,外部观察者也许正读取该属性。若想避免此问题,可在必要时通过GCD中“派发队列”(dispatch queue,🚩详见41条)等手段,将(包括对象内部的)所有数据存取操作都设为同步操作。
现在,就只能在类实现代码内部设置这些属性值了。但即便对外设置了readonly,使用方法也可以使用 “键值编码”(Key-Value Coding,KVC) 来修改值:
[title setValue: @"abc" forKey: @"title"];
❗️这样做等于违规地绕过了本类所提供的API,出现问题得自己承担后果。更甚者,直接用类型信息查询功能查出属性所对应的实例变量在内存布局中的偏移量,以此来人为设置这个实例变量的值。
从技术上讲,即便某个类没有对外公开setter方法,也依然可以想办法修改对应的属性,然而不应该因为这个原因而忽视上述建议。
还需注意一件事情:对象里表示各种collection
的那些属性究竟应该设成可变的、还是不可变的?
比如有个类表示个人信息,其中有一个属性表示此人的诸位朋友。假如开发者可以添加或删除此人的朋友,那么这个属性就需要用可变的Set:
法1: 通常会提供一个readonly
属性供外界使用,该属性将返回不可变的set
,而此set
则是内部那个可变的set
的一份拷贝。
// .h
@interface EOCPerson : NSObject
// 其他属性...
@property (nonatomic, strong, readonly)NSSet* friends;
// 其他方法...
@end
// .m
@implementation EOCPerson {
NSMutableSet* _internalFriends;
}
- (NSSet *)friends {
return [_internalFriends copy];
}
// ...
@end
法2:也可用NSMutableSet来实现friends属性,令该类的用户不借助其他方法而直接操作此属性,但这种过分解耦(decouple)数据的做法很容易出bug。
不要在返回的对象上查询类型以确定其是否可变。 比如使用isKindOfClass:
方法,来确定其是否可变。
开发者或许不建议使用方修改对象中的数据,但可能由于collection很大,copy耗时,而直接将可变collection
返回,但开发者对外声明为不可变collection
了。我们不要假设其为可变collection
,然后通过isKindOfClass:
方法来确定其为可变collection
后对它进行操作。
不要把可变的collection作为属性公开,而应提供相关方法,以此修改对象中可变的collection。
第19条:使用清晰而协调的命名方式
OC中的命名虽长,但其所要表达的意思清晰。比如替换字符串的方法stringByReplaceOccurrencesOfString:withString:
,在其他语言中方法名仅是replace("Az", "By")
,不能清晰地表达出两个参数到底是谁替换谁。
方法与变量名使用 “驼峰式大小写命名法”(camel casing) ——以小写字母开头,其后每个单词首字母大写;类名也用驼峰式命名法,不过其首字母大写,而且前面通常还有两三个大写的前缀字母。
方法命名
在OC中可以这样定义初始化方法:
- (instancetype)initWithSize:(float)width :(float)height;
语法上没有任何问题,但想让该方法被调用时每个变量的含义清晰明了,如下:
- (instancetype)initWithSize:(float)width height:(float)height;
方法命名要言简意赅,也不能太过长,清晰而不啰嗦,能准确表达方法所执行的任务即可。
命名规则
-
如果方法的返回值是新创建的,那么方法名的首个词应该是返回值的类型,除非前面还有修饰符,例如
localizedString
。属性的存取方式不遵循这种命名方式,因为一般认为这些方法不会创建新对象,即便是返回内部对象的一份拷贝,我们也认为那相当于原有的对象。这些存取方法应该按照其所对应的属性来命名。 -
应该把表示参数类型的名词放在参数前面。
-
如果方法要在当前对象上执行操作,那么就应该包含动词;若执行操作时还需要参数,则应该在动词后面加上一个或多个名词。
-
不要使用
str
这种简称,应该用string
这样的全称。 -
Boolean 属性应加 is 前缀。如果某方法返回非属性的 Boolean 值,那么应该根据其功能,选用 has 或 is 当前缀。
@property (nonatomic, assign, getter=isEnabled)BOOL enabled; //NSString - (BOOL)isEqualToString:(NSString *)string;
-
将
get
这个前缀留给那些借由“输出参数”来保存返回值的方法,比如把返回值填充到“C语言式数组”(C-style array)里的那种方法就可以使用这个方法做前缀。- (void)getCharacters:(unichar *)buffer range:(NSRange)aRange;
不同于传统的传值调用,即传递参数的原始值副本,而并不影响原始值本身。输出参数允许函数在执行过程中改变参数的值,并将这些新的值传递回调用者。
类与协议的命名
- 应该为类和协议的名字加上前缀,以避免命名空间冲突。
- 命名同方法命名,使其从左至右读起来较为通顺。
- 继承时要遵守其命名惯例,比如继承自
UIView
的子类,命名末尾必须是View。 - 自定义的委托协议,其名称中应该包含委托发起方的名称,后面再跟上Delegate一词,参照
UITableViewDeleagte
。
第20条:为私有方法名加前缀
-
给私有方法的名称加上前缀,这样可以很容易地将其同公共方法区分开。
-
不要单用一个下划线做私有方法的前缀,因为这种做法是预留给苹果公司用的。
-
给私有方法的名称加上前缀的原因:
- 加个前缀便于和公共方法区分开,有助于调试。
- 便于修改方法名或方法签名。
修改公共方法的名称或签名之前要三思,因为公共API不便随意改动。如果改了的话,使用这个方法的所有开发者都必须更新其代码。而修改私有方法,则只需同时修改本类内部的相关代码即可,不会影响到公共API。给私有方法加前缀就能很容易看出来哪些方法可以随意修改,哪些不应该轻易改动。 - OC 是动态运行时语言,它不像C++和Java那样可以真正声明为私有方法,所以一般我们要在命名中体现出 “私有方法” 语义。
OC语言没办法将方法标为私有。每个对象都可以响应任意消息,而且可以在运行期检视某个对象所能直接响应的消息。根据给定的消息查出其对应的方法,这一工作要在运行期才能完成,所以OC中没有那种约束方法调用的机制用以限定谁能调用此方法、能在哪个对象上调用此方法以及何时能调用此方法。
-
使用何种前缀可根据个人喜好,比如可以使用
p_
作为前缀,p表示private。 -
某次修订后编译器已经不要求使用方法前必须先行声明,所以私有方法一般只在实现的时候声明。
-
苹果喜欢单用一个下划线作为私有方法的前缀,因此苹果在文档中说开发者不应该单用一个下划线做前缀。如果我们这么做了,就可能会在子类中无意覆写父类(苹果类)的同名私有方法,这样会导致本该调用父类的实现而现在却调用自己覆写的实现,从而引发问题。例如,UIViewController 有一个名叫 _resetViewController 的私有方法。你可能会无意间覆写但是你根本不会察觉到,除非你深入研究过 UIViewController。导致的问题是:你的子类的这个方法会被频繁调用。
-
此外,你可能会继承来自三方框架的类,你不知道它们的私有方法以什么名称前缀,除非该框架在文档中明示或者你阅读了源码。同样的别人也可能从你写的类中继承子类。所以为了避免重名问题,可以把自己一贯使用的类名前缀用作私有方法的前缀。
第21条:理解Objective-C错误模型
首先要注意的是,自动引用计数ARC在默认情况下不是“异常安全的”,即如果抛出异常,那么本应在作用域末尾释放的对象现在却不会自动释放了。
如果想生成“异常安全”的代码,可以用过设置编译器的标志来实现,不过这将引入一些额外代码,在不抛出异常时,也照样要执行这部分代码。需要打开的编译器标志叫做-fobjc-arc-exceptioins
。
即使不用ARC,在释放之前如果抛出异常了,那么该资源就不会被释放了:
@throw [NSException exceptionWithName:@"ExceptionName" reason:@"There was an error" userInfo:nil];
[someResource release];
在抛出异常前先释放someResource,这样做当然能解决此问题,不过要是带释放的资源有很多,而且代码的执行路径更为复杂的话,释放资源的代码就容易写得很乱
所以应只有发生了可使整个应用程序崩溃的严重错误时,才应使用异常,抛出异常后,无需考虑恢复问题,应用程序此时也会随之退出。比如有人直接使用了抽象基类,在子类必须覆写的父类方法里抛出异常(抽象基类之前有提到过)。
既然异常只用于处理严重错误(fatal error),那么对其他错误怎么办呢?
在出现“不那么严重的错误”(nonfatal error)时,OC语言所用的编程范式为:
-
令方法返回
nil
/0
,比如,如果初始化方法无法根据传入的参数来初始化当前实例,那么就可以令其返回。 -
使用NSError,此用法更加灵活,因为经由此对象,我们可以把导致错误的原因回报给回调者。NSErroe对象里封装了三条信息:
- Error domain(错误范围,类型为字符串):错误发生的范围,也就是产生错误的根源。通常定义成NSString类型的全局常量。
最好为你自己的库指定一个专用的 “错误范围” 字符串,这样使用方可以知道该错误在你的库中产生。//EOCErrors.h extern NSString* const EOCErrorDomain; //EOCErrors.m NSString* const EOCErrorDomain = @"EOCErrorDomain";
- Error code(错误码,类型为整数):用以指明在某个范围内具体发生了何种错误。通常定义成枚举类型。
用枚举不仅可以解释错误码的含义,而且还给它们起了个有意义的名字。还可以在定义这些枚举的头文件里对每个错误类型加以描述。typedef NS_ENUM(NSUInteger, EOCError) { EOCErrorUnknown = -1, EOCErrorInternalInconsistency = 100, EOCErrorGeneralFault = 105, EOCErrorBadInput = 500, }
- User info(用户信息,类型为字典):有关此错误的额外信息,或许还有导致该错误发生的另一个错误,经由此种信息,可将相关错误串成一条“错误链”(chain of errors)。
- Error domain(错误范围,类型为字符串):错误发生的范围,也就是产生错误的根源。通常定义成NSString类型的全局常量。
创建NSError对象的方法:
NSError常见用法:
-
通过委托协议来传递错误,有错误发生时,委托方会把错误信息经由协议方法传给 delegate 对象。
比如协议NSURLConnectionDelegate之久定义了如下方法:- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;
这比抛出异常好,因为调用方可以自己决定是否要实现该协议方法,是否要处理此错误。
-
经由方法“输出参数”返回给调用者:
- (BOOL)doSomething:(NSError **)error;
传递给方法的参数是个指针,而该指针本身又指向另外一个指针,那个指针指向NSError对象(可当作直接指向NSError对象的指针)。这样一来,此方法不仅能有普通的返回值,还能经由“输出参数”把NSError对象回传给调用者,其用法如下:
NSError* error = nil; BOOL ret = [errorObject doSomething: &error]; if (error) { // There was an error }
若是不关注具体错误,可以给
error
参数传入nil
,直接判断返回的布尔值就行:BOOL ret = [errorObject doSomething: nil]; if (ret) { // There was an error }
实际上,在使用ARC时,编译器会把方法签名中的NSError**
转换成NSError* __autoreleasing*
,也就是说,指针所指向的对象会在方法执行完毕后自动释放。这个对象必须自动释放,因为doSomething:
方法不能保证其调用者可以把此方法中创建的NSError释放掉,所以必须加入autorelease
。该方法通过下列代码把NSError对象传递到“输出参数”中:
- (BOOL)doSomething:(NSError **)error {
// Do something that may cause an error
if (/*There was an error*/) {
if (error) {
*error = [NSError errorWithDomain: domain code: code userInfo: userInfo];
}
return NO;
} else {
return YES;
}
}
这段代码以*error
语法为error参数“解引用”(dereference),也就是说,error所指的那个指针现在要指向一个新的NSError对象了。在解引用之前,必须先保证error参数不是nil,因为空指针解引用会导致“段错误”(segmentation fault)并使应用程序崩溃。调用者在不关心具体错误时,会给error参数传入nil,所以必须判断这种情况。
第22条:理解NSCopying协议
-
拷贝的目的:
- 产生一个副本对象,跟源对象互不影响;
- 修改了源对象,不会影响副本对象;
- 修改了副本对象,不会影响源对象。
-
iOS提供了2个拷贝方法:
- copy:不可变拷贝,产生不可变副本;
- mutableCopy:可变拷贝,产生可变副本。
-
深拷贝和浅拷贝:
拷贝类型 拷贝方式 特点 深拷贝 内存拷贝,让目标对象指针和源对象指针指向 两片
内容相同的内存空间1. 不会增加被拷贝对象的引用计数; 2. 产生了一个内存分配,出现了两块内存。 浅拷贝 指针拷贝,对内存地址的复制,让目标对象指针和源对象指针指向 同一片
内存空间1. 会增加被拷贝对象的引用计数; 2. 没有进行新的内存分配。 注意:如果是小对象如NSString,可能通过 Tagged Pointer
来存储,没有引用计数。深浅拷贝详见这篇博客:【Objective-C】对深浅拷贝的理解
简而言之:
- 深拷贝:内容拷贝,产生新对象,不增加对象引用计数
- 浅拷贝:指针拷贝,不产生新对象,增加对象引用计数
区别:是否影响了引用计数、是否开辟了新的内存空间。
-
对mutable和immutable对象(系统类)进行copy与mutableCopy的结果:
源对象类型 拷贝方式 目标对象类型 拷贝类型 可变对象 copy 不可变 深拷贝 可变对象 mutableCopy 可变 深拷贝 不可变对象 copy 不可变 浅拷贝
不可变对象 mutableCopy 可变 深拷贝 -
以上对collection容器对象进行的深浅拷贝是指对容器对象本身的,对collection中的对象执行的默认都是浅拷贝。也就是说只拷贝容器对象本身,而不复制其中的数据。
主要原因是,容器内的对象未必都能拷贝,而且调用者也未必想在拷贝容器时一并拷贝其中的每个对象。
浅拷贝之后的内容与原始内容均指向相同对象。而深拷贝之后的内容所指向的对象是原始内容中相关对象的一份拷贝。 -
如果想要实现对自定义对象的拷贝,需要遵守
NSCopying
协议,并实现copyWithZone:
方法。- 如果要浅拷贝,
copyWithZone:
方法就返回同一个对象:return self;
- 如果要深拷贝,
copyWithZone:
方法中就创建新对象,并给希望拷贝的属性赋值
- 如果要浅拷贝,
-
copy方法由NSObject实现,该方法只是以NSZone为参数来调用
copyWithZone:
,我们总想覆写copy方法,其实真正需要实现的却是copyWithZone:
方法。
如果自定义对象支持可变拷贝和不可变拷贝(分为可变版本和不可变版本),那么还需要遵NSMutableCopying
协议,并实现mutableCopyWithZone:
方法,返回可变副本。而copyWithZone:
方法返回不可变副本。使用方可根据需要调用该对象的 copy 或 mutableCopy 方法来进行不可变拷贝或可变拷贝。