【Effective Objective-C】—— 接口与API设计

第15条:用前缀避免命名空间冲突

因为OC中没有其他语言那种内置的命名空间机制,所以我们在对文件命名时要十分的注意,若是发生重名冲突,那么应用程序相应的链接过程就会出错,导致运行文件不知道究竟该调用那个文件,因为其中出现了重复的符号。例如:
【Effective Objective-C】—— 接口与API设计_第1张图片
这给错误就是因为something.osomething_else.o中都出现了各自实现的EOCTheClass类,导致编译器无法识别,而出错了。
当然,不仅仅是类文件中可能出现这种错误,程序库中也可能出现这种错误,所以为了避免这种错误,我们唯一的办法就是自己手动的实现命名空间,即为所有的名称都加上适当的前缀,用以分明各种文件。
并且每个类中相应的方法也应该有该类相应的前缀,用以分别该类和其他类中的名称相同的方法。这样做还有个好处就是:若此符号出现在栈回溯信息中,那么我们就很容易就能判断问题出现在那个类的哪个方法了。

要点:

  • 选择与你的公司、应用程序或者二者皆有关联的名称作为类名的前缀,并在所有代码中均使用这一前缀。
  • 若是自己所开发的程序库中用到了第三方库,则应为其中的名称加上前缀。

第16条:提供“全能初始化方法”

全能初始化方法”就是一个初始化方法,但是他的类创建对象的时候无论用什么初始化方法创建,其中代码都会调用某个初始化方法,那么这个初始化方法就是“全能初始化方法”。例如:
【Effective Objective-C】—— 接口与API设计_第2张图片
上述的各种初始化方法都调用了initWithTimeIntervalSinceReferenceDate:虽然我们看不到内部情况,但是这个方法就是“全能初始化方法”。并且,只有在全能初始化方法中,才会存储内部数据,其他的方法只是在存储内部数据后再进行了一些其他操作而已。
看到这里,你可能就突然发现了问题,那么到底该怎么初始化方法呢?

1.重写初始化方法:

经过上述的说明,应该大概了解了“全能初始化方法”,我们知道只有在全能初始化方法中,才会存储内部数据,所以我们要重写初始化方法就一定离不开调用之前的“全能初始化方法”。

大概就是你首先需要在你新的初始化方法中的调用这个类的“全能初始化方法”,如果他是继承关系的话,那么就使用super来调用父类的初始化方法,调用完了之后你就可以编辑你自己的想要实现的初始化方法了,反正总而言之,你全能初始化方法的调用链一定要维持的。
注意: 如果子类的全能初始化方法与超类方法的名称不同,我们总应覆写超类的全能初始化方法,避免子类调用父类的全能初始化方法。

但是,有时候我们可能不想覆写超类的全能初始化方法,因为那样做没有道理,那么我们就可以覆写超类的全能初始化方法,让在调用这个方法的时候抛出异常,这样就可以确保用户一定使用的是你自己定义的方法了,但是我觉得这样的意义并没有多大,唯一可能用到这种方法的,就是你想让程序给初始化方法传入另一个参数,而不传入之前的的数据类型,这也是要到万不得已的时候才使用的方法。

2.一个类有多个全能初始化方法要注意的问题:

通常我们遇到的类都只有一个全能初始化方法,但是偶尔也有类具有多个全能初始化方法,例如:我们熟知的UI控件,通常我们都是使用代码来初始化的,但是我们还可以使用NIB拖动进行初始化,两种初始化解码的方法是不相同的。这个时候问题就来了,我们初始化方法到底该怎么去写?

由于这两种初始化方法的解码方式不同,而且我们也不能人为的改变其解码的方式,那么我们就只能顺其自然,他有两种我们也重写两种初始化方法,注意: 重写的这两种初始化方法一定是分别调用过之前的两种全能初始化方法的,并且表明这两种新的初始化方法分别适用于那种情况。

反正,总的来说,我们就是要维持原来类的调用链,每个子类的全能初始化方法都应该调用其超类的对应方法,并逐层向上。因为其父类有两个全能初始化方法,这两种初始化方法定义出来的数据可能是不同的,若是你在子类中调用了错误的父类初始化方法,它就会可能因为数据类型的问题使程序发生错误。

3.要点:

  • 在类中提供一个全能初始化方法,并与文档里指明。其他初始化方法均应调用此方法。
  • 若全能初始化方法与超类不同,则需覆写超类中的对应方法。
  • 如果超类的初始化方法不适用于子类,那么应该覆写这个超类方法,并在其中抛出异常。

第17条:实现description方法

1.简单讲解description方法:

我们在调试程序的的时候经常要打印并查看对象信息。此时我们就可以用到description方法,其实**description就是对这个对象本身的描述。**例如:
435345345
这里的description描述的内容就会替代这个%@。又例如:
534534
则会输出:
【Effective Objective-C】—— 接口与API设计_第3张图片
然而,如果是一个自定义的类,那么它就会输出该类的地址:
43534534
这是因为你并没有覆写其description方法,只有在自己的类里覆写description方法,这个对象的描述才会改变,否则打印信息时就会调用NSObject类所实现的默认方法。在这个description方法中,你可以输出你想要该对象输出的内容,例如:
【Effective Objective-C】—— 接口与API设计_第4张图片
它就会输出:
【Effective Objective-C】—— 接口与API设计_第5张图片
我建议就是,实现description方法就和系统的那种描述方法差不多就行了,就跟上述说的Array差不多就行了。并且,description没有什么固定的套路输出什么,就比如,你一个字符串的description和一个数组的description肯定是不一样的,你肯定得根据你要输出的对象来考虑description方法的。

2.在description方法中使用字典输出:

在自定义的description方法中,把待打印的信息放到字典里面,然后将字典对象的description方法所输出的内容包含在字符串里并返回,这样就可以实现简易的信息输出方式了,例如:【Effective Objective-C】—— 接口与API设计_第6张图片
【Effective Objective-C】—— 接口与API设计_第7张图片
这样写的好处就是,可以令代码更易维护,并且如果你向该类中新增属性的话,你就在description方法中增加就行了。

3.debugDescription:

这个也是一种描述方法,和description差不多,就是描述的位置不一样,description是在函数调用类的时候触发方法才输出的,而debugDescription是在控制台中使用命令打印该对象时才调用的。当然加断点查看时也可以看到debugDescription的描述。

如果你在description不想将一些内容输出的话,你就可以将那些数据写在debugDescription中,让程序员自己调试时可以方便的看到这些数据,而description方法就输出你想要让用户看到的信息就行了。

4.要点:

  • 实现description方法返回一个有意义的字符串,用以描述该实例。
  • 若想在调试时打印出更详尽的对象描述信息,则应实现debugDescription方法。

第18条:尽量使用不可变对象

不可变对象,我们第一时间想到的肯定是不可变数组那种不可变对象,但是这里的不可变不是这样的,它指的是这个类里边的属性是不能直接被修改的,要实现这种功能,我们就需要用到我们的readonly(只读)修饰符。默认情况下,属性是readwrite(即可读又可写)的,这样修饰出来的类都是“可变的”。就像这样:【Effective Objective-C】—— 接口与API设计_第8张图片
有时可能想修改封装在对象内部的数据,但是却不想令这些数据为外人所改动。这种情况下,通常的做法是在对象内部将readonly属性重新声明为readwrite。当然,如果该属性是nonatomic的,那么这样做可能会产生“竞争条件”,即在对象内部写入某属性时,对象外的观察者也许正读取该属性。若想避免此问题,我们可以在必要时通过“派发队列”等手段,将(包括对象内部的)所有数据存取操作都设为同步操作。就像这样:
234234
现在,这个属性就只能用在实现代码内部设置这些属性了,但其实,在对象外部还可以通过“键值编码”技术来设置这些属性,就像“setValue:forKey:”方法。“点语法”也可以,因为点语法就是调用set方法的。这样做虽说可以改动,但是却违背了本心,还会导致数据不同而出现问题,所以不建议更改。

我们还可以通过readonly来设置一个特殊的属性,就是该属性在外部看是不可变、不可设置的,但是内部的实现其实又定义了一个可变的该数据类型的变量,当用户访问这个属性的时候,我们覆盖在.h处的该属性的get方法,让其返回在实现部分定义的变量的拷贝结果,这样在外部看来这个变量就好像是不可变的似的,我们可以通过相应的方法实现对该变量的增加删除。
当然我们还可以直接设置为可变的类型,但是这样的话外部就可以直接修改了,不建议这样,同时,通过这个我们可以得出:不要在返回的对象上查询类型以确定其是否可变。因为实现部分和定义部分可能返回的是不同的类型。

要点:

  • 尽量创建不可变的对象。
  • 若某属性仅可于对象内部修改,则在“class-continuation分类”中将其由readonly属性扩展为readwrite属性。
  • 不要把可变的collection作为属性公开,而应提供相关方法,以此修改对象中的可变collection。

第19条:使用清晰而协调的命名方式

就是说,命名时要简洁明了,让用户直接知道该方法是怎么使用的,这样的好处就是,代码读起来像日常语言里的句子。我们通常使用“驼峰命名法”,就是以小写字母开头,其后每个单词首字母大写,不论是类还是属性都可以这样命名。

1.给方法命名时的规则:

【Effective Objective-C】—— 接口与API设计_第9张图片

2.类与协议的命名:

应该为类与协议的名称加上前缀,以避免命名空间冲突,而且应该像给方法起名时那样把词句组织好,使其从左至右读起来较为通顺。基本命名规则就是:命名方式应该一致,如果要从其他的类中继承子类,那么就要遵守其原本的命名惯例。 例如:UIView它的子类就应该是***View,表明其来历。

3.要点:

  • 起名时应遵从标准的OC命名规范,这样创建出来的接口更容易为开发者所理解。
  • 方法名要言简意赅,从左至右读起来要像个日常用语中的句子才好。
  • 方法名里不要使用缩略后的类型名称。
  • 给方法名起名时的第一要务就是确保其风格与你自己的代码或所要集成的框架相符。

第20条:为私有方法名加前缀

通常我们在写方法时,并没有对其进行私有共有分类,导致调试时可能很麻烦,现在为私有方法加上前缀,这样便于修改方法或方法签名。具体加什么来代表私有方法因人而异,自己怎么舒服怎么来,唯一注意的是:一定不要只使用_作为前缀,用p_都比那个好,因为苹果公司使用的就是_作为私有方法的前缀的,你自己定义的私有方法名有可能就会和人家自带的冲突。

要点:

  • 给私有方法的名称加上前缀,这样可以很容易地将其同公共方法区分开。
  • 不要单用一个下划线做私有方法的前缀,因为这种做法是预留给苹果公司用的。

第21条:理解Objective-C错误模型

很多编程语言都有“异常”机制,通过异常机制来处理错误,当然OC中也有。
首先我们要注意的是,“自动引用计数”在默认情况下不是“异常安全的”,就是说,如果抛出异常,那么本应该在作用域末尾释放的对象现在却不会释放了,这样就会造成内存泄漏问题,如果想生成“异常安全”的代码,可以通过设置编译器的标志来实现,不过这将引入一些额外的代码,在不抛出异常时,也照样要执行这部分代码。需要打开的编译器标志叫做-fobjc-arc-exceptions

OC语言现在所采用的办法是:只在极其罕见的情况下抛出异常,异常抛出之后,无须考虑恢复问题,而且应用程序此时也应该退出。这就是说,不用再编写复杂的“异常安全”代码了。

既然异常只用于处理严重错误,那么对其他错误怎么办?在出现“不那么严重的错误”时,OC语言所用的编程范式为:令方法返回nil/0,或是使用NSError,以表明其中有错误发生。像我们之前都用过的网络请求,其中就使用到了NSError来表示错误。

1.NSError对象封装的三条信息:

【Effective Objective-C】—— 接口与API设计_第10张图片

2.NSError的常见用法:

NSError的另一种常见的用法是:经由方法的“输出参数”返回给调用者。就是说用一个方法来判断你传过去的error是否真的有错误,返回Boolean值,之后你就可以根据error是否有内容,或者Boolean值来决定处理的代码和不处理的代码。就像这样:【Effective Objective-C】—— 接口与API设计_第11张图片
doSomething:会处理一些事,并且还会将出错的问题返回给error指针传回给调用者,并且还会返回一个Boolean值给ret
【Effective Objective-C】—— 接口与API设计_第12张图片
若你不想知道其出错的问题,那么就可以直接这样写。
实际上,在使用ARC时,编译器会把方法签名中的NSError**转换成NSError*__autoreleasing*,也就是说,指针所指的对象会在方法执行完毕后自动释放。因为doSomething:不能保证其调用者可以把此方法中创建的NSError释放掉。

NSError对象里的“错误范围”、“错误码”、“用户信息”等部分应该按照具体的错误情况填入适当的内容。这样的话,调用者就可以根据错误的类型分别处理各种错误了。错误范围应该定义成NSString型的全局变量,而错误码则定义成枚举类型为佳。
【Effective Objective-C】—— 接口与API设计_第13张图片

3.要点:

  • 只有发生了可使整个应用程序崩溃的严重错误时,才应使用异常。
  • 在错误不那么严重的情况下,可以指派“委托方法”来处理错误,也可以把错误信息放在NSError对象里,经由“输出参数”返回给调用者。

第22条:理解NSCopying协议

我们经常会使用copy函数,但是若是你自定义的类,他自己就不会实现这个函数,此时就需要你自己来实现了,要实现copy函数就的实现NSCopying协议,该协议只有一个方法:

- (id)copyWithZone:(NSZone *)zone;

1.为何会出现NSZone呢?

因为以前开发程序时,会据此把内存分成不同的“”,而对象会创建在某个区里。现在不用了,每个程序只有一个区:“默认区”。所以说,尽管必须实现这个方法,到那时你不必担心其中的zone参数。

copy方法由NSObject实现,该方法只是以“默认区”为参数来调用“copyWithZone:”。所以要实现copy函数,他才是关键。

2.重写copy函数:

想要重写copy函数,要声明该类遵从NSCopying协议,并实现其中的方法即可,例如:【Effective Objective-C】—— 接口与API设计_第14张图片

3.copy和mutableCopy:

其实拷贝就是创建一个新的变量,并将其初始化内容为你要拷贝变量的内容,最终再返回这个新的变量。这里初始化最好使用全能初始化方法。

并且,无论当前实例是否可变,若需获取其可变版本的拷贝,均应调用mutableCopy方法,同理,若需要不可变的拷贝,则总应通过copy方法来获取。

我们通常把拷贝方法称为copy而非immutableCopy的原因在于,NSCopying不仅设计给那些具有可变版本和不可变版本的类来使用,而且还要供其他一些类来使用,而那些类没有“可变”与“不可变”之分,所以说,把拷贝方法叫成immutableCopy不合适。

就是说若是你有一个自定义的类,其中有可变和不可变的类型,那我们肯定就是可变的用可变拷贝不可变的就用不可变的拷贝,那这就不能叫做纯正的immutableCopy了。

4.深拷贝和浅拷贝:

深拷贝:在拷贝对象自身时,将其底层数据也一并复制过去。
浅拷贝:在拷贝对象自身时,只拷贝容器对象本身,而不复制其中的数据。
【Effective Objective-C】—— 接口与API设计_第15张图片

原对象类型 拷贝方式 目标对象类型 拷贝类型(深/浅)
mutable对象 copy 不可变 深拷贝
mutable对象 mutableCopy 可变 深拷贝
immutable对象 copy 不可变 浅拷贝
immutable对象 mutableCopy 可变 深拷贝

5.要点:

  • 若想令自己所写的对象具有拷贝功能,则需实现NSCopying协议。
  • 如果自定义的对象分为可变版本和不可变版本,那么就要同时实现NSCopying与NSMutableCopying协议。
  • 复制对象时需决定采用浅拷贝还是深拷贝,一般情况下应该尽量执行浅拷贝。
  • 如果你所写的对象需要深拷贝,那么可考虑新增一个专门执行深拷贝的方法。

你可能感兴趣的:(Effective,Objective-C,objective-c,开发语言,macos)