# 类别（Category）、拓展（Extension）

> 很多人都知道类别、分类的用法，但是对于一些细节就不是很清楚了，本文主要梳理下这3个概念的细节

## 类别（Category）

### 文件特征
  - 类别文件有2个，分别为 .h 和 .m
  - 命名为： “类名+类别名.h”和“类名+类别名.m”

### 文件内容格式
.h 文件格式
```
#import "类名.h"

@interface 类名 (类别名)
// 在此处声明方法
@end
```

.m 文件格式
```
#import "类名+类别名.h"

@implementation 类名 (类别名)
// 在此处实现声明的方法
@end
```

### 类别的作用

拓展当前类，为类添加方法

### 类别的局限性
- 无法向现有的类添加实例变量（编译器报“instance variables may not be placed in categories”）。Category 一般只为类提供方法的拓展，不提供属性的拓展。但是利用 Runtime 可以在 Category 中添加属性

- 方法名称冲突的情况下，如果 Category 中的方法与当前类的方法名称重名，Category 具有更高的优先级，类别中的方法将完全取代现有类中的方法（调用方法的时候不会去调用现有类里面的方法实现）。

- 当现有类具有多个 Category 的时候，如果每个 Category 都有同名的方法，那么在调用方法的时候肯定不会调用现有类的方法实现。系统根据编译顺序决定调用哪个 Category 下的方法实现。（可以在 Targets -> Build phases -> Compile Sources 下给多个 Category 更换顺序看看到底在执行哪个方法）


### Category 的使用和注意
1. Category 中的方法如果和现有类方法一致，工程中任何调用当前类的方法的时候都会去调用 Category 里面的方法（比如：UIViewCtroller、UITableView这些）的方法时要慎重。因为用Category重写类中的方法会对子类造成很大的影响。比如：用Category 重写了 UIViewCtroller 的方法 A，那么如果你在工程中用到的所有继承自 UIViewCtroller 的子类，去调用方法 A 时，执行的都是 Category 中重写的方法 A,如果不幸的是，你写的方法 A 有 Bug，那么会造成整个工程中调用该方法的所有 UIViewCtroller 子类的不正常。除非你在子类中重写了父类的方法 A，这样子类调用方法 A 时是调用的自己重写的方法 A，消除了父类 Category 中重写方法对自己的影响

2. Category拓展方法按照有没有重写当前类中的方法，分为未重写的拓展方法和重写拓展方法。且类引用自己的 Category 时，只能在 .m 文件中引用（.h 文件引用自己的类别会报错）。子类引用父类的 Category 在 .h 或 .m 都可以。如果类调用 Category 中重写的方法，不用引入 Category 头文件，系统会自动调用 Category 中的重写方法

3. Category 中如果重写了 A 类从父类继承来的某方法，不会影响与 A 同层级的 B 类

4. 子类会不会继承父类的 Category： Category 中重写的方法会对子类造成影响，但是子类不会继承非重写的方法（现有类中没有的方法）。但是在子类中引入父类 Category 的声明文件后，子类就会继承 Category 的非重写方法。继承的表现是：当子类的方法和父类 Category 中的方法名完全相同，那么子类里的方法会覆盖掉父类 Category，相当于子类重写了继承自父类的方法

5. Category 的作用是向下有效的。即只会影响到该类的所有子类。比如 A 类和 B 类是继承自 Super 类的2个子类，当给 A 类添加一个 Category sayHello 方法，仅有A 类的子类才可以使用 sayHello 方法

## 拓展（Extension）
 
### 文件特征
- 只存在一个文件
- 命名方式：“类名_拓展名.h”
```
#import "类名.h"

@interface 类名 ()
// 在此添加私有成员变量、属性、声明方法
@end
```

### 拓展的作用
1. 为类增加额外的属性、成员变量、方法声明

2. 一般将类拓展直接写到当前类的 .m 文件中。不单独创建

3. 一般私有的属性和方法写到类拓展中

4. 和 Category 类似，但是小括号里面没有拓展的名字


### 拓展的局限性
1. Extension 中添加的属性、成员变量、方法属于私有（只可以在本类的 .m 文件中访问、调用。在其他类里面是无法访问的，同时子类也是无法继承的）。假如我们有这样一个需求，一个属性对外是只读的，对内是可以读写的，那么我们可以通过 Extension 实现。

2. 通常 Extension 都写在 .m 文件中，不会单独建立一个 Extension 文件。而且 Extension 必须写到 @implementation 上方，否则编译报错

3. 类拓展定义的方法和属性必须在类的实现文件中实现。如果单独定义类扩展的文件并且只定义属性的话，也需要将类实现文件中包含进类扩展文件，否则会找不到属性的 setter 和 getter 方法。

```
//Web.h
#import "Person.h"
NS_ASSUME_NONNULL_BEGIN
@interface Web : Person
@end
NS_ASSUME_NONNULL_END

//Web.m
#import "Web.h"
#import "Web+H5.h"
@interface Web ()
@property (nonatomic, strong) NSString *skillStacks;
@end
@implementation Web
- (void)test {
    self.skills =  @"iOS && Web && Node && Hybrid";
    self.skillStacks = @"iOS && Web && Node && Hybrid";
}
- (void)show {
    NSLog(@"%@",self.skillStacks);
}
@end
```


## 总结
1. Category 只能拓充方法，不能拓展属性和成员变量（包含成员变量会报错。属性虽然不可以直接拓展，利用 Runtime 可以实现）

2. 如果 Category 中声明了1个属性，那么 Category 只会生成 setter 和 getter 的声明，不会有实现

3. Extension 也被成为匿名的 Category

4. 分类的方法本质是追加在当前类方法列表后，所以分类的方法会覆盖当前类的方法。


##「小插曲」：为 Category 实现属性的 Setter 和 Getter
```
#import "Person.h"

NS_ASSUME_NONNULL_BEGIN

@interface Person (Student)


/**< 学号*/
@property (nonatomic, strong) NSString *studyNumber;

@end

NS_ASSUME_NONNULL_END


#import "Person+Student.h"
#import <objc/message.h>

@implementation Person (Student)

- (void)sayHi {
    NSLog(@"大家好，我叫%@，我今年%zd岁了",self.name,self.age);
}


/*
 * 传统的做法是在 setter 里面这样写
 _studyNumber = studyNumber;
 ARC 自动管理内存
 MRC
 [_studyNumber release];
 [studyNumber retain];
 _studyNumber = studyNumber;
 
 但是在 Category里面不会生成对应的实例变量，因此我们可以利用 Runtime 为我们的 category 关联属性的值
 setter:objc_setAssociatedObject(self, @selector(firstView), firstView, OBJC_ASSOCIATION_RETAIN);
 getter:objc_getAssociatedObject(self, @selector(firstView));
 }
 */
- (void)setStudyNumber:(NSString *)studyNumber {
    objc_setAssociatedObject(self, @selector(studyNumber), studyNumber
                             , OBJC_ASSOCIATION_RETAIN);
}

//@selector(studyNumber)
- (NSString *)studyNumber {
    return  objc_getAssociatedObject(self, @selector(studyNumber));
}

@end
```
说明： `objc_setAssociatedObject` 的第二个参数是`const void * _Nonnull key` 所以可以用 "studyNumber" 或者利用 `@selector()` 的特性返回的数据类型也满足，所以示例代码选用第二种方式

给分类添加属性的时候，为了避免多人开发对于属性添加造成的覆盖，我们需要为属性起一个独特的名字。比如我们的工程是组件化、模块化开展的工程，那么我们可以为属性命名的时候在前面添加当前模块的前缀。

比如我们在 Login-Register-Module 模块为 NSURL 的 Category 添加一个 title 的属性的时候，可以这样命名 LR_Title。请查看下面的代码

```Objective-c
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface NSURL (Title)

@property (nonatomic, copy) NSString *LR_title;

@end

NS_ASSUME_NONNULL_END

#import "NSURL+Title.h"
#import <objc/runtime.h>

@implementation NSURL (Title)

- (void)setLR_title:(NSString *)LR_title
{
    objc_setAssociatedObject(self, @selector(LR_title), LR_title
                             , OBJC_ASSOCIATION_RETAIN);
}

- (NSString *)LR_title
{
    return objc_getAssociatedObject(self, @selector(LR_title));
}

@end

```

 




