编写高质量iOS与OSX代码的52个有效方法-第三章:接口与API协议

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.png
duplicate 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.png
duplicate 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.png

18、尽量使用不可变对象 --

关联第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协议。
  • 复制对象时需决定采用浅拷贝还是深拷贝,一般情况下尽量执行浅拷贝
  • 如果对象需要深拷贝,那么新增一个专门执行深拷贝的方法。

推荐阅读更多精彩内容