【iOS】Effective Objective-C第三章
【iOS】Effective Objective-C第三章前言用前缀避免命名空间冲突提供“全能初始化方法”实现description方法尽量使用不可变对象使用清晰而协调的命名方式为私有方法名加前缀理解Objective-C错误模型理解NSCopying协议NSCopying协议NSMutableCopying协议深浅拷贝前言我们在iOS开发中构建应用程序时经常会在开发自己的应用程序时将其中的部分代码用于后续项目或供他人使用。这需要用到OC中常见的编程范式同时还需了解各种可能碰到的陷阱。用前缀避免命名空间冲突OC没有内置的命名空间机制因此我们在起名时要设法避免潜在的命名冲突否则很容易重名。这样会出错发生命名冲突应用程序链接过程出错无法链接。在运行期载入了含有重名类的程序库动态加载器遭遇重名符号错误整个程序崩溃。避免此问题唯一的办法就是变相实现命名空间为所有名称都加上适当前缀。所选前缀可以是与公司、应用程序或二者皆有关联之名。Apple宣称其保留使用所有“两字母前缀”的权利所以我们自己选用的前缀应该是三个字母的。容易忽视的类的实现文件中所用的纯C函数及全局变量也应加上前缀。这样做有两个好处避免在链接时发生重复符号错误。若此符号在栈回溯信息中则很容易判明问题来自哪块代码。自己开发的程序库中用到的第三方库也应为其中的名称加上前缀。提供“全能初始化方法”所有对象均要初始化。一般情况下开发者需向对象提供额外信息。通常情况下对象若不知道必要的信息则无法完成其工作。例如初始化UITableViewCell类对象时需要指明其样式及标识符用来区分不同类型的单元格。我们把为对象提供必要信息以便其完成工作的初始化方法叫做“全能初始化方法“。创建类实例的方法不止一种我们要在其中选定一个作为全能初始化方法令其他初始化方法都来调用它。只有在全能初始化方法中才会存储内部数据。当底层数据存储机制改变时只需修改此方法的代码就好无须改动其初始化方法。于是引出类继承时需要注意的一个重要问题如果子类的全能初始化方法与其超类方法的名称不同那么总应该覆写超类的全能初始化方法。例如开发者提供EOCRectangle类初始化设置属性覆写init方法来指明本类实例必须使用全能初始化方法来初始化现在创建EOCRectangle的子类EOCSquare该子类的全能初始化方法我们注意到它调用了超类的全能初始化方法。全能初始化方法的调用链一定要维系。然而调用者可能会使用init方法等来初始化EOCSquare对象为了避免出现宽高不等的正方形这种情况我们应该覆写其超类EOCRectangle的全能初始化方法如果超类的初始化方法不适用于子类那么常用的办法是覆写超类的全能初始化方法并于其中抛出异常这种方法看似突兀实则有时是必需的因为那种情况下创建出来的对象其内部数据有可能相互不一致。不过在OC程序中只有当发生严重错误时才抛出异常。因此初始化方法抛出异常是不得已之举。有时我们可能需要编写多个全能初始化方法那么每个子类的全能初始化方法都应该调用其超类的对应方法并逐层向上最后再执行与本类有关的任务。实现description方法调试程序时经常要打印并查看对象信息。最常用的做法是在构建需要打印到日志的字符串时object对象会收到description消息该方法返回的描述信息将取代格式字符串里的%。例如NSArray是系统类已经帮我们写好了description会输出那么如果在自定义类上这么做却会输出除非在自己的类里覆写description方法否则打印信息时会调用NSObject类所实现的默认方法。解决办法就是覆写description方法并将描述此对象的字符串返回即可。可以借助NSDictionary类的description方法使得在description中输出很多互不相同的信息。这样也可使得代码更易维护如果以后要向类中新增属性并在description方法中打印只需修改字典内容即可。NSObject协议中还有另一个方法值得注意那就是debugDescription此方法与description方法相似。区别在于debugDescription方法是开发者在调试器中以控制台命令打印对象时才调用的。在NSObject类的默认方法中此方法只是直接调用了description。具体演示定义一个Person类重写description和debugDescription方法interfacePerson:NSObjectproperty(nonatomic,copy)NSString*name;end-(NSString*)description{return[NSString stringWithFormat:description: %,self.name];}-(NSString*)debugDescription{return[NSString stringWithFormat:debug: %,self.name];}Person*p[Person new];p.nameJack;NSLog(%,p);运行可以看出调用description方法设置断点再运行可以看出调用debugDescription方法。控制器输入po p尽量使用不可变对象设立类时充分运用属性来封装数据。默认情况下属性是既可读也可写的这样设计出来的类都是可变的。笔者建议大家尽量减少对象中的可变对象。原因在于一般情况下我们要建模的数据未必需要改变。例如某数据所表示的对象源自一项只读的网络服务其中可能包含一系列需要显示在地图上的相关点像这种对象就没有必要改变其内容。即使修改新数据也不会推送回服务器。如果把可变对象放入集合之后又修改其内容那么很容易破坏set的内部数据结构使其失去固有的语义。具体到编译实践中则应尽量把对外公布出来的属性设为只读而且在有必要时才将属性对外公布。可是readonly不是不变是外部不可变。因此如果有人试着改变属性值那么编译会报错。有时我们想修改封装在对象内部的数据并不想令外人所改动通常做法是在对象内部将readonly属性重新声明为readwrite。对外可读内部可写。当然如果该属性是nonatomic的那么这样做可能会产生“竞争条件”。在对象内部写入某属性时对象外的观察者也许正读取该属性。若想避免此问题我们可以在必要时通过派发队列等手段将所有数据存取操作都设为同步操作。将属性在分类中重新声明属性的其他特质必须保持不变。这样声明的属性在对象的外部仍然能通过键值编码技术设置这些属性。[pointOfInterest setValue:abcforKey:identifier];KVC会在类中查找setIdentifier方法并借此修改属性。这样等同于绕过了本地所提供的API。在定义类的公共API时要注意对象里表示各种集合的那些属性是应该设成可变还是不可变的。如果内部有需要设置为可变类型的那么在外部调用方法返回时应该传入内部属性拷贝interfaceEOCPerson:NSObjectproperty(nonatomic,copy,readonly)NSString*firstName;property(nonatomic,copy,readonly)NSString*lastName;property(nonatomic,strong,readonly)NSSet*friends;-(id)initWithFirstName:(NSString*)firstName andLastName:(NSString*)lastName;-(void)addFriend:(EOCPerson*)person;-(void)removeFriend:(EOCPerson*)person;end#importEOCPerson.hinterfaceEOCPerson()property(nonatomic,copy,readwrite)NSString*firstName;property(nonatomic,copy,readwrite)NSString*lastName;property(nonatomic,strong)NSMutableSet*internalFriends;// 内部持有的可变集合endimplementationEOCPerson-(NSSet*)friends{return[_internalFriends copy];// 浅拷贝返回不可变类型}-(void)addFriend:(EOCPerson*)person{[_internalFriends addObject:person];}-(void)removeFriend:(EOCPerson*)person{[_internalFriends removeObject:person];}-(id)initWithFirstName:(NSString*)firstName andLastName:(NSString*)lastName{if((self[superinit])){_firstName[firstName copy];_lastName[lastName copy];_internalFriends[NSMutableSet new];}returnself;}end因此我们在编写代码时应通过提供相关方法来修改对象中的可变集合尽量不要将可变集合作为属性公开。使用清晰而协调的命名方式类、方法及变量的命名是OC编程的重要环节。类名首字母大写、通常有两三个前缀字母、方法与变量名以小写字母开头、其后每个单词首字母大写都使用驼峰式大小写命名法。给方法命名时有以下注意事项如果方法的返回值是新建的那么方法名的首个词应是返回值的类型除非前面还有修饰语。应该把表示参数类型的名词放在参数前面。如果方法要在当前对象上执行操作那么就应该包含动词若执行操作时还需要参数则应在动词后面加上一个或多个名词。不要使用str这种简称。Boolean属性应加上is前缀。如果某方法返回非属性的Boolean值那么应根据其功能选用has或is当前缀。将get前缀给那些借由“输出参数”来保存返回值的方法。类与协议的命名也应注意为类与协议名称加上前缀以避免命名空间冲突。为私有方法名加前缀为私有方法名加前缀的原因有有助于调试据此很容易能把公共方法和私有方法区别开。便于修改方法名或方法签名。前缀最好用p_p表示private下划线可把这个字母和真正的方法名区隔开下划线后面的部分按照驼峰法来命名即可。理解Objective-C错误模型自动引用计数在默认情况下不是“异常安全”的。这意味着如果抛出异常那么本应在作用域末尾释放的对象现在却不会自动释放了。如果想生成“异常安全”的代码可以通过设置编译器的标志来实现该标志叫做-fobjc-arc-exceptions。不过这将引入一些额外代码在不抛出异常时也照样要执行这部分代码。-fobjc-arc-exceptions标志的作用开启后编译器会为每个可能抛出异常的作用域生成完善的清理逻辑。当异常发生时ARC 会像正常退出一样释放所有本该在作用域末尾释放的对象。但这种机制即使在程序正常运行、没有抛出任何异常的情况下也会引入额外的运行时开销比如维护栈上的数据结构、注册清理函数等增加代码体积和运行时间。即使不用ARC也很难写出抛出异常时不会导致内存泄漏的代码。OC现在所采用的办法是只在及其罕见的情况下抛出异常抛出后无须考虑恢复问题而且应用程序此时也应退出。对于不那么严重的错误OC语言所用的编程范式是令方法返回nil/0或是使用NSError。NSError对象里封装了三条信息Error domain类型为字符串错误范围即产生错误的根源通常用一个特有的全局变量来定义。Error code类型为整数定义为枚举类型最佳错误码用以指明在某个范围内具体发生了何种词错误。User info类型为字典用户信息即有关此错误的额外信息。在设计API时NSError有以下几种常见用法通过委托协议来传递此错误。有错误发生时当前对象会把错误经由协议中的某个方法传给其委托对象。经由方法的“输出参数”返回给调用者。例如传递给方法的参数是个指针而该指针本身又指向另一个指针那个指针指向NSError对象。或者也可以把他当成一个直接指向NSError对象的指针。这样不仅能有普通的返回值也能经由输出参数把NSError对象回传给调用者。实际上在使用ARC时编译器会把方法签名中的NSError**转换成NSError*_ _autoreleasing*也就是说指针所指的对象会在方法执行完毕后自动释放。理解NSCopying协议NSCopying协议如果想让自己的类支持拷贝操作需要实现NSCopying协议。该协议只有一个方法-(id)copyWithZone:(NSZone*)zone;以前开发程序时会据此把内存分成不同的“区”zone而对象会创建在某个区里面。现在每个程序只有一个区“默认区”。尽管必须实现这个方法也不必担心其中的zone参数。copy方法由NSObject实现该方法只是以“默认区“为参数来调用copyWithZone:。我们总是想覆写copy方法其实真正需要实现的却是copyWithZone方法。若想使某个类支持拷贝功能只需声明该类遵从NSCopying协议并实现其中的方法即可。#importFoundation/Foundation.hinterfaceEOCPerson:NSObjectNSCopyingproperty(nonatomic,copy,readonly)NSString*firstName;property(nonatomic,copy,readonly)NSString*lastName;-(id)initWithFirstName:(NSString*)firstName andLastName:(NSString*)lastName;end-(id)initWithFirstName:(NSString*)firstName andLastName:(NSString*)lastName{if(self[superinit]){_firstName[firstName copy];_lastName[lastName copy];}returnself;}-(id)copyWithZone:(NSZone*)zone{EOCPerson*copy[[[selfclass]allocWithZone:zone]initWithFirstName:_firstName andLastName:_lastName];returncopy;}上述例子中我们采用全能初始化方法来初始化待拷贝对象令其执行所有初始化工作。然而有时除了要拷贝对象还要完成其他一些操作。NSMutableCopying协议该协议也只定义了一个方法-(id)mutableCopyWithZone:(NSZone*)zone;无论当前实例是否可变若需获取其可变版本的拷贝均应调用mutableCopy方法同理若需要不可变的拷贝则应通过copy方法来获取。该关系总是成立的这样就能实现可变版本与不可变版本之间的自由切换。要达到该目的另一个办法是提供三个办法copy、immutableCopy、mutableCopy。其中copy所返回的拷贝对象与当前对象的类型一致而另外两个方法则分别返回不可变版本与可变版本的拷贝。不过这种做法最好不要用在调用者并不知道其所用实例是否真的可变的情况下。深浅拷贝在编写拷贝方法时还要决定应该执行深拷贝还是浅拷贝。深拷贝在拷贝对象自身时将底层数据也一并复制过去。浅拷贝只拷贝容器对象本身而不复制其中数据集合类默认都执行浅拷贝这是因为容器内的对象未必都能拷贝而且调用者也未必想在拷贝容器时一并拷贝其中的每个对象。-(instancetype)initWithArray:(NSArrayObjectType*)array copyItems:(BOOL)flag;若copyItem参数设为YES则该方法会向数组中的每个元素发送copy消息。这个方法才是一个容器的深拷贝而采用普通的copy方式只是一个浅拷贝不会拷贝每一个set中的元素。注意不是遵从了NSCopying协议的对象都会执行深拷贝绝大多数都是浅拷贝。除非该类的文档说明它是用深拷贝来实现NSCopying协议的。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2430661.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!