Objective-C(四)协议与分类

这是Objective-C系列的第4篇。

一、最佳实践

  • 在开发中合理巧妙的使用位段
  • 将类的实现代码分散到便于管理的数个分类之中

    • 使用分类机制把类的实现代码划分成易于管理的小模块;
    • 将应该视为“私有”的方法归入名叫Private的分类中,以隐藏实现细节。
  • 总是为第三方类的分类名称添加前缀

    • 向第三方类中添加分类时,总应该给其名称加上你专用的前缀;
    • 向第三方类中添加分类时,总应该给其中的方法加上你专用的前缀。
  • 勿在分类中声明属性

    • 把封装数据所用的全部属性都定义在主接口里;

    • 在“class-continuation”分类之外的其他分类中,可以定义存取方法,但尽量不要定义属性。

      编者按:在很多第三方开源库中,使用“关联对象”来在分类中定义属性是很常见的手段。

  • 使用“class-continuation”分类隐藏实现细节

    • 通过“class-continuation”分类向类中新增实例变量;
    • 如果某属性在主接口总声明为“readonly(只读)”,而类的内部又要用设置方法修改此属性,那么就在“class-continuation”中将其扩展为“readwrite(可读写)”;
    • 把私有方法的原型声明声明在“class-continuation”里面;
    • 若想使类所遵循的协议不为人所知,则可与“class-continuation”中声明。
  • 通过协议提供匿名对象

    • 协议可在某某种程度上提供匿名类型。具体的对象类型可以淡化成遵从某些一的id类型,协议里规定了对所应事先的方法。
    • 使用匿名对象来隐藏类型名称(或类名)。
    • 如果具体类型不重要,重要的是对象能够响应(定义在协议里的)特定方法,那么可使用匿名对象来表示。

二、实践详解

2.1 位段

在委托代理中,如果要频繁检查该代理是否响应某个方法,那么将代理相应能力缓存起来达到优化。而优化的最佳途径就使用“位段”。“位段”是一个C语言数据类型。

关于位段,简要做个说明:

2.1.1 定义

1
2
3
4
5
6
7
8
9
struct bs{
int a:1;
int :2; //无位段名,它只用来作填充或调整位置
int b:3;
int :0; //空域
int c:5; //从下一单元开始存放
};

struct bs data;

或者:

1
2
3
4
5
6
7
struct bs{
int a:1;
int :2;
int b:3;
int :0;
int c:5;
} data;

2.1.2 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
int main(){
struct{
unsigned a:1;
unsigned b:3;
unsigned c:4;
} bit, *pbit;
bit.a=1;
bit.b=7;
bit.c=15;
printf("%d, %d, %d\n", bit.a, bit.b, bit.c);
pbit=&bit;
pbit->a=0;
pbit->b&=3;
pbit->c|=1;
printf("%d, %d, %d\n", pbit->a, pbit->b, pbit->c);
return 0;
}
  1. 位段的类型只能是int,unsigned int,signed int三种类型,不能是char型或者浮点型;
  2. 位段占的二进制位数不能超过该基本类型所能表示的最大位数,比如在VC中int是占4个字节,那么最多只能是32位;
  3. 位域可以无位域名,这时它只用来作填充或调整位置。无名的位域是不能使用的;
  4. 若位段占的二进制位数为0,则这个位段必须是无名位段,下一个位段从下一个位段存储单元开始存放;

2.1.3 位段在委托代理模式中的应用

1
2
3
4
5
6
7
8
9
10
11
12
13
@class HONetworkFetcher;
@protocol NetworkFetcherDelegate <NSObject>

@optional
- (void)networkFetcher:(HONetworkFetcher*)fetcher didReceiveData:(NSData*)data;
- (void)networkFetcher:(HONetworkFetcher*)fetcher didFailerWithError:(NSError *)error;
- (void)networkFetcher:(HONetworkFetcher*)fetcher didUpdateProgerssTo:(float)progress;

@end

@interface HONetworkFetcher : NSObject
@property (nonatomic ,weak) id<HONetworkFetcherDelegate> delegate;
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#import "HONetworkFetcher.h"
@interface HONetworkFetcher()
{
struct {
unsigned int didReceiveData :1;
unsigned int didFailedWithError :1;
unsigned int didUpdateProgressTo :1;
} _delegateFlags;

}

@end

@implementation HONetworkFetcher

/**
* 在设置代理的时候检查方法可达性,并缓存起来
*/
- (void)setDelegate:(id<HONetworkFetcherDelegate>)delegate
{
_delegate = delegate;
_delegateFlags.didReceiveData = [delegate respondsToSelector:@selector(networkFetcher:didReceiveData:)];
_delegateFlags.didFailedWithError = [delegate respondsToSelector:@selector(networkFetcher:didFailerWithError:)];
_delegateFlags.didUpdateProgressTo = [delegate respondsToSelector:@selector(networkFetcher:didUpdateProgerssTo:)];
}

@end

2.2 将类的实现代码分散到便于管理的数个分类之中

  • 使用分类机制把类的实现代码划分成易于管理的小模块;
  • 将应该视为“私有”的方法归入名叫Private的分类中,以隐藏实现细节。

2.3 总是为第三方类的分类名称添加前缀

分类为现有类添加新功能,假如多个分类都为该类添加了同一个方法名的某一个方法,那么在运行时,会造成该方法名多次覆盖,以最后一次覆盖为主。假如遇到这种情况的bug,很难追溯源头,因为你不知道,其他人也重写了该方法。所以为了避免这种情况的发生,就需要为分类加上前缀,作为一个“命名空间”,比如:

1
2
3
@interface NSString	 (HOG_HTTP)
- (NSString*)hog_urlEncodedString;
@end

即便加了前缀,也难保其他分类不会覆盖你所写的放方法。但是降低了概率。

  • 向第三方类中添加分类时,总应该给其名称加上你专用的前缀;
  • 向第三方类中添加分类时,总应该给其中的方法加上你专用的前缀。

2.4 勿在分类中声明属性

属性是封装数据的方式。在技术上,分类也可以声明属性,但是要避免这种做法。

声明文件:

1
2
3
@interface HOPerson (Friends)
@property (nonatomic ,strong)NSSet *friends;
@end

实现文件:

1
2
@implementation HOPerson(Friends)
@end

这时会发出警告:

1
2
HOPerson+Friends.m:11:17: Property 'friends' requires method 'friends' to be defined - use @dynamic or provide a method implementation in this category
HOPerson+Friends.m:11:17: Property 'friends' requires method 'setFriends:' to be defined - use @dynamic or provide a method implementation in this category

要消除警告,要么添加@dynamic,要么添加对应的setter/getter方法。

下面是在实现文件里添加setter/getter方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#import "HOPerson+Friends.h"
#import <objc/runtime.h>

static const char *kFriendPropertyKey = "kFriendPropertyKey";

@implementation HOPerson(Friends)
//@dynamic friends;

- (NSSet *)friends
{
return objc_getAssociatedObject(self, kFriendPropertyKey);
}

- (void)setFriends:(NSSet *)friends
{
objc_setAssociatedObject(self,
kFriendPropertyKey,
friends,
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

在本例中,正确的做法是将所有的属性都定义在主接口里。主接口是唯一能定义成员变量(数据)的地方。而属性只是定义实例变量及相关存取方法所用的“语法糖”,所以也应遵循同实例变量一样的规则。至于分类机制应将其理解为一种手段,目标在于扩展类的功能,而非封装数据。

  • 把封装数据所用的全部属性都定义在主接口里;
  • 在“class-continuation”分类之外的其他分类中,可以定义存取方法,但尽量不要定义属性。

2.5 使用“class-continuation”分类隐藏实现细节

class-continuation和其他分类不同,它必须定义在其所接续的那个类的实现文件里。其重要之处在于,这是唯一能声明实例变量的分类,而且此分类没有特定的实现文件,其中的方法都应定义在类的主实现文件里。

可参考本文上段中“位段在委托代理模式中的应用”中定义的位段,即实例变量。

在class-continuation中定义实例变量,主要是为了将细节隐藏起来。

另外,在class-continuation中声明只有在类的实现代码中的私有方法也是较为可取的。在编写类的实现代码之前,先在class-continuation中将需要实现的方法原型声明,然后逐一实现。比如:

1
2
3
4
5
6
7
8
9
10
11
@interface HOPerson()
{
NSMutableSet *_internalFriends;
}

- (void)hog_findFriends;

@end

@implementation HOPerson
@end

最后,还有一种情况,就是对象所遵循的协议只应视为私有的话,那么最好也在class-continuation中声明。比如:

1
2
3
4
5
6
7
8
9
10
@interface HOPerson()<NSCopying,NSCoding>
{
NSMutableSet *_internalFriends;
}

- (void)hog_findFriends;
@end

@implementation HOPerson
@end
  • 通过“class-continuation”分类向类中新增实例变量;
  • 如果某属性在主接口总声明为“readonly(只读)”,而类的内部又要用设置方法修改此属性,那么就在“class-continuation”中将其扩展为“readwrite(可读写)”;
  • 把私有方法的原型声明声明在“class-continuation”里面;
  • 若想使类所遵循的协议不为人所知,则可与“class-continuation”中声明。

2.5 通过协议提供匿名对象

  • 协议可在某某种程度上提供匿名类型。具体的对象类型可以淡化成遵从某些一的id类型,协议里规定了对所应事先的方法。
  • 使用匿名对象来隐藏类型名称(或类名)。
  • 如果具体类型不重要,重要的是对象能够响应(定义在协议里的)特定方法,那么可使用匿名对象来表示。