iOS-有效编写高质量Objective-C方法五

我们前面的4篇文章,已经将Effective OC 的前两章节讲完了,接下来几篇文章,主要会讲我们在平时编码中的一些习惯,一些细节问题的处理,包括接口和API的设计,协议delegate和分类的使用等。

用前缀避免命名空间的冲突

因为OC和其他语言不同,没有命名空间这一说法,因此,我们在给类起名时,要设法避免潜在的命名冲突。发生命名冲突时,我们经常会看到这样的错误:

1
2
3
4
5
6
duplicate symbol _OBJC_METACLASS_$_XXXXX in:
build/something.o
build/something_else.o
duplicate symbol _OBJC_CLASS_$_XXXXX in:
build/something.o
build/something_else.o

上面的情况,意思为有两个地方都实现了名为XXXXX的类,这种情况,往往出现在,我们引用多个第三方库,第三方库之间命名冲突、或者是我们自己工程中的命名与第三方库冲突。

所以我们在编写类名时,一般都会加上跟自己工程相关的一些前缀,而且在xcode中是有一个功能,让我们在新建类时,自动给我们加上前缀:

这样在我们新建类时,默认就会有一个PPS的前缀:

注意:

如果你正在编写第三方库,供别人使用,那么请一定要为你的所有类名加上你自定义的前缀,这样,别人用你的SDK才不会出现上面的冲突情况,这一点非常重要。

提供“全能初始化方法”

首先,我们来解释一下什么是全能初始化方法:

举个例子吧

在NSDate类中,初始化方法有下面这几种:

1
2
3
4
5
- (instancetype)init;
- (instancetype)initWithTimeIntervalSinceReferenceDate:(NSTimeInterval)ti;
- (instancetype)initWithTimeIntervalSinceNow:(NSTimeInterval)secs;
- (instancetype)initWithTimeIntervalSince1970:(NSTimeInterval)secs;
- (instancetype)initWithTimeInterval:(NSTimeInterval)secsToBeAdded sinceDate:(NSDate *)date;

在上面的几个初始化方法中,(instancetype)initWithTimeIntervalSinceReferenceDate:是全能初始化方法,意思就是

其他的初始化方法,都是最终都是调用它,生成了NSDate类。

为什么要引入全能初始化方法这个概念呢?在我们平时编码中,经常在生成一个类时,传入一些参数,才能使这个类进行正常的工作,如果我们设计时,这个类有很多的初始化方法,然而进行一段时间编码过后,我们发觉需要修改这个类的底层数据,就是生成这个类必须要使用到的一些数据,那么,这么多的初始化方法,我们都得修改,如果我们有一个全能初始化方法,那么我们只需要改动这个全能初始化方法,底层数据就已经改变。

实例

这里还是引入一个例子:

比如我们要编写一个表示矩形的类:

1
2
3
4
5
6
7
8
#import <Foundation/Foundation.h>

@interface EOCRectangle : NSObject

@property (nonatomic, assign, readonly) float width;
@property (nonatomic, assign, readonly) float height;

@end

这里,为什么将长宽设置成readonly,我们在后面一篇文章中讲,这样一来,我们就需要提供一个初始方法来设置这两个参数:

1
2
- (instancetype)initWithWidth:(float)width
andHeight:(float)height;
1
2
3
4
5
6
7
-(instancetype)initWithWidth:(float)width andHeight:(float)height{
if (self = [super init]) {
_width = width;
_height = height;
}
return self;
}

这里我们会碰到一个问题,如果有人直接用

1
[[EOCRectangle alloc] init];

这个方法来初始化,我们 的长宽不是没办法设置了,那这个类肯定不能正常工作,我们不希望看到这种情况发生,通常,我们有两种方法处理:

第一,在init方法中,传入默认的值,就是讲类必须的参数的默认值传入,形成一个类

1
2
3
-(instancetype)init{
return [self initWithWidth:5.0f andHeight:10.0f];
}

第二种方法,是我们不希望开发者调用init方法,这样类就不能正常工作,我们可以在init方法中,抛出异常,但是一般我们不这样处理,在OC中,只有发生严重错误时,我们才抛出异常

1
2
3
-(instancetype)init{
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"不允许调用这个初始化方法,请调用initWithWidth:andHeight:方法" userInfo:nil];
}
在继承中处理全能初始化方法

如果我们现在需要创建一个正方形类EOCSquare类,让他继承自EOCRectangle类,这很正常,正方形的长宽必须相等。那么我们需要提供一个传入边长的初始化方法。

1
- (instancetype)initWithDimension:(float)dimension;

在实现中:调用父类的方法,传入相同的长宽,即是一个正方形

1
2
3
- (instancetype)initWithDimension:(float)dimension{
return [super initWithWidth:dimension andHeight:dimension];
}

然而,即使我们提供了传入边长的初始化,方法,调用者还是可能会调用initWithWidth:andHeight:或者init方法来初始化,这是我们不愿意看到的,于是就有一个重要的结论:

如果子类的全能初始化方法和父类的全能初始化方法不同,那么总是应该覆写父类的全能初始化方法

1
2
3
4
-(instancetype)initWithWidth:(float)width andHeight:(float)height{
float dimension = MAX(width, height);
return [self initWithDimension:dimension];
}

这时,我们发现,不管调用者调用了initWithWidth:andHeight:还是init方法,都能够正常初始化EOCSquare了,因为如果开发者调用initWithWidth:andHeight:,那么因为我们EOCSquare覆写了,所以调用的是EOCSquare的方法,如果调用者调用了init方法,那么最终调用到的还是EOCSquare的全能初始化方法。

但是,我们一般不覆写父类的全能初始化方法,这样显得毫不合理,改变父类的全能初始化方法逻辑,所以我们一般这样处理,在子类中,覆盖父类的全能初始化方法,并且抛出异常

1
2
3
-(instancetype)initWithWidth:(float)width andHeight:(float)height{
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"不允许调用这个初始化方法,请调用initWithDimension:方法" userInfo:nil];
}

这样一来,我们还需要覆写init方法

1
2
3
-(instancetype)init{
return [self initWithDimension:5.0f];
}

这样,我们就认为,开发者在初始化正方形时,只能传入相应的边长,如果传入长宽,那么认为是调用者自己犯了错误。

总结
  • 在类中提供一个全能初始化方法,并指明其他初始化方法都需要调用全能初始化方法
  • 若全能初始化方法不同,则子类应该覆写超类的全能初始化方法
  • 如果超类的全能初始化方法不适用于子类,那么应该在子类中覆写超类的全能初始化方法,并且在其中抛出异常

实现description

在调试程序时,我们经常需要将对象的信息打印出来,最常用到的方式就是:

1
NSLog(@"object = %@",object);

在打印数组或者字典上,这样是很好用的:

1
2
3
4
5
6
7
8
NSArray *arr = @[@"111",@"222"];
NSLog(@"arr = %@",arr);

//打印出
arr = (
111,
222
)

但是如我们在自定义的类中,就不会像刚才那样输出了,输出的往往是这样:

1
2
3
4
5
EOCRectangle *rectangle = [[EOCRectangle alloc] initWithWidth:10 andHeight:5];
NSLog(@"rectangle = %@", rectangle);

//打印输出
rectangle = <EOCRectangle: 0x1740068f0>

输出的是一堆内存地址

这样一点也不好调试,要想输出对象的信息,我们需要在类中,重写description方法,在写的时候,我们可以借助NSDictionary的输出格式

1
2
3
4
5
6
7
8
9
-(NSString *)description{
return [NSString stringWithFormat:@"%@:%p,%@",
[self class],
self,
@{
@"width":@(_width),
@"height":@(_height),
}];
}

这样我们的输出就是这样的:

1
2
3
4
rectangle = EOCRectangle:0x17000b2b0, {
height = 5;
width = 10;
}

简单明了

还有一个方法:debugDescription

这个方法,是我们在调试的时候,打断点,在xcode的控制台输出的时候,需要打印的东西

1
2
3
-(NSString *)debugDescription{
return @"po 的时候打印我";
}

我们在类EOCRectangle重写了debugDescription方法,然后再控制台打印

这下明白了吧,好,今天先到这里。