Category VS. Class Extension

分类和类的扩展在Objective-C中是紧密相连的话题;但是,他们之间存在着不少区别。能够合理地使用分类类扩展不断能够使得一个类的代码结构更加清晰,还能将私有方法、属性和公开方法、属性合理的区分开。

Category分类

分类提供了为某个类增加其他行为的途径;该类可以是自己写的能够获得其实现源代码的类,也可以是已经封装好的不能够看到实现源代码(如framework的类)。

分类的语法:

#import "DemoChatViewController.h"

@interface DemoChatViewController (DemoGroupChat)

/**
 首次加载群聊消息
 */
- (void)loadGroupChatMessages;

/* 
 other methods
 ...
 */
@end
#import "DemoChatViewController+DemoGroupChat.h"

@implementation DemoChatViewController (DemoGroupChat)
- (void)loadGroupChatMessages {
   /* implementation */
}
@end

上面的代码示例展示的是为类DemoChatViewController写一个名为DemoGroupChat的分类。该分类既可以看做为原来类添加了群聊的相关功能,也可以认为是对原来类的方法按照功能模块来划分不同类别。

在为某个类使用分类的方法进行扩展其功能的时候,需要注意的一点就是分类中的方法是不能够和该类的主要实现中某方法、该类其他分类中的某方法以及该类父类中的方法同名。如果出现同名,那么在运行时,那个方法被调用是未知的。

关于分类,有一点非常重要。分类中尽管是可以申明property,但是该property是不会自动合成实例变量,也不会有accessor methods

  • 分类中是可以申明不同的property的;
  • 这些property不是instance-variable-backed property

所以,不能简单理解为分类中不能够申明property。后面的案例中可以看到,利用分类中可以申明property的特性,可以将一个类的属性合理地暴露给其他类或分类。而在不需要知道这些属性的情形下,只是暴露该类的primary header file主头文件。

同样延续上面代码案例的使用场景,可以设计一个除了DemoChatViewController.h这个主要的头文件(primary header file)以外的另一个头文件DemoChatViewController+Private.h。在该头文件中存放一些不需要被其他类知道,但是需要在DemoChatViewController的多个分类中需要使用的属性。

例如:

#import "DemoChatViewController.h"
@interface DemoChatViewController (Private)
/* 视图 */
@property (nonatomic, readonly, weak) UITableView *chatTable; //消息列表
@property (nonatomic, strong) NSMutableArray *cellData; //消息数据

这样,在前面的案例代码DemoChatViewController+DemoGroupChat.m可以导入该名为private的分类,那么在从服务器获取完群消息之后,就可以直接在方法- (void)loadGroupChatMessages中处理完消息数据,加入到cellData数组中,在调用self.chatTablereloadData方法刷新界面的消息。

那么,刚才已经说过了。分类中声明的property是不会有instance variable被自动合成的,也就是说在运行时,DemoChatViewController的对象是没有chatTablecellData实例变量的。那上面的写法难道没有很大的问题?

当然没有,因为这些property在类扩展中被重新申明了。

Class Extension类扩展

类扩展实际上就是匿名的分类(anonymous category)。它实际上就是在@implementation语句上的未命名分类。

例如,

@interface DemoChatViewController ()
/* 视图 */
@property (nonatomic, weak) UITableView *chatTable; //消息列表
@property (nonatomic, strong) NSMutableArray *cellData; //消息数据
@end

@implementation DemoChatViewController
@end

所以,尽管类扩展是一种特殊的分类。但是其中定义的属性会被自动合成对象的实例变量,也就是对于属性chatTable来说,DemoChatViewController对象会存在一个_chatTable的实例变量。同理,对于cellData也是一样。

如果仔细观察分类DemoChatViewController+Private.h中属性chatTable和类扩展中的该属性,它们在属性的修饰关键字上还有一点不同。

@property (nonatomic, readonly, weak) UITableView *chatTable; //来自分类

@property (nonatomic, weak) UITableView *chatTable; //来自类扩展

也就是分类中的该属性是readonly而在类扩展中是默认readwrite的。这样写的好处是在于,在外部尽管可以使用chatTable;但是,如果视图setchatTable的话,编译器就会警告。而在该类的主要实现文件(primary implementation)中,是完全可以使用self.chatTable = ...来设置该chatTable

另外,需要注意的是类扩展(class extension)中声明的属性或者方法都和该类的实现文件有很强的绑定关系。简单的说就是,被自动合成的实例变量和accessor methods是放到primary implementation文件中的;那扩展中申明的方法也是必须要在primary implementation文件中实现。

示例整合

整合上面的实例代码,分别获得了五个文件:

  • DemoChatViewController.h为主头文件(primary header

    #import <UIKit/UIKit.h>
    
    @interface DemoChatViewController : UIViewController
    @property (nonatomic, assign, readonly) DemoChatType type;
    
    /**
     利用聊天类型来创建新的聊天控制器
    
     @param type 聊天ID类型
     @return 新创建的聊天控制器
     */
     - (instancetype)initWithChatType:(DemoChatType)type NS_DESIGNATED_INITIALIZER;
     @end
    

    在其他类导入该头文件时,只能看到一个公开的属性和方法。这样使得这个控制器的实现被很好地隐藏了起来。

  • DemoChatViewController.m为主实现文件(primary implementation)

    @interface DemoChatViewController ()
    /* 视图 */
    @property (nonatomic, weak) UITableView *chatTable; //消息列表
    @property (nonatomic, strong) NSMutableArray *cellData; //消息数据
    /* 其他 */
    @property (nonatomic, assign) DemoChatType type;
    @end
    
    @implementation DemoChatViewController
     - (instancetype)initWithChatType:(DemoChatType)type {
     /* implementation ... */
     }
    @end
    

    在该类的实现文件上部,包括了该类的扩展。扩展中重写了Private分类头文件以及主头文件中的属性。

  • DemoChatViewController+Private.h为私有头文件

    #import "DemoChatViewController.h"
    @interface DemoChatViewController (Private)
    /* 视图 */
    @property (nonatomic, readonly, weak) UITableView *chatTable; //消息列表
    @property (nonatomic, strong) NSMutableArray *cellData; //消息数据
    

    方便该类的各个分类之间使用该类定义的属性。

  • DemoChatViewController+GroupChat.h为群聊分类头文件

    #import "DemoChatViewController.h"
    
    @interface DemoChatViewController (DemoGroupChat)
    
    /**
    首次加载群聊消息
    */
    - (void)loadGroupChatMessages;
    
    /* 
    other methods
    ...
    */
    @end
    

    定义群聊功能分类的头文件。

  • DemoChatViewController+GroupChat.h为群聊分类实现文件

    #import "DemoChatViewController+DemoGroupChat.h"
    
    @implementation DemoChatViewController (DemoGroupChat)
    - (void)loadGroupChatMessages {
    /* implementation */
    }
    

    实现群聊的功能。