编写高质量iOS与OSX代码的52个有效方法-第三章:接口与AP
15、使用前缀避免命名空间冲突
1、重命名符号错误
OC没有其他语言内置的命名空间(namespace),命名时要避免潜在的命名冲冲突(naming clash):
15-1.png
比如如下错误,就是重命名符号错误(duplicate symbol error)。
duplicate symbol _OBJC_CLASS_$_DogObject in:
xxx/DogObject-ED8631F460AAA56A.o
xxx/DogObject-917EE703FAC7406E.o
duplicate symbol _OBJC_METACLASS_$_DogObject in:
xxx/x86_64/DogObject-ED8631F460AAA56A.o
xxx/DogObject-917EE703FAC7406E.o
ld: 2 duplicate symbols for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
-
解决方法:把错误中提到的
duplicate symbol _OBJC_CLASS_$_DogObject
类名检查一遍,重新配置。 -
避免方法(尤其是在引入很多三方库,或者项目工程文件较多的时候,应该说在所有的项目中都要如此):变相实现命名空间--为所有名称都加上是当前缀。
另,苹果宣称保留使用所有两个字母的前缀(two-letter Prefix)的权利。所以自己选用前缀最好是三个字母。
2、给新增分类和分类方法加上前缀(第25条)
分类机制通常用于向无源码的既有类中新增功能。
分类的方法是直接加到类中的,就好比是类中固有的方法,将分类方法加入类中这一操作时在运行期系统加载分类时完成的。运行期系统会把分类中所实现的每个方法都加入类的方法列表中。
如果类中本来就有这个方法,分类中又实现了一次,那么分类中方法会覆盖原来的实现代码。有可能会发生多次覆盖。
如果多个分类名称相同,在运行期,是不会报错的。但是加载的分类有可能不是你所期望的。
如果有相同的方法,那么运行时调用的不一定是你想要的方法。
运行期不会报错,但是在实现结果的时候,就会出现未知错误。
15-2.png比如实现了NSString的两个分类,同时都有一个分类方法"- (NSString *)urlEncordedString;"那么在调用过程中,就不知道是调用哪个分类中实现的方法。同样能够正常编译通过。
NSString *urlString = @"http://www.baidu.com";
NSLog(@"%@",[urlString urlEncordedString]);
因为不会报错,这种问题比较难发现,所以在写之前就避免这种情况就显得非常重要。
当然如果分类名相同,但是方法名不同时,有可能出现的问题是:No visible @interface for 'NSString' declares the selector 'seconString'
,你无法调用自己实现的方法。系统只是提供最后加载到的分类,如此而已。
- 如何避免:添加分类时,给分类名称加上专用前缀,同时给分类方法名加上专用前缀。
3、类的实现文件中所用的纯C函数及全局变量
在类的实现文件中所用的纯C函数及全局变量,在编译好的目标文件中,这些名称要算作“顶级符号”(top-level symbol)。
如果在不同的类文件中实现同样的C函数,就会报重命名符号错误(duplicate symbol error)
15-3.pngduplicate symbol _completion in:
xxx/ViewController.o
xxx/NSString+Http.o
ld: 1 duplicate symbol for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
duplicate symbol _completion
会指出错误方法名completion
,另外会在下面的描述中指明在那些文件中出现冲突。
-
解决方法:找到错误的类和方法名,修改。
-
避免方法:C函数名加前缀,同时加上类名信息,在回溯查找问题是就能很快确定位置。
同样的,即使在实现文件中声明全局静态变量,在不同文件中声明相同名称的变量,也会出现名称冲突错误:
15-4.pngduplicate symbol _NameString in:
xxx/ViewController.o
xxx/NSString+Http.o
ld: 1 duplicate symbol for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
-
解决方法:找到错误的类和声明的变量名,修改。
-
避免方法:声明全局变量,变量名前加前缀。
4、所开发的程序库中用到第三方库,给第三方库加前缀
这个问题很简单,如果要把自己封装的程序库给别人用,同时使用了不同的第三方库。那么在别人引用的时候,如果他工程中也使用相同的第三方库,就会出现重命名符号错误。
另外考虑到所使用第三方库版本不同,那么,在封装自己的程序库时就要将所用到的第三方库中的文件添加前缀,避免此类问题。
16、提供全能初始化方法
全能初始化方法(designated initializer):为对象提供必要信息以便其能完成工作的初始化方法。
可以通过警告或者设置默认值调用全能初始化方法的方式,实现初始化。
全能初始化方法的调用链一定要维系,也即是,集成关系中,初始化方法的维护调用。
Mac OS X 的APPKit会iOS的UIKit两个UI框架都广泛运用序列化机制(serialization mechanism),将对象序列化,保存至XML格式的XIB文件中农。这些XIB文件通常用来存放视图控制器机器视图布局。加载NIB文件时,系统会在解压缩的过程中解码视图控制器。
-
在类中提供一个全能初始化方法,并于文档里指明。其他初始化方法均应调用此方法。
-
若全能初始化方法与超类不同,则需要覆写超类中的对应方法。
-
若超类初始化方法不适用于子类,那么应该覆写这个超类方法,并在其中抛出异常。
17、实现description方法
实现description
方法返回一个有意义的字符串,用以描述该实例。
- (NSString *)description {
return [NSString stringWithFormat:@"%@ %zd",_dogName,_dogAge];
}
若想在调试时(LLDB)打印出更详尽的对象描述信息,则应实现debugDescription
方法。
- (NSString *)debugDescription {
return [NSString stringWithFormat:@"<%@: %p \"%@ %zd \">",[self class],self,_dogName,_dogAge];
}
打印结果
17-1.png18、尽量使用不可变对象 --
关联第6条-属性
尽量减少对象中的可变内容,应该尽量把对外公布出来的属性设为只读,并且只在必要时才将属性对外公布。
如果想要修改封装在对象内部的数据,同时不将哲学数据为外人所动,可以在对象内部将readonly属性重新声明为readwrite。
在定义类的公共API时,对象里表示各种collection的那些属性究竟应该设成可变的,还是不可变的。
- 尽量创建不可变的对象
- 若某属性仅可于对象内部修改,则在class-continuation分类中将其由readonly扩展为readwrite。
- 不要把可变的collection作为属性公开,而应提供相关方法,以此修改对象中的可变collection。
19、使用清晰协调的命名方式
OC中一般采用驼峰式大小写命名法。
1)方法命名
- 如果方法的返回值是新创建的,那么方法名的首个词应是返回值的类型,除非前面还有修饰语,如
localizedString
。属性的存取方法不遵循这种命名方式,一边惹味这种方法不会创建新对象,即使又是返回内部对象的一份拷贝,也认为那相当于原有的对象。这些存取方法应按照其所对应的属性来命名。
- (NSString *)stringOfDogInfomation;
- (NSDictionary *)dictionaryOfDogInfomation;
-
应该把表示参数类型的名词放在参数前面。
-
如果方法要在对象上执行操作,就应包含动词,若执行操作还需要参数,应在动词后面加上一个或多个名词。
-
不要使用
str
这样的简称,而用string
这样的全称。
[string lowercaseString];
- Boolean属性应加is前缀,如果方法返回非属性的Boolean值,那么应该根据其功能,选用has或is前缀。
[string hasSuffix:@"this"];
[string isEqualToString:@"xxxx"];
-
get
前缀留给那些借由输出参数来保存返回值的方法,
清晰明了,统一规范
2)类与协议的命名
应该为类和协议加上前缀,避免命名空间冲突。
命名方式要协调一致,如果要从其他框架中继承子类,务必遵循其命名惯例。
若要自定义委托协议,则名称中应包含委托发起方的名字,再加上Delegate
。
- 起名时,遵从标准的OC命名规范
- 方法名要言简意赅,从左只有读起来想个日常用语中的句子。
- 方法名里不要使用缩略后的类型名称
- 方法起名,确保风格与自己的代码或所要集成的框架相符。
20、为私有方法名加前缀
为在内部使用的私有方法加前缀,区分公共方法和私有方法,便于修改方法名和方法签名。
依据个人习惯,p_method
。
- 在私有方法名称前加上前缀,区分私有和公共方法。
- 不要单用一个下划线做私有方法的前缀,因为这种是苹果公司预留的。
21、理解OC错误模型
1)异常 exception --fatal error致命错误
OC中,在激起罕见的情况下抛出异常,异常抛出之后不再考虑恢复问题,应用程序此时应该退出。不需要再辨析复杂的“异常安全”代码。
异常一般只用于处理严重错误(fatal error 致命错误)。
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:string userInfo:nil];
比如编写某个抽象基类,正确用法是先从中集成一个子类,再用这个子类。这种情况下,如果直接使用了这个抽象基类的,那么可以抛出异常。
OC没有办法将某个类标记为“抽象类”。要想达成效果,最好的办法是在那些子类必须覆写的超类方法里抛出异常。
2)其他错误 --nonfatal error 非致命错误
OC语言所用编程范式为:令方法返回nil/0,或是使用NSError,表明有错误发生。
NSError对象封装了三条消息:
-
Error domain(错误范围,类型为字符串)
错误发生的范围,也就是产生错误的根源,通常用一个特有的全局变量来定义。如NSURLErrorDomain-处理URL的子系统,在从URL中解析或取得数据出错。 -
Error code 错误码,其类型为整数
独有的错误代码,用以指明在某个范围内具体发生了何种错误。某个特定范围内可能会发生一些列相关错误,这些错误情况通常采用enum来定义。如,HTTP请求出错时,会把HTTP状态码设为错误码。 -
User info 用户信息,其类型是字典
有关次错误的额外信息,其中或许包含一段本地化的描述(localized description),或许还包含导致该错误发生的另一个错误,经由此种信息,可将相关错误串成一条错误连(chain of errors)
NSError的用法:
-
通过委托协议来传递错误。
当有错误发生时,当前对象会把错误信息经由协议的某个方法传递给其委托对象(delegate)。如NSURLConnection
在其委托协议NSURLConnectionDelegate
中定义代理方法- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;
-
经由方法的输出参数返回给调用者。
- (BOOL)doSomething:(NSString *)thing error:(NSError **)error {
if ([thing isEqualToString:@"1"]) {
return YES;
}
*error = [NSError errorWithDomain:NSURLErrorDomain code:100 userInfo:@{@"key":@"something wrong"}];
return NO ;
}
NSError *error;
BOOL ret = [littleDog doSomething:@"0" error:&error];
if (!ret) {
NSLog(@"error : %@",[error debugDescription]);
}
另外,定义自己的指定的专用错误范围字符串,使用这个字符串创建NSError对象,就能确定错误来源。
extern NSString *const ZYDErrorDomain;
typedef NS_ENUM(NSUInteger,ZYDError) {
ZYDErrorUnknown = -1, //未知错误
ZYDErrorBadInput = 500,
};
3)
- 只有发生了可使整个应用程序崩溃的严重错误时,才应使用异常。
- 一般错误,可使用指派委托方法来处理错误,也可以把错误信息放在NSError对象中,经由输出参数返回给调用者。
22、理解NSCopying协议
1)不可变拷贝 NSCopying
OC 中如果需要拷贝对象,需要通过copy方法完成。如果希望自己的类支持拷贝操作,就要实现NSCopying协议。该协议只要一个方法:
- (id)copyWithZone:(NSZone *)zone;
在以前开发程序时,会据此吧内存分成不同的区(zone),而对象会创建在某个区里面。现在不用,每个程序只有一个区:默认区(default zone)。所以需要实现这个方法,但是不用担心zone
参数。
若要某个类支持拷贝功能,需要改类声明遵从NSCoping协议,并实现其中的方法就可以。
.h
@interface DogObject : NSObject <NSCopying>
@end
.m
@interface DogObject()
{
NSMutableArray *_familys;//内部成员变量,并非属性
}
@end
@implementation DogObject
- (instancetype)initWithDogName:(NSString *)dogName age:(NSInteger)age {
self = [super init];
if (self) {
_dogName = [dogName copy];
_dogAge = age;
}
return self;
}
#pragma mark -- NSCopying
- (id)copyWithZone:(NSZone *)zone {
DogObject *copy = [[[self class] allocWithZone:zone] initWithDogName:_dogName age:_dogAge];
copy -> _familys = [_familys mutableCopy]; //有
return copy;
}
2)可变拷贝 NSMutableCopying
定义一个方法:
- (id)mutableCopyWithZone:(NSZone *)zone
,与copy类似,也用默认的zone参数来调mutableCopyWithZone:
。如果类分为可变版本,可不可变版本,需要实现NSMutableCopying。
3)深拷贝
深拷贝:在拷贝对象自身是,将其底层数据也一并复制过去。
浅拷贝:之拷贝容器对象本身,而不复制漆黑中的数据。
容易内的对象并不都能拷贝,而且调用者也未必要在拷贝容器的同时一并拷贝其中的每个对象。
一般NSCopying大多数情况下执行的是浅拷贝,如需要在对象上执行深拷贝,那么除非该类的文档说它是用深拷贝来实现NSCopying协议的,否则,要么寻找能够执行深拷贝的方法,要么自己编写方法来实现。
比如,NSArray中的方法:- (instancetype)initWithArray:(NSArray<ObjectType> *)array copyItems:(BOOL)flag
如果flag为YES,该方法会向数组中每个元素发送copy信息,用拷贝好的创建新的Array,并返回给调用者。
可以给对象创建自定义深拷贝方法:
- (id)deepCopy {
DogObject *copy = [[[self class] alloc] initWithDogName:_dogName age:_dogAge];
copy -> _familys = [[NSMutableArray alloc] initWithArray:_familys copyItems:YES];
return copy;
}
4)
- 令自己所写的对象具有拷贝功能,需要实现NSCopying协议
- 如果自定义对象分为可变和不可变版本,需要同时实现NSCopying与NSMutableCopying协议。
- 复制对象时需决定采用浅拷贝还是深拷贝,一般情况下尽量执行浅拷贝
- 如果对象需要深拷贝,那么新增一个专门执行深拷贝的方法。