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

解析Objective-C协议(Protocol)的语法与应用

2022-02-015.2k 阅读

一、Objective-C 协议基础概念

1.1 协议的定义

在Objective-C中,协议(Protocol)是一种特殊的接口形式,它定义了一系列方法的声明,但不包含方法的实现。协议提供了一种方式,让不同类的对象能够遵守一组共同的方法约定。通过协议,一个类可以表明自己能够执行某些特定的任务,即使这些类之间没有直接的继承关系。

例如,我们定义一个简单的协议 Flyable,表示具有飞行能力的对象:

@protocol Flyable <NSObject>
- (void)fly;
@end

在上述代码中,使用 @protocol 关键字定义了一个名为 Flyable 的协议。<NSObject> 表示这个协议继承自 NSObject 协议,NSObject 协议定义了一些基本的方法,如 descriptionhash 等,几乎所有的Objective-C类都直接或间接遵守 NSObject 协议。协议中声明了一个 - (void)fly; 方法,表示遵守该协议的对象应该实现这个飞行方法。

1.2 协议的分类

1.2.1 非正式协议(Deprecated)

早期的Objective-C中有非正式协议的概念,它通过向 NSObject 类别(Category)添加方法声明来实现。例如:

@interface NSObject (MyInformalProtocol)
- (void)mySpecialMethod;
@end

任何类只要包含了这个类别,就被认为遵守了这个非正式协议。然而,非正式协议存在一些缺点,比如编译器无法在编译期检查类是否实现了协议方法,并且在代码可读性和维护性方面较差。随着Objective-C的发展,非正式协议已经被正式协议和类别所取代,在现代Objective-C开发中不建议使用。

1.2.2 正式协议

正式协议使用 @protocol 关键字定义,如前面的 Flyable 协议示例。正式协议具有严格的语法和编译器检查机制,能确保遵守协议的类实现了协议中声明的方法(除非这些方法被标记为可选)。正式协议是Objective-C中实现多态和代码复用的重要手段之一。

二、协议的语法细节

2.1 协议的继承

协议可以继承自其他协议,通过继承,新协议会包含被继承协议的所有方法声明。例如:

@protocol Moveable <NSObject>
- (void)move;
@end

@protocol Flyable <Moveable>
- (void)fly;
@end

这里 Flyable 协议继承自 Moveable 协议,所以遵守 Flyable 协议的类不仅要实现 fly 方法,还需要实现 move 方法。协议继承可以形成一个层次结构,有助于组织和管理不同功能的方法集合。

2.2 可选方法与必需方法

在协议中,方法可以分为可选方法和必需方法。必需方法要求遵守协议的类必须实现,而可选方法则可以选择实现。

2.2.1 必需方法

默认情况下,协议中声明的方法都是必需方法。例如在前面的 Flyable 协议中,- (void)fly; 就是一个必需方法,任何遵守 Flyable 协议的类都必须实现这个方法,否则编译器会报错。

2.2.2 可选方法

要声明可选方法,需要在协议中使用 @optional 关键字。例如:

@protocol Flyable <NSObject>
- (void)fly;
@optional
- (void)land;
@end

在这个协议中,- (void)land; 是一个可选方法,遵守 Flyable 协议的类可以选择实现这个方法。当我们调用可选方法时,需要先检查对象是否实现了该方法,以避免运行时错误。可以使用 respondsToSelector: 方法来检查,示例代码如下:

@interface Bird : NSObject <Flyable>
@end

@implementation Bird
- (void)fly {
    NSLog(@"Bird is flying.");
}
@end

Bird *bird = [[Bird alloc] init];
if ([bird respondsToSelector:@selector(land)]) {
    [bird land];
}

在上述代码中,Bird 类遵守了 Flyable 协议并实现了 fly 方法,但没有实现 land 方法。在调用 land 方法之前,通过 respondsToSelector: 检查对象是否实现了该方法,这样可以避免因调用未实现的方法而导致程序崩溃。

2.3 协议的声明位置

协议可以在不同的位置声明,常见的有以下几种:

2.3.1 在头文件中声明

将协议声明在头文件中是最常见的做法,这样可以让其他类方便地引入并遵守该协议。例如:

// Flyable.h
@protocol Flyable <NSObject>
- (void)fly;
@end

在其他类中,可以通过 #import "Flyable.h" 引入该协议,并让类遵守它:

// Bird.h
#import "Flyable.h"

@interface Bird : NSObject <Flyable>
@end

这种方式使得协议具有良好的可复用性和模块性,不同的类可以根据需要引入并遵守协议。

2.3.2 在类的接口中声明

协议也可以在类的接口中声明,这样声明的协议通常与该类有紧密的关联。例如:

@interface Animal : NSObject
@protocol Eatable <NSObject>
- (void)eat;
@end
@end

在这种情况下,Eatable 协议是 Animal 类接口的一部分。其他类如果要遵守这个协议,需要在引入包含 Animal 类接口的头文件后,像下面这样遵守协议:

@interface Dog : NSObject <Animal::Eatable>
@end

这里使用 Animal::Eatable 来指定遵守 Animal 类中声明的 Eatable 协议。

2.3.3 在类的实现文件中声明

协议还可以在类的实现文件(.m 文件)中声明,这种方式声明的协议作用范围仅限于该实现文件内部。例如:

// Animal.m
#import "Animal.h"

@protocol PrivateProtocol <NSObject>
- (void)privateMethod;
@end

@interface Animal () <PrivateProtocol>
@end

@implementation Animal
- (void)privateMethod {
    NSLog(@"This is a private method.");
}
@end

在上述代码中,PrivateProtocol 协议在 Animal.m 文件中声明,并且 Animal 类的私有接口遵守了这个协议。这个协议和它的方法对于其他文件是不可见的,常用于实现类内部的一些特定功能和协作。

三、协议的应用场景

3.1 实现多态

协议是实现多态的重要手段之一。通过让不同的类遵守同一个协议,我们可以以统一的方式处理这些类的对象。例如,我们定义一个 Flyable 协议,有 BirdAirplane 两个类都遵守这个协议:

@protocol Flyable <NSObject>
- (void)fly;
@end

@interface Bird : NSObject <Flyable>
@end

@implementation Bird
- (void)fly {
    NSLog(@"Bird is flying.");
}
@end

@interface Airplane : NSObject <Flyable>
@end

@implementation Airplane
- (void)fly {
    NSLog(@"Airplane is flying.");
}
@end

// 使用协议实现多态
NSArray<id<Flyable>> *flyers = @[[[Bird alloc] init], [[Airplane alloc] init]];
for (id<Flyable> flyer in flyers) {
    [flyer fly];
}

在上述代码中,NSArray<id<Flyable>> 表示一个包含遵守 Flyable 协议对象的数组。通过遍历这个数组,我们可以调用每个对象的 fly 方法,尽管 BirdAirplane 类没有继承关系,但由于它们都遵守了 Flyable 协议,所以可以以统一的方式处理它们的飞行行为,这就是多态的体现。

3.2 代理模式

代理模式是协议在Objective-C中非常常见的应用场景。在代理模式中,一个对象(代理对象)代表另一个对象(委托对象)处理某些任务。通过协议,委托对象定义了代理对象需要实现的方法。

例如,我们有一个 ViewController 类,它需要在某个操作完成后通知另一个对象。我们可以定义一个协议和代理属性:

@protocol ViewControllerDelegate <NSObject>
@optional
- (void)viewControllerDidFinishTask:(ViewController *)controller;
@end

@interface ViewController : UIViewController
@property (nonatomic, weak) id<ViewControllerDelegate> delegate;
@end

@implementation ViewController
- (void)performTask {
    // 执行任务
    if ([self.delegate respondsToSelector:@selector(viewControllerDidFinishTask:)]) {
        [self.delegate viewControllerDidFinishTask:self];
    }
}
@end

然后,我们有另一个类 AnotherClass 遵守这个协议并作为代理:

@interface AnotherClass : NSObject <ViewControllerDelegate>
@end

@implementation AnotherClass
- (void)viewControllerDidFinishTask:(ViewController *)controller {
    NSLog(@"ViewController finished task.");
}
@end

在使用时,我们可以将 AnotherClass 的实例设置为 ViewController 的代理:

ViewController *vc = [[ViewController alloc] init];
AnotherClass *delegate = [[AnotherClass alloc] init];
vc.delegate = delegate;
[vc performTask];

这样,当 ViewController 完成任务时,会通过代理调用 AnotherClass 中实现的协议方法,实现了对象之间的解耦和事件传递。

3.3 数据传递与通信

在iOS开发中,不同视图控制器之间的数据传递和通信是常见的需求。协议可以有效地实现这一功能。例如,我们有一个 DetailViewController 用于显示详细信息,MainViewController 用于导航到 DetailViewController 并接收从 DetailViewController 返回的数据。

首先,在 DetailViewController 中定义协议和代理属性:

@protocol DetailViewControllerDelegate <NSObject>
- (void)detailViewController:(DetailViewController *)controller didSelectData:(id)data;
@end

@interface DetailViewController : UIViewController
@property (nonatomic, weak) id<DetailViewControllerDelegate> delegate;
@end

@implementation DetailViewController
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    id data = // 获取选中的数据
    if ([self.delegate respondsToSelector:@selector(detailViewController:didSelectData:)]) {
        [self.delegate detailViewController:self didSelectData:data];
    }
}
@end

然后,在 MainViewController 中遵守协议并设置为代理:

@interface MainViewController : UIViewController <DetailViewControllerDelegate>
@end

@implementation MainViewController
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    if ([segue.identifier isEqualToString:@"ShowDetail"]) {
        DetailViewController *detailVC = segue.destinationViewController;
        detailVC.delegate = self;
    }
}

- (void)detailViewController:(DetailViewController *)controller didSelectData:(id)data {
    // 处理返回的数据
    NSLog(@"Received data: %@", data);
}
@end

通过这种方式,当在 DetailViewController 中用户选择了数据时,会通过协议将数据传递给 MainViewController,实现了不同视图控制器之间的数据通信。

3.4 组件复用与扩展

协议有助于实现组件的复用和扩展。例如,我们开发了一个通用的图表绘制组件,不同的业务场景可能需要不同的图表样式和交互。我们可以通过协议定义一些可定制的方法,让使用该组件的类根据自身需求进行实现。

@protocol ChartCustomization <NSObject>
@optional
- (UIColor *)chartBackgroundColor;
- (UIColor *)chartLineColor;
- (void)chartDidSelectPoint:(CGPoint)point;
@end

@interface ChartView : UIView
@property (nonatomic, weak) id<ChartCustomization> customizationDelegate;
@end

@implementation ChartView
- (void)drawRect:(CGRect)rect {
    UIColor *bgColor = [self.customizationDelegate chartBackgroundColor];
    if (bgColor) {
        [bgColor setFill];
        UIRectFill(rect);
    }
    // 其他绘制代码
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    UITouch *touch = touches.anyObject;
    CGPoint point = [touch locationInView:self];
    if ([self.customizationDelegate respondsToSelector:@selector(chartDidSelectPoint:)]) {
        [self.customizationDelegate chartDidSelectPoint:point];
    }
}
@end

在不同的业务类中,可以遵守 ChartCustomization 协议并实现相应方法来定制图表的行为和样式:

@interface FinancialChartViewController : UIViewController <ChartCustomization>
@end

@implementation FinancialChartViewController
- (UIColor *)chartBackgroundColor {
    return [UIColor lightGrayColor];
}

- (UIColor *)chartLineColor {
    return [UIColor blueColor];
}

- (void)chartDidSelectPoint:(CGPoint)point {
    NSLog(@"Financial chart point selected: %@", NSStringFromCGPoint(point));
}
@end

这样,通过协议,图表组件可以在不同的业务场景中复用,并且根据具体需求进行扩展和定制。

四、协议与其他语言特性的关系

4.1 协议与继承

4.1.1 区别

继承是一种类与类之间的父子关系,子类继承父类的属性和方法,并且可以重写父类的方法。而协议是一种行为约定,不同类之间通过遵守协议来表明它们具有某些共同的行为,这些类之间不一定有继承关系。

例如,Bird 类和 Airplane 类没有继承关系,但它们都可以遵守 Flyable 协议来表示具有飞行能力。而在继承体系中,Dog 类继承自 Animal 类,Dog 类会自动拥有 Animal 类的属性和方法。

4.1.2 结合使用

在实际开发中,继承和协议常常结合使用。一个类可以在继承体系中扮演特定的角色,同时通过遵守协议来扩展其功能。例如,UIViewController 类继承自 UIResponder 类,同时遵守了许多协议,如 UITableViewDataSourceUITableViewDelegate 等,使得 UIViewController 类既具有基本的响应者功能,又能处理表格视图的数据和交互。

4.2 协议与类别

4.2.1 区别

类别(Category)是为已有的类添加方法的一种方式,它可以在不继承类的情况下为类增加功能。但类别不能添加实例变量,并且如果类别中声明的方法与类本身或其他类别中的方法同名,会产生覆盖问题。

而协议只是定义方法声明,不包含方法实现,遵守协议的类需要自己实现协议方法。协议更侧重于定义一种行为规范,不同类通过遵守协议来实现多态。

4.2.2 结合使用

类别可以用来为遵守协议的类提供默认的方法实现。例如,我们有一个 MyProtocol 协议和一个 MyClass 类遵守该协议:

@protocol MyProtocol <NSObject>
- (void)myMethod;
@end

@interface MyClass : NSObject <MyProtocol>
@end

@implementation MyClass
// 可以不实现myMethod方法
@end

@interface MyClass (MyProtocolDefaultImpl) <MyProtocol>
@end

@implementation MyClass (MyProtocolDefaultImpl)
- (void)myMethod {
    NSLog(@"Default implementation of myMethod.");
}
@end

在这个例子中,MyClass 类本身可以不实现 myMethod 方法,而通过类别 MyClass (MyProtocolDefaultImpl) 提供了默认实现。这样,既利用了协议的规范作用,又通过类别提供了方便的默认实现,增强了代码的灵活性。

五、协议的高级特性与注意事项

5.1 协议的类型限定

在声明变量、属性或方法参数时,可以使用协议进行类型限定。例如:

id<Flyable> flyer;
@property (nonatomic, strong) id<Flyable> myFlyer;
- (void)handleFlyer:(id<Flyable>)flyer;

这样可以确保变量、属性或参数是遵守特定协议的对象,提高代码的类型安全性。同时,也可以使用多个协议进行类型限定,用逗号分隔:

id<Flyable, Moveable> object;

表示 object 必须同时遵守 FlyableMoveable 协议。

5.2 协议的泛型

从Objective-C 2.2开始,引入了泛型支持,协议也可以与泛型结合使用。例如:

@protocol Collection <NSObject>
@property (nonatomic, strong) NSArray<id> *items;
@end

@interface MyCollection : NSObject <Collection>
@end

@implementation MyCollection
@property (nonatomic, strong) NSArray<id> *items;
@end

在上述代码中,Collection 协议定义了一个 items 属性,类型为 NSArray<id>。这里的 id 表示任意类型。如果我们希望更具体地限定 items 数组中的元素类型,可以使用泛型:

@protocol Collection <NSObject>
@property (nonatomic, strong) NSArray<id<T>> *items;
@end

@interface MyCollection : NSObject <Collection>
@end

@implementation MyCollection
@property (nonatomic, strong) NSArray<id<T>> *items;
@end

这里的 <T> 是一个类型参数,在使用 MyCollection 时,可以指定具体的类型来替换 T,如 MyCollection<String *> *stringCollection;,表示 stringCollectionitems 数组中只能包含 NSString 对象。

5.3 注意事项

5.3.1 方法命名冲突

在定义协议方法时,要注意避免与其他类或协议中的方法命名冲突。特别是在大型项目中,不同模块可能定义了相似功能的协议,如果方法命名相同,可能会导致难以调试的问题。可以通过使用前缀或遵循一定的命名规范来减少冲突的可能性。

5.3.2 协议版本控制

当协议发生变化时,需要考虑协议的版本控制。如果在已发布的协议中添加了必需方法,可能会导致现有的遵守该协议的类编译失败。一种解决方法是使用可选方法来逐步引入新功能,或者在新协议版本中继承旧协议,并在新协议中声明新的必需方法,让需要使用新功能的类遵守新协议。

5.3.3 内存管理

在使用协议和代理时,要注意内存管理问题。特别是在代理模式中,通常将代理属性声明为 weak,以避免循环引用导致的内存泄漏。例如,在前面的 ViewControllerAnotherClass 的代理示例中,ViewControllerdelegate 属性声明为 weak,防止 ViewControllerAnotherClass 之间相互持有对方,从而造成内存泄漏。