MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

了解Objective-C类别(Category)的语法与作用

2021-08-096.3k 阅读

一、Objective - C 类别(Category)的语法

(一)类别定义的基本语法结构

在Objective - C中,定义一个类别(Category)主要由@interface@implementation两部分组成。其基本语法格式如下:

// 类别接口定义
@interface ClassName (CategoryName)
// 在此声明新的方法
@end

// 类别实现
@implementation ClassName (CategoryName)
// 实现刚才声明的方法
@end

其中,ClassName是你要为其添加类别的原始类的名称,CategoryName是你给这个类别起的名字。需要注意的是,类别只能为类添加方法,不能添加实例变量。

(二)方法声明与实现

  1. 方法声明 在类别接口部分,声明方法的方式和在类的接口中声明方法类似。例如,我们为NSString类创建一个类别,添加一个判断字符串是否为数字字符串的方法:
@interface NSString (NumberCheck)
- (BOOL)isPureNumber;
@end

这里声明了一个实例方法isPureNumber,返回一个布尔值,用于判断调用该方法的NSString实例是否只包含数字字符。

  1. 方法实现 在类别实现部分,实现刚才声明的方法。实现的语法和类的方法实现基本一致:
@implementation NSString (NumberCheck)
- (BOOL)isPureNumber {
    NSCharacterSet *nonNumberSet = [[NSCharacterSet decimalDigitCharacterSet] invertedSet];
    return [self rangeOfCharacterFromSet:nonNumberSet].location == NSNotFound;
}
@end

在这个实现中,我们利用NSCharacterSet来获取非数字字符集,然后检查字符串中是否存在非数字字符。如果不存在,则返回YES,表示该字符串是纯数字字符串。

(三)类别与原类方法的关系

  1. 方法优先级 当类别中的方法与原类中的方法重名时,类别中的方法会覆盖原类的方法。例如,假设NSString原类中有一个description方法,我们在类别中也定义一个description方法:
@interface NSString (CustomDescription)
- (NSString *)description;
@end

@implementation NSString (CustomDescription)
- (NSString *)description {
    return [NSString stringWithFormat:@"Custom description: %@", self];
}
@end

当我们调用NSString实例的description方法时,实际调用的是类别中定义的这个方法,而不是原类的description方法。

  1. 调用原类被覆盖的方法 在类别中,如果想要调用原类被覆盖的方法,可以通过super关键字,但这需要一些技巧。因为类别没有继承关系,所以不能直接使用super。一种常见的做法是使用运行时(Runtime)机制。例如,我们可以通过class_getInstanceMethod函数获取原类的方法实现,然后通过objc_msgSendSuper函数来调用它:
#import <objc/runtime.h>

@interface NSString (CallOriginalDescription)
- (NSString *)description;
@end

@implementation NSString (CallOriginalDescription)
- (NSString *)description {
    Method originalMethod = class_getInstanceMethod([super class], @selector(description));
    IMP originalIMP = method_getImplementation(originalMethod);
    NSString *(*originalFunc)(id, SEL) = (void *)originalIMP;
    return originalFunc(self, @selector(description));
}
@end

在这个例子中,我们通过运行时函数获取了NSString原类的description方法的实现,并调用它。这样就可以在类别中调用原类被覆盖的方法。

二、Objective - C 类别(Category)的作用

(一)将类的实现分开到多个文件中

  1. 大型类的模块化管理 对于一些大型的类,其实现代码可能非常冗长。如果将所有代码都放在一个.m文件中,会使得文件难以阅读和维护。通过使用类别,可以将类的不同功能模块分开到不同的文件中。

例如,假设我们有一个Person类,包含很多功能,如基本信息操作、社交信息操作、工作信息操作等。我们可以为Person类创建不同的类别来分别实现这些功能:

// Person+BasicInfo.m
@interface Person (BasicInfo)
- (void)setName:(NSString *)name;
- (NSString *)name;
@end

@implementation Person (BasicInfo)
- (void)setName:(NSString *)name {
    _name = name;
}
- (NSString *)name {
    return _name;
}
@end

// Person+SocialInfo.m
@interface Person (SocialInfo)
- (void)addFriend:(Person *)friend;
- (NSArray *)friends;
@end

@implementation Person (SocialInfo)
- (void)addFriend:(Person *)friend {
    if (!_friends) {
        _friends = [NSMutableArray array];
    }
    [_friends addObject:friend];
}
- (NSArray *)friends {
    return _friends;
}
@end

// Person+WorkInfo.m
@interface Person (WorkInfo)
- (void)setJob:(NSString *)job;
- (NSString *)job;
@end

@implementation Person (WorkInfo)
- (void)setJob:(NSString *)job {
    _job = job;
}
- (NSString *)job {
    return _job;
}
@end

这样,每个类别文件专注于实现一类功能,使得代码结构更加清晰,便于维护和扩展。

  1. 团队协作开发 在团队开发中,不同的开发人员可以负责不同的类别文件。比如,开发人员A负责Person类的基本信息相关功能,就可以专注于Person+BasicInfo.m文件;开发人员B负责社交信息相关功能,就可以在Person+SocialInfo.m文件中进行开发。这样可以避免多人同时修改一个大文件导致的代码冲突,提高开发效率。

(二)为系统类添加自定义方法

  1. 扩展系统类功能 Objective - C的系统类已经提供了丰富的功能,但在实际开发中,我们可能还需要一些额外的功能。通过类别,我们可以为系统类添加自定义方法。

例如,UIView类是iOS开发中视图的基础类。我们可以为UIView类添加一个方法,用于快速设置其圆角半径:

@interface UIView (CornerRadius)
- (void)setCornerRadius:(CGFloat)radius;
@end

@implementation UIView (CornerRadius)
- (void)setCornerRadius:(CGFloat)radius {
    self.layer.cornerRadius = radius;
    self.layer.masksToBounds = YES;
}
@end

这样,在任何UIView及其子类(如UILabelUIButton等)的实例上,我们都可以直接调用setCornerRadius:方法来设置圆角半径,而无需每次都重复编写设置layer.cornerRadiuslayer.masksToBounds的代码。

  1. 提高代码复用性 通过为系统类添加类别方法,我们可以将一些常用的功能封装起来,提高代码的复用性。例如,我们可以为NSDate类添加一个方法,用于获取当前日期是星期几:
@interface NSDate (Weekday)
- (NSString *)weekdayString;
@end

@implementation NSDate (Weekday)
- (NSString *)weekdayString {
    NSDateComponents *components = [[NSCalendar currentCalendar] components:NSCalendarUnitWeekday fromDate:self];
    NSInteger weekday = components.weekday;
    NSArray *weekdayStrings = @[@"Sunday", @"Monday", @"Tuesday", @"Wednesday", @"Thursday", @"Friday", @"Saturday"];
    return weekdayStrings[weekday - 1];
}
@end

这样,在任何需要获取日期对应的星期几的地方,我们都可以直接调用weekdayString方法,而无需在每个地方都编写获取日期组件并转换为星期字符串的代码。

(三)实现非正式协议(Informal Protocol)

  1. 非正式协议的概念 非正式协议在Objective - C中是一种通过类别来模拟协议的方式。它不像正式协议(@protocol)那样强制类必须实现某些方法,而是提供了一种约定俗成的方式,让类可以选择性地实现一些方法。

例如,在iOS开发中,UITableViewDataSourceUITableViewDelegate是正式协议,要求实现某些方法来配置和管理表格视图。但有时候,我们可能希望有一些更灵活的方式,让类可以选择性地提供某些功能。这时就可以使用非正式协议。

  1. 通过类别实现非正式协议 假设我们有一个MyTableView类,我们希望其他类可以为它提供一些自定义的行为,但又不想使用正式协议那么严格的方式。我们可以通过类别来实现:
// MyTableView.h
@interface MyTableView : UIView
// 一些基本的属性和方法
@end

// MyTableView+CustomBehavior.h
@interface MyTableView (CustomBehavior)
- (void)customizeCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath;
@end

// 某个类实现这个非正式协议的方法
@interface MyViewController : UIViewController <MyTableViewDelegate>
@end

@implementation MyViewController
- (void)customizeCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
    // 在这里自定义单元格的外观或行为
    cell.textLabel.textColor = [UIColor redColor];
}
@end

在这个例子中,MyTableView通过类别CustomBehavior定义了一个方法customizeCell:atIndexPath:,这就是一个非正式协议的方法。其他类(如MyViewController)可以选择实现这个方法,来为MyTableView提供自定义行为。

(四)模拟多继承

  1. Objective - C中的继承限制 在Objective - C中,类只支持单继承,即一个类只能有一个直接父类。这在某些情况下可能会限制代码的灵活性,因为我们可能希望一个类从多个类中获取不同的功能。

  2. 通过类别模拟多继承 通过类别,我们可以在一定程度上模拟多继承的效果。例如,我们有两个类FlyableSwimmable,分别表示具有飞行和游泳能力的类:

@interface Flyable : NSObject
- (void)fly;
@end

@implementation Flyable
- (void)fly {
    NSLog(@"I can fly.");
}
@end

@interface Swimmable : NSObject
- (void)swim;
@end

@implementation Swimmable
- (void)swim {
    NSLog(@"I can swim.");
}
@end

现在假设我们有一个Duck类,它既需要飞行能力,又需要游泳能力。由于Objective - C不支持多继承,我们可以通过类别来实现:

@interface Duck : NSObject
// Duck类本身的属性和方法
@end

@interface Duck (Fly) <Flyable>
- (void)fly;
@end

@implementation Duck (Fly)
- (void)fly {
    NSLog(@"Duck can fly.");
}
@end

@interface Duck (Swim) <Swimmable>
- (void)swim;
@end

@implementation Duck (Swim)
- (void)swim {
    NSLog(@"Duck can swim.");
}
@end

这样,Duck类通过类别分别实现了FlyableSwimmable类的功能,在一定程度上模拟了多继承。但需要注意的是,这和真正的多继承还是有区别的,比如类别不能继承实例变量,而且方法的调用和冲突处理也有不同。

三、类别使用中的注意事项

(一)方法命名冲突

  1. 冲突风险 由于类别可以为类添加方法,当多个类别为同一个类添加同名方法时,就会产生方法命名冲突。例如,我们有两个类别NSString+Extension1NSString+Extension2,都为NSString类添加了一个print方法:
@interface NSString (Extension1)
- (void)print;
@end

@implementation NSString (Extension1)
- (void)print {
    NSLog(@"Print from Extension1: %@", self);
}
@end

@interface NSString (Extension2)
- (void)print;
@end

@implementation NSString (Extension2)
- (void)print {
    NSLog(@"Print from Extension2: %@", self);
}
@end

当在代码中调用NSString实例的print方法时,最终调用的是哪个类别的print方法是不确定的,这取决于编译和链接的顺序。

  1. 避免冲突的方法 为了避免方法命名冲突,在为类别命名方法时,应该使用一个独特的前缀。例如,在公司开发项目时,可以使用公司名称的缩写作为前缀。比如,如果公司名称是ABC,那么类别方法可以命名为abc_print,这样可以大大降低命名冲突的风险。

(二)实例变量的局限性

  1. 不能直接添加实例变量 类别只能为类添加方法,不能直接添加实例变量。这是因为类别在运行时的结构决定了它没有自己的实例变量存储空间。例如,我们不能在类别中这样定义实例变量:
@interface NSString (InvalidIVar)
// 以下是无效的,不能在类别中直接定义实例变量
NSString *extraInfo;
@end
  1. 替代方案 如果确实需要在类别中添加类似实例变量的功能,可以使用关联对象(Associated Objects)。关联对象是通过运行时函数来为对象动态添加属性的一种机制。例如,我们为NSString类添加一个关联对象来存储额外信息:
#import <objc/runtime.h>

@interface NSString (ExtraInfo)
- (void)setExtraInfo:(NSString *)info;
- (NSString *)extraInfo;
@end

@implementation NSString (ExtraInfo)
static const char *kExtraInfoKey = "extraInfoKey";
- (void)setExtraInfo:(NSString *)info {
    objc_setAssociatedObject(self, kExtraInfoKey, info, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)extraInfo {
    return objc_getAssociatedObject(self, kExtraInfoKey);
}
@end

在这个例子中,我们通过objc_setAssociatedObjectobjc_getAssociatedObject函数来设置和获取关联对象,从而实现了类似在类别中添加实例变量的功能。

(三)类别与继承的关系

  1. 类别不会影响继承体系 类别是在运行时动态地为类添加方法,它不会改变类的继承体系。例如,SubClass继承自SuperClass,我们为SuperClass添加一个类别,这个类别中的方法对于SubClass来说,就如同SuperClass本身的方法一样,可以被继承和重写。
@interface SuperClass : NSObject
@end

@interface SuperClass (ExtraMethod)
- (void)extraMethod;
@end

@implementation SuperClass (ExtraMethod)
- (void)extraMethod {
    NSLog(@"Extra method in SuperClass.");
}
@end

@interface SubClass : SuperClass
@end

@implementation SubClass
// 可以重写类别中的方法
- (void)extraMethod {
    NSLog(@"Overridden extra method in SubClass.");
}
@end
  1. 注意方法覆盖与调用 当子类重写了类别为父类添加的方法时,调用子类实例的该方法会执行子类的实现。但如果子类没有重写,就会执行类别中的方法。同时,在类别中调用原类方法时,需要注意避免递归调用导致的栈溢出等问题。

通过深入了解Objective - C类别(Category)的语法与作用,以及在使用中需要注意的事项,我们可以更好地利用这一特性来优化我们的代码结构,提高代码的复用性和可维护性。无论是在大型项目开发中,还是在日常的代码编写中,合理运用类别都能为我们带来很多便利。