Objective-C(四)协议与分类

本系列说是语言基础,只是摘《Effective Objective-C 2.0》书中精要,选开发中实践的经验之谈,汇集于此,便于查阅,或者为来访者提供一份参考。

编排按《Effective Objective-C 2.0》中条目。

位段

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

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

定义

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

struct bs data;

或者:

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

示例

#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,则这个位段必须是无名位段,下一个位段从下一个位段存储单元开始存放;

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

@class JSNetworkFetcher;
@protocol JSNetworkFetcherDelegate <NSObject>
@optional

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

@end

@interface JSNetworkFetcher : NSObject

@property (nonatomic ,weak) id<JSNetworkFetcherDelegate> delegate;

@end



#import "JSNetworkFetcher.h"
@interface JSNetworkFetcher()
{
    struct {
        unsigned int didReceiveData                 :1;
        unsigned int didFailedWithError             :1;
        unsigned int didUpdateProgressTo            :1;
    } _delegateFlags;

}

@end

@implementation JSNetworkFetcher

/**
 *  在设置代理的时候检查方法可达性,并缓存起来
 */
- (void)setDelegate:(id<JSNetworkFetcherDelegate>)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

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

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

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

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

@interface NSString     (JSG_HTTP)
- (NSString*)jsg_urlEncodedString;
@end

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

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

勿在分类中声明属性

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

声明文件:

@interface JSPerson (Friends)
@property (nonatomic ,strong)NSSet *friends;
@end

实现文件:

@implementation JSPerson(Friends)
@end

这时会发出警告:

JSPerson+Friends.m:11:17: Property 'friends' requires method 'friends' to be defined - use @dynamic or provide a method implementation in this category
JSPerson+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方法:

#import "JSPerson+Friends.h"
#import <objc/runtime.h>

static const char *kFriendPropertyKey  = "kFriendPropertyKey";

@implementation JSPerson(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”分类之外的其他分类中,可以定义存取方法,但尽量不要定义属性。

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

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

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

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

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

@interface JSPerson()
{
    NSMutableSet *_internalFriends;
}

- (void)jsg_findFriends;

@end

@implementation JSPerson

@end

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

@interface JSPerson()<NSCopying,NSCoding>
{
    NSMutableSet *_internalFriends;
}

- (void)jsg_findFriends;

@end

@implementation JSPerson

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

通过协议提供匿名对象

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