第3章 接口和API设计
第15条:用前缀避免命名空间冲突
OC没有其他语言那种内置的命名空间机制。因此,我们在起名时要设法避免潜在的命名冲突,否则很容易就重名了。若是发生重名冲突,那么应用程序相应的链接过程就会出错。例如:
错误原因在于,应用程序中的两段代码都各自实现了名为EOCTheClass的类,会导致该类所对应的类符号和“元类”符号各定义了两次。
你可能是把两个相互独立的程序库都导入到当前项目,而它们又恰好有重名的类。
避免此问题的唯一办法就是变相实现命名空间:为所有名称都加上适当前缀。所选前缀可以是与公司、应用程序或二者皆有关联之名。
使用Cocoa创建应用程序时一定要注意,Apple宣称其保留使用所有“两字母前缀”的权利,所以你自己选用的前缀应该是三个字母的。
不仅是类名,应用程序中的所有名称都应加前缀。如果要为既有类新增“分类”,那么一定要给“分类”及“分类”中的方法加上前缀。开发者可能会忽视另外一个容易引起命名冲突的地方,那就是类的实现文件中所用的纯C函数及全局变量,在编译好的目标文件中,这些名称是要算做“顶级符号”的。
并且每个类中相应的方法也应该有该类相应的前缀,用以分别该类和其他类中的名称相同的方法。这样做还有个好处就是:若此符号出现在栈回溯信息中,那么我们就很容易就能判断问题出现在那个类的哪个方法了。
如果用第三方库编写自己的代码,并准备将其在发布为程序库供他人开发应用程序所用,那么尤其要注意重复符号问题。你的程序库所包含的那个第三方库也许还会为应用程序本身所引入。这时应该给你所用的那一份第三方库代码都加上你自己的前缀。
要点:
- 选择与你的公司、应用程序或二者皆有关联之名称作为类名的前缀,并在所有代码中均使用这一前缀。
- 若自己所开发的程序库中用到了第三方库,则应为其中的名称加上前缀。
第16条:提供“全能初始化方法”
所有对象均要初始化。以iOS的UI框架UIKit为例,其中有个类叫做UITableViewCell,初始化该类对象时,需要指明其样式及标识符,标识符能够区分不同类型的单元格。我们把这种可为对象提供必要信息以便其能完成工作的初始化方法叫做“全能初始化方法”。
如果创建类实例的方法不止一种,那么这个类就会有多个初始化方法。这当然很好,不过仍然要在其中选定一个作为全能初始化方法,令其他初始化方法都来调用它。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:”是全能初始化方法。也就是说,其余的初始化方法都要调用它。于是,只有在全能初始化方法中,才会存储内部数据。而当底层数据存储机制改变时,只需修改此方法的代码就好,就无须改动其他初始化方法。
比如说,我们要编写一个表示矩形的类。其接口可以这样写:
#import <Foundation/Foundation.h>@interface EOCRectangle : NSObject
@property (nonatomic, assign, readonly) float width;
@property (nonatomic, assign, readonly) float height;
@end
我们把属性声明为只读。不过这样一来,外界就无法设置Rectangle对象的属性了。开发者可能会提供初始化方法以设置这两个属性:
- (id)initWithWidth:(float)width andHeight:(float)height {if ((self = [super init])) {_width = width;_height = height;}return self;
}
那如果我们用[[EOCRectangle alloc] init]来创建矩形会如何呢?这么做时合乎规则的,因为EOCRectangle的超类NSObject实现了这个名为init的方法,调用完该方法后,全部实例变量都将设为0。
如果我们想要用该方法初始化之后的矩形是一个默认的宽度与高度值,或是抛出异常,指明本类实例必须用“全能初始化方法”来初始化。我们要重新覆写init方法:
//Using default values
- (id)init {return [self initWithWidth:5.0 fandHeight:10.0f];
}//Throwing an exception
- (id)init {@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Must use initWithWidth:andHeight: instead." userInfo:nil];
}
我们设置默认值的那个init方法调用了全能初始化方法
然后我们现在创建一个EOCSquare类(本类表示正方形),令其成为EOCRectangle的子类。新类的初始化方法应该如下编写:
#import "EOCRectangle.h"@interface EOCSquare : EOCRectangle
- (id)initWithDimension:(float)dimension;
@end@implementation EOCSquare- (id)initWithDimension:(float)dimension {return [super initWithWidth:dimension andHeight:dimension]
}@end
上述方法就是EOCSquare类的全能初始化方法,而且它调用了超类的全能初始化方法。回过头看看EOCRectangle类的实现代码,你就会发现,那个类也调用了其超类的全能初始化方法。全能初始化方法的调用链一定要维系。然而,调用者可能会使用init方法或initWithWidth: andHeight:方法来初始化EOCSquare对象,这样子会创建出宽高不一致的正方形。
于是就产生了一个问题:如果子类的全能初始化方法与超类方法的名称不同,那么总应覆写超类的全能初始化方法,并在该方法调用该类的全能初始化方法。
在上方的例子中就应该像下面这样覆写EOCRectangle的全能初始化方法:
- (id) initWithWidth: (float)width andHeight: (float)height {float dimension = MAX(width, height);return [self initWithDimension:dimension];
}
覆写了这个方法后,即便使用init来初始化EOCSquare对象,也能照常工作。原因在于,EOCRectangle类覆写了init方法,并以默认值为参数调用了该类的全能初始化方法。
有时候我们不想覆写超类的全能初始化方法,常用的办法是覆写超类的全能初始化方法并于其中抛出异常:
- (id) initWithWidth:(float)width andHeight:(float)height {@throw [NSException exceptionWithName:NSInternalInconsistancyException reason:@"Must use initWithDimension: instead." userInfo:nil];
}
这时候在EOCRectangle与EOCSquare这个例子中,调用init方法也会抛出异常,因为init方法也得调用“initWithWidth:andHeight:”。此时可以覆写init方法,并在其中以合理的默认值来调用initWithDimension:方法:
- (id) init {return [self initWithDimension:5.0f];
}
有时候可能需要编写多个全能初始化方法。比如说:如果某对象的实例有两种完全不同的创建方法,必须分开处理。以NSCoding 协议为例,此协议提供了“序列化机制”,对象可依此指明其自身的编码及解码方式。NSCoding协议定义了下面这个初始化方法,遵从该协议者都应实现此方法:
- (id) initWithCoder:(NSCoder *)decoder;
我们在实现此方法时一般不调用平常所使用的那个全能初始化方法,因为该方法要通过“解码器”将对象数据解压缩,所以和普通的初始化方法不同。而且,如果超类也实现了NSCoding,那么还需要调用超类的initWithCoder:方法。于是,子类中有不止一个初始化方法调用了超类的初始化方法,因此,严格的说,在这种情况下出现了两个全能初始化方法。
#import <Foundation/Foundation.h>@interface EOCRectangle : NSObject<NSCoding>@property (nonatomic, assign, readonly) float width;
@property (nonatomic, assign, readonly) float height;- (id)initWithWidth:(float)width andHeight:(float)height;
@end@implementation EOCRectangle//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 initializerif ((self = [super init])) {_width = [decoder decodeFloatForKey:@"width"];_height = [decoder decodeFloatForKey:@"height"];}return self;
}@end
NSCoding协议的初始化方法没有调用本类的全能初始化方法,而是调用了超类的相关方法。然而,若超类也实现了NSCoding,则需改为调用超类的initWithCoder.初始化方法,例如,在此情况下,EOCSquare类就得这么写:
#import "EOCRectangle.h"@interface EOCSquare : EOCRectangle
- (id)initWithDimension:(float)dimension;
@end@implementation EOCSquare//Designated initializer
- (id) initWithDimension:(float)dimension {return [super initWithWidth:dimension andHeight:dimension];
}//Superclass designated initializer
- (id) initWithWidth:(float)width andHeight:(float)height {float dimension = MAX(width, height);return [self initWithDimension:dimension];
}//NSCoding designated initialzer
- (id) initWithCoder:(NSCoder *)decoder {if ((self = [super initWithCoder:decoder])) {//EOCSquare's specific initalizer}return self;
}@end
每个子类的全能初始化方法都应该调用其超类的对应方法,并逐层向上,实现initWithCoder:时也要这样,应该先调用超类的相关方法,然后在执行与本类有关的任务。这样编写出来的EOCSquare 类就完全遵守NSCoding协议了。
要点:
- 在类汇总给你提供一个全能初始化方法,并与文档里指明。其他初始化方法均应调用此方法。
- 若全能初始化方法与超类不同,则需覆写超类中的对应方法。
- 如果超类的初始化方法不适用于子类,那么应该覆写这个超类方法,并在其中抛出异常。
第17条:实现description方法
调试程序时,需要打印并查看对象信息。一种办法是编写代码把对象的全部属性都输出到日志中。不过最常见的做法还是像下面这样:
NSLog(@"object = %@", object);
在构建需要打印到日志的字符串时,object对象会收到description消息,该方法所返回的描述信息将取代“格式字符串”里的%@。比方说,object是个数组,若用下列代码打印其信息:
NSArray *object = @[@"A string", @(123)];
NSLog(@"object = %@", object);
则会输出
object = {"A string"123
}
然而,在自定义的类上这么做,那么输出的信息却是下面这样:
object = <EOCPerson: 0x7fd9a1600600>
但是可以看到,这种方法只是打印了类名和对象的内存地址。如果我们想要打印更多的信息。我们只需要覆写description 方法并将描述此对象的字符串返回即可,否则打印信息时就会调用NSObject类所实现的默认方法。例如,有下面这个 代表个人信息的类
#import <Foundation/Foundation.h>@interface EOCPerson : NSObject
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;- (id)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName;
@end@implementation EOCPerson- (id)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName {if ((self = [super init])) {_firstName = [firstName copy];_lastName = [lastName copy];}return self;
}
@end
该类的description方法通常可以这样实现:
- (NSString *) description {return [NSString stringWithFormat:@"<%@: %p, \"%@ %@\">", [self class], self, _firstName, _lastName];
}
如果按照上方的代码的话,那么EOCPerson对像的输出结果就会如下:
EOCPerson *person = [[EOCPerson alloc] initWithFirstName:@"Bob" lastName:@"Smith"];
NSLog(@"person = %@", person);
//Output
//person = <EOCPerson: 0x7fb249c030f0, "Bob Smith">
有个简单的办法,可以在description中输出很多互不相同的信息,那就是借助NSDictioniary类的description方法。此方法输出的信息的格式如下:
{key: value;foo: bar;
}
自定义的description方法中,把待打印的信息放到字典里面,然后将字典对象的description方法所输出的内容包含在字符串里并返回,例如:下面这个类表示某地点的名称和地理坐标(纬度与经度):
#import <Foundation/Foundation.h>@interface EOCLocation : 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@imiplementation EOCLocation
- (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,@{@"latitude":_title,@"latitude":@(_latitude),@"longitude":@(_longitude)}];
}
输出的信息格式为:
location = <EOCLocation: 0x7f98f2e01d20, {latitude = "51.506"longitude = 0;title = London;
}>
用NSDictionary来实现该功能可以令代码更容易维护:如果以后还要向类中新增属性,并且要在description方法中打印,那么只需要修改字典内容即可
debugDescription一种描述方法,和description差不多,就是描述的位置不一样,description是在函数调用类的时候触发方法才输出的,而debugDescription是在控制台中使用命令打印该对象时才调用的。当然加断点查看时也可以看到debugDescription的描述。
如果你在description不想将一些内容输出的话,你就可以将那些数据写在debugDescription中,让程序员自己调试时可以方便的看到这些数据,而description方法就输出你想要让用户看到的信息就行了。
要点:
- 实现description方法返回一个有意义的字符串,用以描述该实例
- 若想在调试时打印出更详尽的对象描述信息,则应实现debugDescription方法。
第18条:尽量使用不可变对象
默认情况下,属性是“既可读又可写的”,这样创建出来的类都是可变的。但是在设计类的时候,我们应充分运用属性来封装数据,在使用属性时,则可将器声明为“只读”。
一般情况下,我们要建模的数据未必都需要改变。比如说,我们从服务器中请求来的数据加载成UI显示在屏幕上,这些数据一般都是不需要去改变的。因此,我们尽量要减少对象中的可变内容。
具体到编程中,则应该尽量把对外公布出来的属性设为只读,而且只在确有必要时才将属性对外公布。
为了将某类做成不可变的类,需要把所有属性都声明为readonly:
例如
#import <Foundation/Foundation.h>
@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;
@end
如果有人试着改变属性值,那么编译的时候就会报错。对象汇总给你的属性值可以读出,但是无法写入,开发者在使用对象时就能肯定其底层数据不会改变。
既然这些属性都没有设置方法(setter),那为何还要指定内存管理语义呢,如果不指定,采用默认的语义也可以:
@property (nonatomic, readonly)NSString *identifier;
@property (nonatomic, readonly) NSString *title;
@property (nonatomic, readonly) float latitude;
@property (nonatomic, readonly) float longitude;
我们还是应该在文档里指明实现所用的内存管理语义,这样的话在以后想把它变为可读写的属性时就会简单一些。
有时候肯想修改封装在对象内部的数据,但是却不想令这些数据被外人所改动。通常的做法就是在对象内部将readonly属性重新声明为readwrite。将属性重新声明为readwrite这一操作可于“分类”中完成,在公共接口中声明的属性可于此处重新声明,属性的其他特质必须保持不变就行,而readonly可以扩展为readwrite。
例如上方的例子,在其所声明类的分类中,可以写成下方的样子:
#import"EOCPointOfInterest.h"
@interface EOCPointOfInterest()
@property (nonatomic, copy, readwrite)NSString* identifier;
@property (nonatomic, copy,readwrite) NSString* title;
@property (nonatomic, assign, readwrite) float latitude; eproperty (nonatomic, assign, readwrite)float longitude;
@end
@implementation EOCPointOfInterestend
现在,只能于其属性所在类的实现代码内部设置这些属性了,其实更准确的说,在对象外部,仍然能通过**“键值编码”(KVC)**技术设置这些属性值,比如说,可以像下面这样修改属性值:
[pointOfInterest setValue:@"abc" forKey:@"identifier"];
这样子可以改动属性值,因为KVC会在类里查找“setIdentifier:”方法,并借此修改此属性。即使没有于公共接口中公布此方法,它也依然包含在类中。
要点:
- 尽量创建不可变的对象
- 若某属性进可于对象内部修改,则在“分类”中将其由属性扩展为readwrite属性
- 不要把可变的collection作为属性公开,而应提供相关方法,以此修改对象中的可变collection。
第19条:使用清晰而协调的命名方式
一开始学习的人通常会觉得OC的语言很繁琐,因为其语法结构使得代码读起来和句子一样。以下方代码为例:
NSString *text = @"The quick brown fox jumped over the lazy dog";
NSString *newText = [text stringByReplacingOccurrencesOfString:@"fox" withString:@"cat"];
这个句子虽然繁长,但是准确描述了开发者想做的事。
下面介绍一种命名方式:
驼峰命名法:
方法和变量名的首个单词的首个字母小写,然后后面的每个单词的首字母大写。类名也用驼峰命名法,另外,类名一般还有三个前缀字母。
方法命名
给方法命名时的注意事项可总结成下面几条规则:
- 如果方法的返回值是新创建的,那么方法名的首个词应是返回值的类型,除非前面还 有修饰语,例如localizedString。
- 应该把表示参数类型的名词放在参数前面。
- 如果方法要在当前对象上执行操作,那么就应该包含动词;若执行操作时还需要参数, 则应该在动词后面加 上一个或多个名词。
- 不要使用str 这种简称,应该用string 这样的全称。
- Boolean 属性应加is 前缀。如果某方法返回非属性的Boolean 值,那么应该根据其功 能,选用has 或is 当前缀。
- 将get 这个前缀留给那些借由“输出参数〞来保存返回值的方法,比如说,把返回值填充到〝C语言式数组” ( C - style array ) 里的那种方法就可以使用这个词做前缀 。
类和协议的命名
其中最重要的一点就是,命名方式应该协调一致。如果要从其他框架中继承子类,那么务必遵循其命名惯例。比方说,要从UIView类中继承自定义的子类,那么类名末尾的词必须是View。同理,若要创建自定义的委托协议,则其名称中应该包含委托发起方的名称,后面再跟上Delegate一词。
要点:
- 起名时应遵从标准的OC命名规范,这样创建出来的接口更容易为开发者所理解。
- 方法名要言简意赅,从左至右读起来要像个日常用语中的句子才好。
- 方法名里不要使用缩略后的类型名称。
- 给方法名起名时的第一要务就是确保其风格与你自己的代码或所要集成的框架相符。
第20条:为私有方法名加前缀
编写类的实现代码时,经常需要写一些只在内部使用的方法。应该为这种方法的名称加上某些前缀,这有助于调试因为据此很容易能把公共方法和私有方法区别开。
一般用p_作为前缀,p表示“private”(私有的),下划线后面的部分按照常用的驼峰命名即可,其首字母要小写。例如,包含私有方法的EOCObject类可以这样写:
- (void) p_privateMethod {/* ... */
}
与公共方法不同,私有方法不出现在接口定义中。有时可能要在“分类”里声明私有方法,然而最近修订的编译器已经不要求在使用方法前必须先行声明了。所以说,私有方法一般只在实现的时候声明。
苹果公司喜欢单用一个下划线做私有方法的前缀。所以开发者当避免这样使用前缀,因为你有可能会无意间覆写父类的同名方法。
要点:
- 给私有方法的名称加上前缀,这样可以很容易地将其同公共方法区分开。
- 不要单用一个下划线做私有方法的前缀,我也这种做法是预留给苹果公司的。
第21条:理解Objective-C错误模型
很多编程语言都有“异常”机制,通过异常机制来处理错误,当然OC中也有。
首先我们要注意的是,“自动引用计数”在默认情况下不是“异常安全的”,就是说,如果抛出异常,那么本应该在作用域末尾释放的对象现在却不会释放了,这样就会造成内存泄漏问题,如果想生成“异常安全”的代码,可以通过设置编译器的标志来实现,不过这将引入一些额外的代码,在不抛出异常时,也照样要执行这部分代码。需要打开的编译器标志叫做-fobjc-arc-exceptions。
OC语言现在所采用的办法是:只在极其罕见的情况下抛出异常,异常抛出之后,无须考虑恢复问题,而且应用程序此时也应该退出。这就是说,不用再编写复杂的“异常安全”代码了。
异常只应该用于极其严重的错误,比如说,你编写了某个抽象基类,它的正确用法是先从中继承一个子类,然后使用这个子类。这种情况下,如果有人直接使用了这个抽象类,那么就可以考虑抛出异常
- (void) mustOverrideMethod {NSString *reason = [NSString stringWithFormat: @"%@ must be oberridden", NSStringFormSelector(_cmd)];@throw [NSException exceptionWithName:NSInterNalInconsistencyException reason:reason userInfo:nil];
}
既然异常只用于处理严重错误,那么对其他错误怎么办?在出现“不那么严重的错误”时,OC语言所用的编程范式为:令方法返回nil/0,或是使用NSError,以表明其中有错误发生。
比如说,如果初始化方法无法根据传入的参数来初始化当前实例,那么就可以令其返回nil/0:
- (id)initWithValue:(id)value {if ((self = [super init])) {if (/* Value means instance can't bo created */) {self = nil;} elsef {//Initialize instance}}return self;
}
这样一来,如果if语句发现无法用传入的参数值来初始化当前实例,就会把self设置成nil,整个方法的返回值也就是nil了,调用者发现初始化方法并没有把实例创建好,于是便可确定其中发生了错误。
NSError的用法更加灵活,我们可以经由此对象,把导致错误的原因回报给调用者。NSError对象里封装了三条信息:
- Error domain(错误范围,其类型为字符串)
错误发生的范围。也就是产生错误的根源,通常用一个特有的全局变量来定义。比方说,“处理URL的子系统”(URL-handling subsystem)在从URL中解析或取得数据时如果出错了,那么就会使用NSURLErrorDomain来表示错误范围。 - Error code(错误码,其类型为整数)
独有的错误代码,用以指明在某个范围内具体发生了何种错误。某个特定范围内可能会发生一系列相关错误,这些错误情况通常采用enum来定义。例如,当HTTP请求出错时,可能会把HTTP状态码设为错误码。 - User info(用户信息,其类型为字典)
有关此错误的额外信息,其中或许包含一段“本地化的描述”(localized description),或许还含有导致该错误发生的另外一个错误,经由此种信息,可将相关错误串成一条“错误链”(chain of errors)。
在设计API时,NSError的第一种常见用法时通过委托协议来传递此错误。有错误发生时,当前对象会把错误信息经由协议中的某个方法传给其委托对象。
NSError的另一种常见的用法是:经由方法的“输出参数”返回给调用者。
就是说用一个方法来判断你传过去的error是否真的有错误,返回Boolean值,之后你就可以根据error是否有内容,或者Boolean值来决定处理的代码和不处理的代码。就像这样:
NSError error =nil;
BOOL ret =[object doSomething:&error];
if (error){
// There was an error
}
doSomething:会处理一些事,并且还会将出错的问题返回给error指针传回给调用者,并且还会返回一个Boolean值给ret。
NSError对象里的“错误范围”、“错误码”、“用户信息”等部分应该按照具体的错误情况填入适当的内容。这样的话,调用者就可以根据错误的类型分别处理各种错误了。错误范围应该定义成NSString型的全局变量,而错误码则定义成枚举类型为佳。
//EOCErrors.h
extern NSString *const EOCErrorDomain;typedef NS_ENUM(NSUInterger, EOCError) {EOCErrorUnknown = -1EOCErrorInternalInconsistency = 100EOCErrorGeneralFault = 105;EOCErrorBadInput = 500;
}
//EOCErrors.m
NSString *const EOCErrorDomain = @"EOCErrorDomain";
要点:
- 只有发生了可使整个应用程序崩溃的严重错误时,才应使用异常。
- 在错误不那么严重的情况下,可以指派“委托方法”来处理错误,也可以把错误信息放在NSError对象里,经由“输出参数”返回给调用者。
第22条:理解NSCopying协议
我们经常会使用copy函数,但是若是你自定义的类,他自己就不会实现这个函数,此时就需要你自己来实现了,要实现copy函数就的实现NSCopying协议,该协议只有一个方法:
- (id)copyWithZone:(NSZone *)zone;
copy方法由NSObject实现,该方法只是以“默认区”为参数来调用“copyWithZone:”。所以要实现copy函数,他才是关键。
想要重写copy函数,要声明该类遵从NSCopying协议,并实现其中的方法
#import <Foundation/Foundation.h>
@interface EOCPerson : NSObject <NSCopying>
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
- (id) initwithFirstName: (NSString*)firstName andLastName: (NSString*) lastName;
@end
实现协议中规定的方法:
- (id) copyWithZone: (NSZone*)zone {EOCPerson *copy = [[[self class] allocWithZone: zone] initwithFirstName: firstName andLastName: lastName];return copy;
}
mutableCopy方法,此方法来自另一个叫做NSMutableCopying的协议。该协议与NSCopying类似,也只定义了一个方法,然而方法名不同:
(id)mutableCopyWithZone:(NSZone*)zone
在编写拷贝方法时,还要决定一个问题,就是应该执行“深拷贝”(deep copy)还是“浅拷贝”(shallow copy)。深拷贝的意思就是:在拷贝对象自身时,将其底层数据也一并复制过去。Foundation框架中的所有collection类在默认情况下都执行浅拷贝,也就是说,只拷贝容器对象本身,而不复制其中数据。这样做的主要原因在于,容器内的对象未必都能拷贝,而且调用者也未必想在拷贝容器时一并拷贝其中的每个对象。
图3-2描述了深拷贝与浅拷贝的区别
一般情况下,在自定义的类中以浅拷贝的方式实“copyWithZone:”方法。但如果有必要的话,也可以增加一个执行深拷贝的方法。以NSSet为例,该类提供了下面这个初始化方法,用以执行深拷贝:
-(id) deepCopy {
EOCPerson *copy =[[[self class] alloc]initWithFirstName:_firstNameandLastName:_lastName];copy->_friends=[[NSMutableSet alloc] initWithSet:_friendscopyItems:YES];return copy;
}
-(id) initWithSet:(NSArray*)array copyItems:(BOOL) copyItems
若copyItem参数设为 YES,则该方法会向数组中的每个元素发送copy消息,用拷贝好的元素创建新的set,并将其返回给调用者。
要点:
- 若想令自己所写的对象具有拷贝功能,则需实现NSCopying协议。
- 如果自定义的对象分为可变版本和不可变版本,那么就要同时实现NSCopying与NSMutableCopying协议。
- 复制对象时需决定采用浅拷贝还是深拷贝,一般情况下应该尽量执行浅拷贝。
- 如果你所写的对象需要深拷贝,那么可考虑新增一个专门执行深拷贝的方法。
相关文章:
第3章 接口和API设计
第15条:用前缀避免命名空间冲突 OC没有其他语言那种内置的命名空间机制。因此,我们在起名时要设法避免潜在的命名冲突,否则很容易就重名了。若是发生重名冲突,那么应用程序相应的链接过程就会出错。例如: 错误原因在…...
HBase入门:实现原理
文章目录 说明HBase的实现原理HBase功能组件表和 RegionRegion 的定位 说明 本文参考自林子雨老师的《大数据技术原理与应用(第三版)》教材内容,仅供学习和交流 HBase的实现原理 HBase功能组件 HBase 的实现包括 3 个主要的功能组件:库函数ÿ…...
Redis入门到实战-基础篇+实战篇+高级篇+原理篇
Redis入门到实战-基础篇实战篇高级篇原理篇 文章目录 Redis入门到实战-基础篇实战篇高级篇原理篇一、基础篇二、实战篇三、高级篇四、原理篇 一、基础篇 1.基础篇笔记:https://blog.csdn.net/cygqtt/article/details/126974142 二、实战篇 1.实战篇笔记:…...
redis 工具类
在Spring Boot项目中,Redis是一个常用的分布式缓存解决方案。下面展示的RedisCache工具类封装了对Redis进行基本操作的方法,包括存储和获取各种类型的数据、设置过期时间以及处理集合类型的缓存。 /*** redis 工具类***/ SuppressWarnings(value { &q…...
焕新升级,不同以“网” | AnyCase客户端全新上线
升级啦~ 2024年1月23日 箱讯AnyCase官网全新改版上线! 全球贸易All in One集成平台 集物流服务、外贸服务、供应链金融服务、企业风控服务、碳中和服务于一体 添加图片注释,不超过 140 字(可选) 优化首页布局→体验升级 此次…...
导出 MySQL 数据库表结构、数据字典word设计文档
一、第一种 :利用sql语句查询 需要说明的是该方法应该适用很多工具,博主用的是navicat SELECT TABLE_NAME 表名,( i : i 1 ) AS 序号,COLUMN_NAME 列名, COLUMN_TYPE 数据类型, DATA_TYPE 字段类型, CHARACTER_MAXIMUM_LENGTH 长度, IS_NULLABLE…...
conda管理python安装包与虚拟环境的相关命令汇总
conda的简单介绍 Anaconda,是一个开源的Python发行版本,包含了conda、Python以及一大堆安装好的工具包及依赖项。 conda是Anaconda中的一个开源的、Python包和环境的管理工具,包含于Anaconda的所有版本当中。因此使用conda需要先安装Anacon…...
Vue3引用echart5 报错解决
一、TypeError: Cannot read properties of undefined (reading type) 原因:由于把echart实例绑定到了一个响应式的变量上 解决方案 【1】使用markRaw 把响应式变量定为非响应式变量 import { markRaw } from vue; export default {data() {return {chartConta…...
浅析HTTP协议
首先,前端请求后端数据,后端响应数据给前端,这是我们大家都知道的,那其中所涉及到的数据传输协议又是什么呢?这个传输规范就是我们大名鼎鼎的HTTP协议! 什么是HTTP协议? HTTP(超文本…...
etcd未授权到控制k8s集群
在安装完 K8s 后,默认会安装 etcd 组件,etcd 是一个高可用的 key-value 数据库,它为 k8s 集群提供底层数据存储,保存了整个集群的状态。大多数情形下,数据库中的内容没有加密,因此如果黑客拿下 etcd&#x…...
制作一个简单的HTML个人网站
在当今数字化的世界里,拥有一个个人网站已经成为了展示个人品牌、分享作品和信息的必备工具。虽然有各种复杂的内容管理系统(CMS)和平台可以帮助我们快速搭建个人网站,但对于初学者或者想要了解更多技术细节的人来说,从…...
头歌C语言字符数组
目录 第1关:字符逆序 任务描述 相关知识(略) 编程要求 测试说明 第2关:字符统计 任务描述 相关知识(略) 编程要求 测试说明 第3关:字符插入 任务描述 相关知识(略) 编程要求 测试说明 第4关:字符串处理 任务描述 相关知识(略)...
【mongoDB】文档 CRUD
目录 1.插入文档 批量插入: 2.查询文档 3.更新文档 4.删除文档 deleteOne() deleteMany() findOneAndDelete() 1.插入文档 可以使用 insert () 方法或者 save() 方法向集合中插入文档 语法如下: db.collection_name.insert(document) collectio…...
每日一题——LeetCode1337.矩阵中战斗力最弱的K行
方法一 个人方法 排序 题目要求就是找出每行有多少个1,根据每行1的个数进行排序,但是是把每行在数组中的位置索引进行排序,并返回前k项 所以先统计每行1的个数,并将数组转化为[index,count]就是索引加个数的数组形式,…...
docker指令存档
目录 Docker 1、概念 2、架构图 3、安装 4、Docker怎么工作的? 5、Docker常用命令 帮助命令 镜像命令 1、查看镜像 2、帮助命令 3、搜索镜像 4、拉取镜像 5、删除镜像 容器命令 1、启动 2、查看运行的容器 3、删除容器 4、启动&停止 其他命令…...
Pandas ------ 向 Excel 文件中写入含有 multi-index 和 Multi-column 表头的数据
Pandas ------ 向 Excel 文件中写入含有 multi-index 和 Multi-column 表头的数据 引言正文 引言 之前在 《pandas向已经拥有数据的Excel文件中添加新数据》 一文中我们介绍了如何通过 pandas 向 Excel 文件中写入数据。那么对于含有多表头的数据,我们该如何将它们…...
ChatGPT 和文心一言 | 两大AI助手哪个更胜一筹
欢迎来到英杰社区: https://bbs.csdn.net/topics/617804998 欢迎来到阿Q社区: https://bbs.csdn.net/topics/617897397 📕作者简介:热爱跑步的恒川,致力于C/C、Java、Python等多编程语言,热爱跑步ÿ…...
flink学习之窗口处理函数
窗口处理函数 什么是窗口处理函数 Flink 本身提供了多层 API,DataStream API 只是中间的一环,在更底层,我们可以不定义任何具体的算子(比如 map(),filter(),或者 window()),而只是…...
Python 基于pytorch从头写GPT模型;实现gpt实战
1.GPT简介 GPT(Generative Pre-trained Transformer)模型是一种基于Transformer架构的生成式预训练模型,由OpenAI开发。它采用了无监督学习的方式进行预训练,然后通过微调适应特定的任务。GPT模型的结构由多层Transformer解码器组…...
2023年NOC大赛(学而思赛道)创意编程Python初中组决赛真题
2023年NOC大赛(学而思赛道)创意编程Python初中组决赛真题 题目总数:7 总分数:100 编程题 第 1 题 问答题 二进制回文 编程实现: 输入一个正整数,判断它的二进制形式是否是回文数,如果是输出True…...
头歌C++之Switch控制语句编程实训
目录 第1关:根据输入数字判断是星期几 本关必读 本关任务 测试说明 第2关:根据输入的数值和运算符做相应运算 本关必读 本关任务 测试说明 第3关:根据输入年月计算该月份的天数 本关必读 本关任务...
CNN卷积理解
1 卷积的步骤 1 过滤器(卷积核)(Filter或Kernel): 卷积层使用一组可学习的过滤器来扫描输入数据(通常是图像)。每个过滤器都是一个小的窗口,包含一些权重,这些权重通过训…...
DataKit迁移MySQL到openGauss
前言 本文将分享DataKit迁移MySQL到openGauss的项目实战,供广大openGauss爱好者参考。 1. 下载操作系统 https://www.openeuler.org/zh/download https://support.huawei.com/enterprise/zh/doc/EDOC1100332931/1a643956 https://support.huawei.com/enterprise…...
Dockerfile里ADD * 保留原来的目录结构
1、问题 给新模块写Dockerfile,很多静态资源分散在各个目录,于是Dockerfile里我直接一句: ADD ./* /dest/镜像出来后,启动容器,进入容器种后发现:文件拷贝成功,但原来的目录结构都不在了&…...
C++ 利用容器适配器,仿函数实现栈,队列,优先级队列(堆),反向迭代器,deque的介绍与底层
C 利用容器适配器,仿函数实现栈,队列,优先级队列【堆】,反向迭代器,deque的介绍与底层 一.容器适配器的介绍二.利用容器适配器实现栈和队列1.stack2.queue 三.仿函数介绍1.什么是仿函数2.仿函数的使用3.函数指针的使用1.函数指针的用处2.利用函数指针完成回调3.利用仿函数完成回…...
C语言实战系列二:简单超市收银系统
从一个简单的超市收银系统,我们来练习一个系统如何设计,然后如何实现的思路。 在Ubuntu环境下使用C语言编写一个简单的超市收银系统。以下是一个基本的示例,涵盖了商品管理、购物车、交易处理等功能。 代码 #include <stdio.h> #inc…...
coding推送代码Jenkins自动构建部署
实现功能:我们向coding推送代码,通过webhook自动通知Jenkins,实现自动构建部署 coding 项目设置 / 开发者选项 / Service Hook 输入以下参数 发送POST请求服务 URL:htttp://xxx用户名:xxx密码:xxx Jen…...
Kettle-Docker部署+Sqlserver数据同步Mysql+Start定时任务
一. 背景介绍 1. ETL是什么 ETL(Extract-Transform-Load),即数据抽取、转换、装载的过程。它是一种思想,主要是说,从不同的数据源获取数据,并通过对数据进行处理(格式,协议等转换&a…...
《微信小程序开发从入门到实战》学习九十三
7.1 视图容器组件 7.1.3 swiper与swiper-item组件 swiper组件的显示效果如下图所示: indicator-dots、indicator-color和indicator-active-color三个属性用于设置swiper组件下方的指示点。设置指示点的颜色时,可以使用HexColor,也可以使用r…...
Java服务端使用freemarker+wkhtmltoimage生成Echart图片
目录 1.通过 freemarker 将ftl转成html 1.1 freemarker 手册: 1.2 添加freemarker maven依赖 1.3 添加 echart-test.ftl 模版文件 1.4 添加 FreemarkerTool 工具类 1.5 添加测试main方法 1.6 运行,生成echart-test-时间戳.html 文件 2. 通过wkhtmltoimage将html 转为p…...
小红书推广文案/外贸网站推广优化
Example028 原文链接:Example028 题目 线性表 [a1, a2, a3, ..., an] 中的元素递增有序且按顺序存储于计算机内。要求设计一个算法,完成用最少时间在表中查找数值为 x 的元素,若找到,则将其与后继元素位置相交换,若找…...
php框架做网站好处/环球贸易网
L1-003 个位数统计 (15 分) 给定一个 k 位整数 Ndk−110k−1⋯d1101d0 (0≤di≤9, i0,⋯,k−1, dk−1>0),请编写程序统计每种不同的个位数字出现的次数。例如:给定 N100311,则有 2 个 0,3 个 1,和 1 个…...
美国疫情最新数据/手机网站关键词seo
<head> <base target"_blank" /> </head><body> <a href"http://www.w3school.com.cn">W3School</a> </body> 为页面上所有链接规定默认目标转载于:https://www.cnblogs.com/xlj-code/p/7327645.html...
国内有名的网站设计公司/seo职位招聘
话说前头 webpack前段时间有听说一下,现在已经到了3.x的版本,自己没去接触。因为之前使用gulp来作为自己的项目构建工具。现在感觉gulp使用的趋势在减少。现在这段时间去接触了webpack,感觉很不错,它的模块化打包机制,…...
aspnet网站开发/爱站网关键词挖掘机
官方教程: https://docs.microsoft.com/zh-cn/visualstudio/ide/create-a-visual-basic-winform-in-visual-studio?viewvs-2019...
linux上安装wordpress/seo营销推广全程实例
代理设计模式: 动态代理举例: 接口: 被代理类: 测试:...