介绍
代码是写给程序员看的,所以代码命名需要一个约定俗成的规范,否则接口就会有歧义,别人则会因此被迷惑从而增加阅读、沟通成本。本文就是为了教你写出更通俗易懂的接口。
引用:Introduction to Coding Guidelines for Cocoa
代码基础命名规范
这一章主要展示OOP(面向对象编程)容易被人忽视的一些关于类、方法、函数、常量以及编程接口的其他元素的命名。
基本原则
-
清晰明了
- 不能因为贪图简洁而影响了接口的明确性
-insertObject:atIndex: // good -insert:at: // bad
- 尽可能不要用单词缩写,除非这个缩写是众所周知的,例如:max、min、app、func、temp等等。
-setBackgroundColor: // good -setBgColor: //bad
- 不能引起歧义
-sendPort // 不知道是发送到端口还是返回这个端口 -displayName // 不知道是展示一个名字还是返回一个需要展示的名字
-
一致性
- 尽量使用cocoa框架中出现过的名称,当你定义了一个具有多态性的类时接口保持一致性尤为重要。
- (NSInteger)tag // NSView,NSCell,NSControl
-
不引用自己
- 命名不应该自己引用自己
NSString // good NSStringObject // bad
- 可按位运算的常量(
NS_OPTIONS
)与通知是例外
UIViewAutoresizingFlexibleWidth // good UIKeyboardWillShowNotification // good
前缀
前缀的重要性不言而喻,尤其是当你不得不使用其他人些的一些框架的时候,包括苹果的框架和第三方框架。前缀可以帮助我们避免冲突,例如类重复定义,分类方法命名重复等等...
- 前缀有个规定的格式:由2-3个大写字母组成,注意万万不能使用下划线
- 命名类、协议、函数、常量和
typedef
结构体的时候可以使用前缀,但是命名方法不需要使用到前缀,因为方法只存在于类的命名空间中,不同的类有重名的方法实属正常。顺便说一下,结构体中的字段也不必要使用前缀
书写约定
- 不要使用标点符号、下划线、破折号等等,应采用驼峰标识来命名。如
setBackgroundColor:
- 方法名:首字母小写,不用前缀。如
fileExistsAtPath:isDirectory:
- 类名:前缀+首字母大写。如
UITableView
- 方法名:首字母小写,不用前缀。如
- 避免以下划线作为开头来命名方法以表示这方法为私有方法,不过允许以下划线开头来命名实例变量以示其为私有变量。苹果保留这种使用约定,也就是说你要是写了一个以下划线开头的方法,很可能在未来某一天会把苹果内部的方法给重载掉而引起灾难性的后果(是的,苹果就是这么说的)。而私有方法的命名规范在下面会讲到
类和协议命名规范
类名必须能从名称中一目了然这个类是干什么的,并且应该带有前缀。Foundation
框架中的类处处可见,就不举例子了。
协议则应根据这一组的行为来命名。
大部分协议定义了一系列与类无关的方法,这种协议需要从命名上就与类区分开,最普遍的就是使用动名词的方法("…ing")
NSLocking // good
NSLock // bad,听起来就像一个类
而有一些协议定义了一些列互不关联的方法,这种协议往往作为与协议主要表达式的类关系紧密。这种情况的话协议名就跟类名保持一致。最经典的例子就是NSObject
与
。
头文件
头文件名字代表着这个文件里面包含着什么东西。
- 定义一个孤立的类或者协议,除非这些类或者协议就很强的关联性,否则请把他们放在不同的头文件里
NSLocale.h // NSLocale这个类的声明,没别的了
- 定义一系列强关联性的类或者协议,请将他们的定义放在主类、协议、分类的头文件中
NSLock.h // NSLocking协议,以及NSLock、NSConditionLock、NSRecursiveLock这几个类的声明
- 包含框架中向外暴露的所有头文件,命名得跟框架名一样。
Foundation.h // Foundation.framework
- 给另一个框架里的类加API。例如给NSString加一个分类,请在原类名后面拼接上"Additions"。如
NSBundleAdditions.h
- 相关的一些方法和数据类型。如果你有一组相关的函数、常量、结构体或其他数据类型等,将他们放在同一个头文件中然后给这个头文件取一个恰当的名字
方法命名规范
这一节主要是讲如何正确命名我们平时打码中随处可见的元素 - Method。
通用规则
以下有几条在给方法命名的时候必须牢记的准则:
-
首字母小写,然后接着的每个单词的首字母大写。不要给方法名加前缀。
- 有两个例外,第一是如果你要描述的这个单词是众所周知的单词缩写,例如TIFF of PDF这种,则可以全大写。第二是当你需要用前缀去给私有方法分组归类方便区分的时候可以使用前缀
如果方法代表的是一个动作,那么请使用动词作为方法名的开头
- (void)invokeWithTarget:(id)target; - (void)selectTabViewItem:(NSTabViewItem *)tabViewItem; // PS: 不要用 'do' 或者 'does' 表示动作, 并且永远不要在动词前加形容词(adj.)或者副词(adv.)
如果该方法返回一个属性,那么直接用这个属性来明明即可。另外除非该方法可返回多个值,否则没必要在方法名前面加'get'
- (CGSize)cellSize; // good - (CGSize)calculateCellSize; // bad - (CGSize)getCellSize; // bad
在所有方法参数前用关键字表示
- (void)sendAction:(SEL)aSelector toObject:(id)anObject forAllCells:(BOOL)flag; // good - (void)sendAction:(SEL)aSelector :(id)anObject :(BOOL)flag; // bad
在参数前用一个单词来简单形容这个参数是干嘛用的
- (id)viewWithTag:(NSInteger)aTag; // good - (id)taggedView:(int)aTag; // bad
当你创建一个比你所继承的方法更为具体的一个方法时,在方法末尾新增一个关键词即可,这种情况在UIKit很常见
- (id)initWithFrame:(CGRect)frame; - (id)initWithFrame:(CGRect)frame mode:(int)mode; - (id)initWithFrame:(CGRect)frame mode:(int)mode ...
不要使用'and'来连接两个两个方法参数
- (int)runModalForDirectory:(NSString *)path file:(NSString *) name types:(NSArray *)fileTypes; // good - (int)runModalForDirectory:(NSString *)path andFile:(NSString *)name andTypes:(NSArray *)fileTypes; // bad // PS: 当你的方法有多个参数时,用 'and' 就显得非常非常啰嗦
如果方法描述的是两个独立的操作,这时候可以用'and'来连接
- (BOOL)openFile:(NSString *)fullPath withApplication:(NSString *)appName andDeactivate:(BOOL)flag; // NSWorkspace // PS: 这种情况一般比较少见
存取方法(Accessor Methods)
简单地说,存取方法即set方法和get方法
-
属性为名词时
- (Type)noun; - (void)setNoun:(Type)noun; // example - (NSString *)title; - (void)setTitle:(NSString *)title;
-
属性为形容词时
- (BOOL)isAdjective; - (void)setAdjective:(BOOL)flag; // example - (BOOL)isEditing; - (void)setEditing:(NSString *)editing;
-
属性为动词时
- (BOOL)verbObject; - (void)setVerbObject:(BOOL)flag; // example, 动词为一般现在时 - (BOOL)showsAlpha; - (void)setShowsAlpha:(BOOL)showsAlpha;
-
不要把动词变成形容词
- (BOOL)acceptsGlyphInfo; // good - (BOOL)glyphInfoAccepted; // bad
-
可使用情态动词(如 can、should、will 等等)来阐明意思,但永远不要用do or does
- (void)setCanHide:(BOOL)flag; // good - (void)setShouldCloseDocument:(BOOL)flag; // good - (void)setDoesAcceptGlyphInfo:(BOOL)flag; // bad
-
当方法可能返回多个参数时,使用get为方法前缀
- (void)getLineDash:(float *)pattern count:(int *)count phase:(float *)phase; // PS: 这种情况下方法实现中,参数应该可以接收 NULL,因为调用方可能并不在意某些参数的返回值是多少
代理方法(Delegate Methods)
-
以发送消息的某类的对象作为方法名的开头
- (BOOL)tableView:(NSTableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath; - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)options; // PS: 类名省略前缀,并以小写字母开头。如UIApplication -> application
-
除非方法只有一个参数(即发送者),否则类名会紧跟在冒号后面(参数是代理对象的引用)
- (void)applicationDidEnterBackground:(UIApplication *)application;
-
有一种特殊情况,如果代理方法是用来统一处理通知的话,该方法的唯一参数就是通知本身
- (void)windowDidChangeScreen:(NSNotification *)notification;
-
使用
will
或者did
来告知代理某些行为将要或者已经发生- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView; - (void)scrollViewDidScroll:(UIScrollView *)scrollView;
-
你可以使用
should
来询问代理自己是否需要做某些事情- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView;
集合类型方法(Collection Methods)
对于管理一个集合的对象,这里直接说容器了哈。约定好方法的形式如下:
- (void)addElement:(ElementType)element;
- (void)removeElement:(ElementType)element;
- (NSArray *)elements;
以下是一些细化的准则:
如果集合是无序的,返回
NSSet
比NSArray
更好-
对于插入/删除元素这种行为,应该提供类似上述方法的API,如:
- (void)insertElement:(ElementType)element atInex:(NSUInteger)index; - (void)removeElementAtIndex:(NSUInteger)index;
方法参数(Method Arguments)
直接上注意点吧:
- 参数名小写字母开头,遵循驼峰命名法
- 尽量用参数的类型去告诉别人这个参数是不是指针,而不是用参数名
- 尽量不要用1~2个字母作为参数名
- 尽量不要缩写到只剩几个字母
私有方法(Private Methods)
大部分情况下,私有方法命名跟公有方法是一样的。有个很简便的方法去区分私有方法就是给他加一个前缀。但是有个弊端就是,你不知道会不会无意中重写了苹果框架中的私有方法。。。
但是苹果框架中的私有方法大部分都是以下划线作为前缀的。所以我们就尽量不要用下划线作为前缀明明我们自己的方法了。另外如果我们子类化一个使用特别广泛的类例如UIView
,那么我们给自己的类加私有方法的时候可以用上独一无二的前缀,例如公司名+项目名XX_addObject:
。
函数命名规范
函数命名我们需要遵循以下几个规范:
-
函数命名有点像方法命名,但是有一点不同:
- 他们以你用于类名或常量的相同前缀开头
- 前缀后面紧跟着的第一个字母总是大写
-
大部分函数以说明该函数有什么用的动词作为函数名开头
void NSDeallocateObject(id object);
查询属性的函数还有一组命名规则:
-
如果函数返回的是他的第一个参数的属性,可省略动词
float NSHeight(NSRect aRect); // CGFloat CGRectGetWidth(CGRect rect); // UIKit下不符合这个规范
-
如果返回的是引用,使用
Get
const char *NSGetSizeAndAlignment(const char *typePtr, unsigned int *sizep, unsigned int *alignp);
-
如果返回值是布尔值的话,函数名应以变形后的动词开头
BOOL NSDecimalIsNotANumber(const NSDecimal *dcm);
以上这三个规则说实话我感觉有点过时了。。。大家看看就好
属性以及数据类型命名规范
这一节讲的是属性、实例变量、常量、通知以及异常的命名规范。
属性和实例变量
命名属性跟前面讲过的存取方法很类似,忘记了的话可以回去看看哦,举个例子应该就能马上回想起来了:
@property (nonatomic, strong) NSString *title;
@property (nonatomic, assign) BOOL showsAlpha;
如果属性表达的是形容词的话,省略is
然后在getter方法里体现出来:
@property (nonatomic, assign, getter=isEditable) BOOL editable;
绝大部分情况下,你声明一个属性相当于同时合成(synthesize)了相应的实例变量, 例如:
@implementation MyClass {
BOOL _showsAlpha;
}
你也可以手动加上@synthesize
去决定相应的实例变量名,如:
@implementation MyClass
@synthesize showsAlpha = myShowsAlpha // 此时生成的实例变量名就为myShowsAlpha而不再是_showsAlpha了,一般不建议这样用
有几点需要注意的是:
- 避免明确声明实例变量。开发者只需关心属性,接口即可,具体是以什么实例变量去存储的开发者不需要理会,正常情况下只需要让系统给我们自动生成的实例变量即可
- 如果确实需要声明实例变量,请明确使用
@private
或@protected
来声明它。尤其是如果你确信你的类会被其他子类继承,那么子类会需要访问到该实例变量的话,使用@protected
即可 - 如果该实例变量是该类实例的可访问属性,请确保你已经为他写了存取方法,如果可以的话还是请声明为属性
@property
吧
常量
常量怎么命名往往取决于这个常量是如何被创建的
枚举常量
一般情况下,我们会使用枚举给一系列整形常量分组。具体命名规范和命名函数差不多。大家可以回头去看看,这里给一个例子
typedef enum _NSMatrixMode { // 这个_NSMatrixMode可省略
NSRadioModeMatrix = 0,
NSHighlightModeMatrix = 1,
NSListModeMatrix = 2,
NSTrackModeMatrix = 3
} NSMatrixMode; // 甚至连这个NSMatrixMode都可以省略
带const修饰符的变量
可用const
修饰符去创建一个跟其他常量没有任何联系的不可变常量,例如:
const float NSLightGray;
其他类型的常量
一般情况下,不要用
#define
去定义常量,integer
常量就用枚举去解决,float
常量就用const
去解决-
全大写字母的预编译指令
#ifdef DEBUG // code will be processed #endif
编译器定义的宏前后都有双下划线,例如
__MACH__
-
字符串常量的话常常被用作Notification Name或者Dictionary Key,需要注意的是在编译器该字符串的值就需要被确定下来。例如
// .h UIKIT_EXTERN NSString *kViewDidShowNotification; // .m NSString *kViewDidShowNotification = @"kViewDidShowNotification";
通知和异常
通知和异常都有类似的命名规范。请往下看:
通知
一般来说,如果一个类有代理(delegate)的话,他的通知都能找到其对应的代理方法。通知的命名一般包括以下几个部分:
[Name of associated class] + [Did | Will] + [UniquePartOfName] + Notification
例如:UIApplicationDidBecomeActiveNotification
异常
异常一般如此命名:
[Prefix] + [UniquePartOfName] + Exception
例如:NSDraggingException
常见的缩写及首字母缩写(Acceptable Abbreviations and Acronyms)
一般情况下不建议在写代码的时候使用英文单词缩写,除非是一些人尽皆知的或者前人已经使用过的缩写。例如,alloc
,init
,app
等等。另外还有一些名词如果不用缩写的话可能人们看了也不知道为何物,例如,XML
,JPG
,PNG
等等。这种情况下就建议直接用缩写了。
对SDK开发者的提示和技巧
技巧都是通用的,不仅仅针对SDK开发者,可以运用在日常开发中。
初始化
以下开始介绍SDK的初始化的建议。
类初始化(Class Initialization)
+initialize
方法提供一个地方可以给你以懒加载的形式(即该类运行时首次被使用到的时候)执行某些一次性代码。一般来说会在这里设置版本号。
Runtime
会自动帮我们调用继承链上每一个类的+initialize
方法,即便你没有实现他,并且当子类没有实现该方法时会默认调用父类方法,所以该方法可能调用不止一次。如果想要保证写在这里的代码整个应用生命周期只会调用一起的话,请使用dispatch_once()
。
+ (void)initialize {
static dispatch_once_t onceToken = 0;
dispatch_once(&onceToken, ^{
// the initializing code
}
}
Note:
由于Runtime会帮我们调用每一个类的initialize方法,所以如果某子类没有实现initialize方法的话,就会发生在子类的上下文里调用父类的initialize方法的。所以如果想要在对应的上下文里实现相应的initialize方法,可以在方法里面判断一下类型,这种方法比dispatch_once更好。
if (self == [NSFoo class]) { // the initializing code }
永远不要自己去调用+initialize
方法,如果你想要触发这个方法的话,随意调用一个没有副作用的方法即可,例如
[NSImage self];
指定的初始化器(Designated Initializers)
Designated Initializers
简单地说就是一个类的init
(假设init
方法就是该类的Designated Initializers
)方法会默认调用其父类的init
方法,然后该类的其他初始化方法内部都会调用该类的init
方法。例如UIView
的-initWithFrame:
方法。但是如果你的类是类似NSSring
和其他面向类族集Class clusters
的抽象类,并且不想覆盖init
方法,那么子类应该实现自己的初始化方法。
Designated Initializers
应该被清楚地标识出来,因为如果其他类想要继承自你这个类的话,他只需要重写这个 Designated Initializers
即可让其他初始化方法都按照设计完成初始化工作。
当你在实现SDK中的类时,通常需要实现其归档和解档方法,-initWithCoder:
和-encodeWithCoder:
。需要注意的是,不要在解档期间去做任何事情。如果你的类实现了
协议,那么最好在你的Designated Initializers
中调用-initWithCoder
方法(-initWithCoder
本身也是Designated Initializers
)。
初始化期间的错误检测
一个好的设计的初始化方法应该完成以下3个步骤确保正确地检测和传播错误:
- 调用
super
的指定的初始化器后给self
重新赋值 - 检查
self
是否为nil
,这一步主要检测调用super
的初始化方法是否成功 - 如果在初始化过程中发生了错误,释放自身,并返回
nil
例子如下:
- (id)init {
self = [super init]; // Call a designated initializer here.
if (self != nil) {
// Initialize object ...
if (someError) {
[self release];
self = nil;
}
}
return self;
}
版本控制和兼容性
通常来说,当你为你的SDK加了新方法、类时,没必要为每个新功能组指定一个新的版本号。开发正可以通过OC的runtime方法respondsToSelector:
去验证在指定系统下该方法是否可用。
但是你可以采用多种技术手段去确保SDK的每个新版本都记性正确标号,并且提高对旧版本的兼容性。
SDK版本号
如果没办法简单地通过runtime tests的方式去检测新功能或bug修复的存在,那么则应该为开发人员提供方式来检查更改。一种方法就是存储SDK的版本号,然后开发人员可以根据这个版本号去查相应资料:
- 为每个版本号编写相应的文档,注明release note
- 把版本号存储在某个地方并提供可访问的方法。例如存在SDK的
info.plist
文件里
键控存档(Keyed Archiving)
如果SDK中的对象需要被写入nib
文件中,那么他们必须自己能够归档。除此以外,你还需要利用归档机制去归档所有文档数据。
有以下几点需要注意:
- 如果忘了将某个key归档,那么在取值的时候会得到
nil
、NULL
、NO
、0
或者0.0
。取决于API的返回值类型。因此,测试这些返回值能够帮助你减少错误,此外,还可以找出问题所在。 - 归档和解档的方法都应该确保能够向后兼容。例如,新版本的归档方法里会写入新的键值对,同时也会写入旧的key-value,老版本依然能够识别出这些老的键值对。因此,解档方法里面需要以合理的方式去处理这些缺失值,以及将来的版本里保持一定的灵活性。
- 一个推荐的归档key命名规范是使用SDK的API前缀+实例对象的名称。确保任何父类和子类的命名都不会冲突即可。
- 如果你有一个工具方法输出的是基本数据类型(换言之,不是对象),需要确保使用唯一键。例如用于归档矩形
archiveRect
应该使用一个键参数并且使用一个给定的键,或者如果他写出的是多个值(例如,四个浮点数),则应将自己的唯一位拼接到提供的键里面(PS:这个我直译过来读不太懂...) - 由于编译器和字节序的依赖性,按原样存档位域可能很危险。 出于性能方面的考虑,仅当需要多次写入许多位时才应对它们进行归档。
异常和错误
大多数Cocoa的框架都不强制开发者去捕捉并处理异常。因为异常不会在正常使用过程中抛出,并且通常不用于传达预期的运行时或用户错误。这些错误的例子包括:
- 文件未找到
- 没有这样的用户
- 尝试打开一个错误类型的文档
- 将字符串转为指定编码时出错
然而,当出现变成或逻辑错误时Cocoa会抛出异常,例如:
- 数组索引超出范围
- 尝试改变一个不可变的对象
- 错误参数类型
理想的情况下,开发者在应用上线之前的测试环节中就能捕捉到这些类型的异常并且解决掉,因此,应用无需在运行时处理异常。如果一个异常抛出并且应用没有去捕捉他时,最顶层的默认handler将会捕捉并上报这些异常然后继续执行程序。开发者可以选择自己去捕捉并处理这些异常,提供选项让用户保存数据并退出应用程序。
错误是Cocoa的框架鱼其他软件库的一方面。Cocoa的方法一般不返回错误代码。如果存在一个合理的或者类似错误的原因,则该方法依赖对布尔值或者对象是否为空的简单判断;并且在文档中应能够查到返回NO或者nil的原因。你不应使用错误代码来指示要在运行时处理编程错误,而应引发异常,或者在某些情况下,只需记录错误而不引发异常即可。
例如,NSDictionary
的objectForKey:
方法要么返回找到对象,要么找不到时返回nil。NSArray
的objectAtIndex:
方法则永不能返回nil,因为NSArray
对象不能存储nil并且根据定义,任何越界访问都是编程错误,应导致异常。许多init
方法当他们根据提供的参数无法初始化时也会返回nil。
在少数情况下,一个方法确实需要多个不同的错误代码,这种情况则应在按引用参数中指定他们,该参数返回错误代码、本地化错误字符串或一些其他的描述错误的信息。
框架数据
处理框架数据的方式会影响性能、跨平台稳定性和其他一些目的。本节讨论涉及框架数据的技术。
常量数据(Constant Data)
从性能方面考虑,最好将尽可能多的框架里面的数据标记为常量,这样能减少Mach-O二进制文件的__DATA
段的大小。不是const
修饰的全局和静态数据最终都会出现在__DATA
段的__DATA
节中。这种数据会占用每一个使用到该框架的应用中的部分内存。尽管额外的500字节(例如)看起来无足轻重,但可能会导致所需的页数增加-每个应用额外增加4KB。
你应该将任何常量数据用const
来修饰。如果block中没有char *
指针,那么就会导致数据被放在__TEXT
段(这会使这段代码真正地保持不变);否则这段代码就会被放在__DATA
段中,但不会被写入(除非未执行预绑定或者因为必须在加载时滑动二进制文件而违反了预绑定)
你应该初始化静态变量去确保他们被合并到__DATA
段的__DATA
节中,而不是__bss
节中。如果没有明显的值可用于初始化,请使用0、NULL、0.0或者适当的值。
位域(Bitfields)
如果代码假定该值是布尔值的话,使用带符号的值作为位域(尤其是一位位域)则往往会导致奇奇怪怪的行为。因为只能在这样的位域中存储的值是0和-1(取决于编译器的实现),所以讲该位域与1进行比较是错误的。例如,如果你在代码里遇到这种情况:
BOOL isAttachment:1;
int startTracking:1;
你应该把类型改为unsigned int
。
位域的另一个问题是归档。通常不应该以他的表现形式去写入磁盘或归档,因为在另一种架构或者编译器上面再次读取的时候格式可能会不一样。
内存分配(Memory Allocation)
在写框架的时候,尽量避免完全分配内存。如果处于某种原因需要临时缓冲区,通常使用堆栈要比分配缓冲区更好。但是堆栈的大小有限制(通常总共大小为512KB),因此是否使用堆栈取决于功能和所需缓冲区的大小。通常,如果缓冲区的大小小于等于1000字节,则可以使用堆栈。
一种策略是一开始使用堆栈,如果大小增长到超出了堆栈缓冲区的大小,则切换成分配到内存的缓冲区。以下代码就是做的这件事:
#define STACKBUFSIZE (1000 / sizeof(YourElementType))
YourElementType stackBuffer[STACKBUFSIZE];
YourElementType *buf = stackBuffer;
int capacity = STACKBUFSIZE; // In terms of YourElementType
int numElements = 0; // In terms of YourElementType
while (1) {
if (numElements > capacity) { // Need more room
int newCapacity = capacity * 2; // Or whatever your growth algorithm is
if (buf == stackBuffer) { // Previously using stack; switch to allocated memory
buf = malloc(newCapacity * sizeof(YourElementType));
memmove(buf, stackBuffer, capacity * sizeof(YourElementType));
} else { // Was already using malloc; simply realloc
buf = realloc(buf, newCapacity * sizeof(YourElementType));
}
capacity = newCapacity;
}
// ... use buf; increment numElements ...
}
// ...
if (buf != stackBuffer) free(buf);
对象比较(Object Comparison)
值得注意的是,通用的对象比较方法-isEqual:
和与对象类型关联的比较方法如-isEqualToString:
有着很大的区别。-isEqual:
方法允许你讲任意对象作为参数传递,如果对象不是同一类,则返回NO。而-isEqualToString:
和-isEqualToArray:
之类的方法通常假定参数为指定的类型(即接收方的类型)。因此,他们内部不会进行类型检查,因此他们执行速度更快、但并不那么安全。对于外部资源,例如应用程序的info.plist
或首选项Preferences
,首选使用-isEqual:
方法,因为他更安全;当已知类型时,请使用-isEqualToString:
。
关于-isEqual:
方法的另一点是他与hash方法的关系。对于Cocoa集合中的对象(例如NSDictionary
或者NSSet
),一个基本不变式是如果[A isEqual:B] == YES
,那么[A hash] == [B hash]
。因此,如果在类中重写了-isEqual:
方法,则还需要重写-hash
方法以保证该不变式。默认情况下,-isEqual:
方法查找每个对象地址的指针相等,并且hash根据每个对象的地址返回一个哈希值,因此该不变量成立。