【iOS】小蓝书学习(三)

OC编程规范与常见问题解析

前言

在我们学习使用OC构建应用程序时,经常会用到别人所写的代码,也可以将自己的代码开源出来供别人使用,这需要用到OC中常见的编程范式,也需要了解各种可能碰到的陷阱。本章的学习主要就是了解这些内容。

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

  • 在OC中,没有其他语言那种内置的命名空间机制。鉴于此,我们在起名时要设法避免潜在的命名冲突,否则很容易就重名了。
  • 为了避免这个冲突产生,我们要为所有名称都加上适当的前缀。所选前缀可以是与公司、应用程序或二者皆有关联之名
  • 如果自己所开发的程序中用到了第三方库,就应该为其中的名称加上前缀。

Apple宣称其保留使用所有“两字母前缀”的权利,所以自己许阿农的前缀应该是三个字母的

// 公司/应用程序相关的类名前缀
// 公司前缀
@interface ABCCompanyPerson : NSObject
@property (nonatomic, copy) NSString *name;
- (void)work;
@end

// 应用程序前缀
@interface MyAppViewController : UIViewController
@property (nonatomic, strong) ABCCompanyPerson *person;
@end

// 第三方库的类名前缀
// 第三方库前缀
@interface TPThirdPartyManager : NSObject
+ (instancetype)sharedManager;
- (void)performTask;
@end

// 你的程序库,使用了第三方库
@interface YourLibraryClass : NSObject
@property (nonatomic, strong) TPThirdPartyManager *manager;
- (void)doSomething;
@end

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

所有对象都要初始化,在初始化的时候,有的对象可能须开发者向其提供额外信息,不过一般来说还是要提供的。以iOS的UI框架UIKit为例,其中有个类叫做UItableViewCell,初始化该类对象时,需要指明其样式及标识符,标识符能够区分不同类型的单元格。我们把这种可为对象提供必要信息以便其能够完成工作的初始化方法叫做**“全能初始化方法”**。

全能初始化方法:

  • 在创建实例的方式不止一种,这个类就会有多个初始化方法。在这个过程中,我们就要选定一个作为全能初始化方法。其余的初始化方法初始化的时候都要调用全能初始化方法
  • 只有在全能初始化方法中,才能存储内部数据。(在底层数据存储机制改变时,只需修改全能初始化方法中的代码就好了)
- (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

这里以NSDate为例,上面代码块中就是它的几种初始化方法,在这些方法中,- (id)initWithTimeIntervalSinceReferenceDate: (NSTimeInterval)seconds是它的全能初始化方法,所以其他的初始化方法都会调用这个方法来进行初始化操作。

我们需要注意的是,在实现全能初始化操作时,一定要注意全能初始化的调用链一定要维系。有时候,如果子类的全能初始化方法与超类方法的名称不同,那么总应覆写超类的全能初始化方法。(有时候我们不想覆写超类的全能初始化方法,这种情况下我们一般方法是覆写超类的全能初始化并于其中抛出异常)

有时候,我们会碰到某对象的实例有两种完全不同的创建方法,这个时候,我们需要分开处理。由于这两种初始化方法的解码方式不同,而且我们也不能人为的改变其解码的方式,那么我们就只能顺其自然,他有两种我们也重写两种初始化方法,注意: 重写的这两种初始化方法一定是分别调用过之前的两种全能初始化方法的,并且表明这两种新的初始化方法分别适用于那种情况。
反正,总的来说,我们就是要维持原来类的调用链,每个子类的全能初始化方法都应该调用其超类的对应方法,并逐层向上。因为其父类有两个全能初始化方法,这两种初始化方法定义出来的数据可能是不同的,若是你在子类中调用了错误的父类初始化方法,它就会可能因为数据类型的问题使程序发生错误。

要点:

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

第17条:实现description方法

调试程序时,经常需要打印并查看对象,最常用的做法是向下面这样:

NSLog(@"object:%@", object);

在构建需要打印到日志的字符串时,object对象会收到description消息,该方法所返回的描述信息将取代“格式字符串”里的"%@"。例如:

NSArray* object = @[@"A string", @(123)];
NSLog(@"object = %@", object);
//输出结果	
object = (
  "A string",
   123
)

但是如果在自定义类上这么做,那么输出的信息却是:

object = <ECOperson: 0x7fd9a1600600>

这是由于我们没有重写description方法,这时我们应该重写该方法并将描述该对象的字符串返回即可,例如下面这样:

- (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;
}>

在自定义的description方法中,把待打印的信息放到字典里面,然后将字典对象的description方法所输出的内容包含在字符串里并返回,这样就可以实现简易的信息输出方式了。

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

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

要点:

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

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

在默认情况下,属性是“既可读又可写的”,这样设计出来的类都是可变的。当我们要使用不可变对象时,我们则需要使用readonly修饰符,这样的话,就会将属性变为只读的了。例如:

@property (nonatomic, copy, readonly) NSString *identifier; 
@property (nonatomic, copy, readonly) NSString *title;
@property (nonatomic, assign, readonly) float latitude;

这样的话,如果有人要改变属性值,那么编译的时候就会报错。

有时可能想修改封装在对象内部的数据,但是却不想令这些数据为外人所改动。这种情况下,通常的做法是在对象内部readonly属性重新声明为readwrite。当然,如果该属性是nonatomic的,那么这样做可能会产生“竞争条件”,即在对象内部写入某属性时,对象外的观察者也许正读取该属性。若想避免此问题,我们可以在必要时通过“派发队列”等手段,将(包括对象内部的)所有数据存取操作都设为同步操作。

@property (nonatomic, copy, readwrite) NSString *identifier; 

这样就可以在代码内部设置这些属性值了。在对象外部,仍然可以通过“键值编码”技术设置这些属性值,例如:

[pointOfInterest setValue:@"abc" forKey:@"identifier"];

这样子可以改动属性值,因为KVC会在类里查找setIdentifier:方法,并借此修改此属性。即使没有于公共接口中公布此方法,它也依然包含在类中。不过,这样做等于违规地绕过了本类所提供的API,要是开发者使用这种“杂技代码”的话,那么得自己来应对可能出现的问题。

要点:

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

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

方法命名的规则:

  • 如果方法的返回值是新创建的,那么方法名的首个词应是返回值的类型,除非前面还 有修饰语,例如localized String。属性的存取方法 不遵循这种命名方式,因为一般认 为这些方法不会* 创建新对象,即便有时返回内部对象的一份拷贝, 我们也认为那相当 于原有的对象。这些存取方法应该按照其所对应的属性来命名。
  • 应该把表示参数类型的名词放在参数前面。
  • 如果方法要在当前对象上执行操作,那么就应该包含动词;若执行操作时还需要参数, 则应该在动词后面加 上一个或多个名词。
  • 不要使用str 这种简称,应该用string 这样的全称。
  • Boolean属性应加is 前缀。如果某方法返回非属性的Boolean值,那么应该根据其功能,选用hasis当前缀。
  • get这个前缀留给那些借由“输出参数〞来保存返回值的方法,比如说,把返回值填充到"C语言式数组"里的那种方法就可以使用这个词做前缀 。

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

要点:

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

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

要点:

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

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

当前很多种编程语言都有都有“异常”机制,OC也不例外。

首先我们需要注意的是,“自动引用计数(ARC)”在默认情况下不是“异常安全的”。具体来说,如果抛出异常,那么本应在作用域末尾释放的对象现在不会自动释放了。如果想生成“异常安全”的代码,可以通过设置编译器的标志来实现,不过这将引入一些额外代码,在不抛出异常时,也照样要执行这部分代码。需要打开的编译器标志叫做-fobjc-arc-exceptions

可是在释放资源之前如果抛出异常了,那么该资源就不会被释放了

id someResource = /*...*/;
if (/*check for error*/) {
	@throw [NSException exceptionWithName:@"ExceptionName" reason:@"There was an error" userInfo:nil];
}
[someResource doSomething];
[someResource release];

在抛出异常之前释放doSomething,这样做可以解决问题,不过要是待释放的资源有很多,并且代码执行路径更为复杂的话,那么释放资源的代码就很容易写乱了。

在OC中,只有及其罕见的情况下才会抛出异常,异常抛出之后,无需考虑恢复的问题,而且应用程序此时也应该退出。对于代码中出现的不那么严重的问题,OC语言所采取的编程范式为令方法返回nil/0,或是使用NSError,去表明其中有错误发生。例如:

 - (id)initWithValue:(id)value {
	if (self = [super init]) {
		if (/*Value means instance can't be created*/) {
			self = nil;
		} else {
			//Initialize instance
		}
	}
	return self;
}

在这种情况下,如果if语句发现无法用传入的参数值来初始化当前实例,那么就会把self设置成nil,这样整个方法的返回值就是nil了。

NSError的用法更加灵活,因为经由此对象,我们可以把导致错误的原因回报给调用者。NSError对象里封装了三条消息:

  • Error domain(错误范围,类型为字符串)产生错误的根源,通常用一个特有的全局变量来定义。
  • Error code(错误码,类型为整数)独有的错误代码,用以指明在某个范围内具体发生了何种错误。某个特定范围可能会发生一系列相关错误,这些错误通常采用enum定义。
  • User info(用户信息,类型为字典)有关错误的额外信息,其中或许包含一段“本地化描述”(localized description),或许还含有导致该错误发生的另外一个错误,经由此种信息,可将相关错误串成一条“错误链”(chain of errors)。

要点:

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

第22条:理解NSCopying协议

如果想令自己的类支持拷贝操作,就要用到NSCopying协议,该协议只有一个方法:

- (id)copyWithZone:(NSZone*)zone

copy方法由NSObject实现,该发法只是以“默认区”为参数来调用copyWithZone,当我们想覆写copy方法时,其实真正需要实现的就是copyWithZone方法。
举例说明:

#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; 
}

深拷贝和浅拷贝:
深拷贝:在拷贝对象自身时,将其底层数据也一并复制过去。
浅拷贝:在拷贝对象自身时,只拷贝容器对象本身,而不复制其中的数据。

可以参考该篇文章:
【OC】深浅拷贝

没有专门定义深拷贝的协议,所以具体执行方法由每个类来确定,只需要决定自己所写的类是否提供深浅拷贝方法即可。大多数情况下,执行的都是浅拷贝。

要点:

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值