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

Objective-C协议(Protocol)语法规范与实现规则

2023-04-262.5k 阅读

一、Objective-C 协议概述

在 Objective-C 编程中,协议(Protocol)是一种强大的机制,它定义了一系列方法的声明,但不包含这些方法的实现。一个类可以声明遵循(adopt)某个协议,这意味着该类承诺实现协议中定义的方法。协议为不同类之间的代码复用和交互提供了一种灵活的方式,类似于其他语言中的接口概念,但又有其独特之处。

协议允许开发者创建一组方法的集合,这些方法可以被多个不相关的类实现。例如,假设我们有一个应用程序,其中有 Car 类、Airplane 类和 Boat 类。如果我们希望这些类都具有一个可以报告当前速度的功能,我们可以定义一个协议 SpeedReporting,然后让 CarAirplaneBoat 类遵循这个协议并实现报告速度的方法。这样,不同类型的交通工具虽然在本质上是不同的类,但却可以通过遵循相同的协议来提供统一的行为。

二、协议的语法规范

(一)协议的定义

在 Objective-C 中,使用 @protocol 关键字来定义协议。其基本语法如下:

@protocol ProtocolName <NSObject>
// 方法声明
@end

这里,ProtocolName 是协议的名称,你可以根据实际情况自定义。尖括号 <NSObject> 表示该协议继承自 NSObject 协议。NSObject 协议定义了一些基本的方法,如 initdescription 等,几乎所有的 Objective-C 类都遵循 NSObject 协议。如果你的协议不需要继承其他协议,可以省略尖括号及其内容。

@protocol@end 之间,你可以声明一系列的方法。方法声明的语法与类中方法声明类似,例如:

@protocol SpeedReporting <NSObject>
- (NSInteger)currentSpeed;
@end

上述代码定义了一个名为 SpeedReporting 的协议,它声明了一个实例方法 currentSpeed,该方法返回一个 NSInteger 类型的值,表示当前速度。

(二)协议方法的类型

  1. 必需方法(Required Methods) 默认情况下,协议中声明的方法都是必需方法。这意味着任何遵循该协议的类都必须实现这些方法,否则编译器会发出警告。例如:
@protocol MyProtocol <NSObject>
- (void)requiredMethod;
@end

如果一个类 MyClass 声明遵循 MyProtocol 协议,但没有实现 requiredMethod 方法,编译器会提示类似“Class 'MyClass' does not fully implement protocol 'MyProtocol'”的警告。

  1. 可选方法(Optional Methods) 有时,我们可能希望协议中的某些方法是可选的,即遵循协议的类可以选择实现或不实现这些方法。在 Objective-C 中,可以使用 @optional 关键字来标记可选方法。例如:
@protocol MyProtocol <NSObject>
- (void)requiredMethod;
@optional
- (void)optionalMethod;
@end

现在,遵循 MyProtocol 协议的类只需要实现 requiredMethod 方法,而 optionalMethod 方法是可选的。

(三)协议的继承

协议可以继承自其他协议,就像类可以继承自其他类一样。通过继承,新协议将包含父协议的所有方法声明。语法如下:

@protocol NewProtocol <OldProtocol>
// 新协议可以添加自己的方法声明
@end

例如,如果我们有一个基础协议 BasicProtocol

@protocol BasicProtocol <NSObject>
- (void)basicMethod;
@end

然后我们可以定义一个继承自 BasicProtocol 的新协议 AdvancedProtocol

@protocol AdvancedProtocol <BasicProtocol>
- (void)advancedMethod;
@end

任何遵循 AdvancedProtocol 协议的类,不仅需要实现 advancedMethod 方法,还需要实现 BasicProtocol 中的 basicMethod 方法。

三、类遵循协议

(一)声明遵循协议

一个类通过在类的接口定义中列出协议名称来声明遵循该协议。语法如下:

@interface MyClass : NSObject <MyProtocol>
// 类的其他声明
@end

这里,MyClass 类声明遵循 MyProtocol 协议。如果一个类需要遵循多个协议,可以用逗号分隔协议名称,例如:

@interface MyClass : NSObject <Protocol1, Protocol2, Protocol3>
// 类的其他声明
@end

(二)实现协议方法

当一个类声明遵循某个协议后,它必须实现协议中的所有必需方法。对于可选方法,根据实际需求决定是否实现。例如,对于前面定义的 SpeedReporting 协议:

@protocol SpeedReporting <NSObject>
- (NSInteger)currentSpeed;
@end

@interface Car : NSObject <SpeedReporting>
@property (nonatomic, assign) NSInteger speed;
@end

@implementation Car
- (NSInteger)currentSpeed {
    return _speed;
}
@end

在上述代码中,Car 类声明遵循 SpeedReporting 协议,并实现了 currentSpeed 方法,返回当前速度。

四、协议的使用场景

(一)代理模式(Delegate Pattern)

代理模式是协议最常见的应用场景之一。在代理模式中,一个对象(称为代理)负责代表另一个对象(称为委托)处理某些任务。例如,在 iOS 开发中,UITableView 会将一些事件委托给它的代理对象处理。UITableView 定义了 UITableViewDelegate 协议,其中包含了如 tableView:didSelectRowAtIndexPath: 这样的方法,用于处理用户点击表格行的事件。UITableView 的代理对象(通常是视图控制器)需要遵循 UITableViewDelegate 协议并实现相应的方法。示例代码如下:

@interface MyViewController : UIViewController <UITableViewDelegate>
@property (nonatomic, strong) UITableView *tableView;
@end

@implementation MyViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    _tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
    _tableView.delegate = self;
    [self.view addSubview:_tableView];
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    NSLog(@"You selected row %ld in section %ld", (long)indexPath.row, (long)indexPath.section);
}
@end

在这个例子中,MyViewController 类遵循 UITableViewDelegate 协议,并实现了 tableView:didSelectRowAtIndexPath: 方法,当用户点击 UITableView 中的某一行时,该方法会被调用。

(二)数据来源模式(DataSource Pattern)

与代理模式类似,数据来源模式用于为某个对象提供数据。例如,UITableView 还需要一个数据来源对象来提供表格显示的数据。UITableView 定义了 UITableViewDataSource 协议,其中包含了如 tableView:numberOfRowsInSection:tableView:cellForRowAtIndexPath: 这样的方法。数据来源对象(通常也是视图控制器)需要遵循 UITableViewDataSource 协议并实现这些方法。示例代码如下:

@interface MyViewController : UIViewController <UITableViewDataSource>
@property (nonatomic, strong) NSArray *dataArray;
@property (nonatomic, strong) UITableView *tableView;
@end

@implementation MyViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    _dataArray = @[@"Item 1", @"Item 2", @"Item 3"];
    _tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
    _tableView.dataSource = self;
    [self.view addSubview:_tableView];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return _dataArray.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"];
    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"Cell"];
    }
    cell.textLabel.text = _dataArray[indexPath.row];
    return cell;
}
@end

在上述代码中,MyViewController 类遵循 UITableViewDataSource 协议,并实现了 tableView:numberOfRowsInSection:tableView:cellForRowAtIndexPath: 方法,为 UITableView 提供了数据。

(三)多态性的实现

通过协议,我们可以实现多态性。例如,假设有多个类遵循同一个协议,我们可以将这些类的对象视为同一类型(协议类型)来处理。以下是一个简单的示例:

@protocol Shape <NSObject>
- (CGFloat)area;
@end

@interface Circle : NSObject <Shape>
@property (nonatomic, assign) CGFloat radius;
@end

@implementation Circle
- (CGFloat)area {
    return M_PI * _radius * _radius;
}
@end

@interface Rectangle : NSObject <Shape>
@property (nonatomic, assign) CGFloat width;
@property (nonatomic, assign) CGFloat height;
@end

@implementation Rectangle
- (CGFloat)area {
    return _width * _height;
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Circle *circle = [[Circle alloc] init];
        circle.radius = 5.0;
        Rectangle *rectangle = [[Rectangle alloc] init];
        rectangle.width = 4.0;
        rectangle.height = 3.0;

        NSArray<id<Shape>> *shapes = @[circle, rectangle];
        for (id<Shape> shape in shapes) {
            CGFloat area = [shape area];
            NSLog(@"The area is %f", area);
        }
    }
    return 0;
}

在这个例子中,CircleRectangle 类都遵循 Shape 协议,并实现了 area 方法。我们可以将 CircleRectangle 对象放入一个数组中,数组类型为 NSArray<id<Shape>>,表示数组中的元素都是遵循 Shape 协议的对象。通过遍历数组,我们可以调用每个对象的 area 方法,实现了多态性。

五、协议与类别(Category)的区别

(一)定义目的

  1. 协议:主要用于定义一组方法的声明,不包含实现,多个不相关的类可以遵循同一个协议,以提供统一的行为接口。协议强调的是不同类之间的共同行为定义。
  2. 类别:用于为已有的类添加新的方法,这些方法会成为类的一部分。类别通常用于在不修改类的原始代码的情况下,为类增加功能。例如,当你无法访问类的源代码时,可以通过类别为其添加方法。

(二)实现方式

  1. 协议:遵循协议的类必须实现协议中的必需方法,可选方法根据需要实现。协议本身不提供方法的实现代码。
  2. 类别:类别在实现文件(.m 文件)中提供新方法的实现。在类别中,不能添加新的实例变量,只能添加方法。

(三)应用场景

  1. 协议:常用于实现代理模式、数据来源模式以及多态性等,促进不同类之间的交互和代码复用。
  2. 类别:主要用于为现有类添加功能,例如为系统类(如 NSStringUIView 等)添加自定义的便捷方法,方便在项目中使用。

六、协议的高级特性

(一)协议作为方法参数和返回值类型

  1. 作为方法参数类型 协议可以作为方法的参数类型,这使得方法可以接受遵循特定协议的任何对象。例如,我们定义一个方法,它接受一个遵循 SpeedReporting 协议的对象,并打印其速度:
- (void)printSpeedOfVehicle:(id<SpeedReporting>)vehicle {
    NSInteger speed = [vehicle currentSpeed];
    NSLog(@"The vehicle's speed is %ld", (long)speed);
}

在调用这个方法时,可以传入任何遵循 SpeedReporting 协议的对象,如前面定义的 Car 对象:

Car *car = [[Car alloc] init];
car.speed = 60;
[self printSpeedOfVehicle:car];
  1. 作为返回值类型 协议也可以作为方法的返回值类型。例如,我们定义一个工厂方法,它返回一个遵循 Shape 协议的对象:
- (id<Shape>)createShape {
    // 根据某种逻辑决定返回 Circle 还是 Rectangle
    BOOL shouldReturnCircle = arc4random_uniform(2);
    if (shouldReturnCircle) {
        Circle *circle = [[Circle alloc] init];
        circle.radius = 3.0;
        return circle;
    } else {
        Rectangle *rectangle = [[Rectangle alloc] init];
        rectangle.width = 2.0;
        rectangle.height = 4.0;
        return rectangle;
    }
}

调用这个方法时,可以得到一个遵循 Shape 协议的对象,然后调用其 area 方法:

id<Shape> shape = [self createShape];
CGFloat area = [shape area];
NSLog(@"The area of the shape is %f", area);

(二)协议的嵌套

在 Objective-C 中,协议可以嵌套在其他协议或类中。例如,我们可以在一个协议中定义另一个协议:

@protocol OuterProtocol <NSObject>
@protocol InnerProtocol <NSObject>
- (void)innerMethod;
@end
- (id<InnerProtocol>)getInnerObject;
@end

这里,OuterProtocol 定义了一个嵌套的 InnerProtocol,并声明了一个方法 getInnerObject,该方法返回一个遵循 InnerProtocol 的对象。一个遵循 OuterProtocol 的类需要实现 getInnerObject 方法,并返回一个符合 InnerProtocol 的对象:

@interface MyClass : NSObject <OuterProtocol>
@end

@implementation MyClass
- (id<OuterProtocolInnerProtocol>)getInnerObject {
    // 假设我们有一个 InnerObject 类遵循 InnerProtocol
    InnerObject *innerObject = [[InnerObject alloc] init];
    return innerObject;
}
@end

需要注意的是,在引用嵌套协议时,使用 OuterProtocolInnerProtocol 这种方式,即外层协议名和内层协议名连接在一起。

(三)协议的属性声明

虽然协议本身不能包含实例变量,但可以声明属性。协议中的属性声明只是规定了遵循协议的类应该提供的属性访问方法,而不是实际定义属性。例如:

@protocol MyProtocol <NSObject>
@property (nonatomic, copy) NSString *name;
@end

遵循 MyProtocol 协议的类必须提供 name 属性的访问方法,要么通过合成属性(@synthesize),要么自己实现 namesetName: 方法:

@interface MyClass : NSObject <MyProtocol>
@end

@implementation MyClass
@synthesize name = _name;
@end

或者手动实现访问方法:

@implementation MyClass
- (NSString *)name {
    return _name;
}
- (void)setName:(NSString *)name {
    _name = [name copy];
}
@end

七、在运行时检查协议遵循情况

在 Objective-C 中,可以在运行时检查一个对象是否遵循某个协议。这在某些情况下非常有用,例如当你不确定一个对象是否支持特定的行为时,可以先进行检查。使用 respondsToSelector: 方法结合协议方法的选择器来检查对象是否实现了协议中的方法,或者使用 conformsToProtocol: 方法直接检查对象是否遵循某个协议。

例如,对于前面定义的 SpeedReporting 协议和 Car 类:

Car *car = [[Car alloc] init];
if ([car conformsToProtocol:@protocol(SpeedReporting)]) {
    NSLog(@"The car conforms to SpeedReporting protocol");
    if ([car respondsToSelector:@selector(currentSpeed)]) {
        NSInteger speed = [car currentSpeed];
        NSLog(@"The car's speed is %ld", (long)speed);
    }
} else {
    NSLog(@"The car does not conform to SpeedReporting protocol");
}

在上述代码中,首先使用 conformsToProtocol: 方法检查 car 对象是否遵循 SpeedReporting 协议。如果遵循,再使用 respondsToSelector: 方法检查是否实现了 currentSpeed 方法。如果实现了,就可以调用该方法获取速度。

通过这些方法,我们可以在运行时灵活地处理对象的协议遵循情况,提高程序的健壮性和灵活性。

八、协议使用中的常见问题与注意事项

(一)方法签名不匹配

当一个类声明遵循某个协议,但实现的方法签名与协议中声明的方法签名不匹配时,编译器可能不会给出明确的错误提示。例如,协议中声明的方法是 - (void)myMethod:(NSString *)param;,而类中实现的方法是 - (void)myMethod:(NSNumber *)param;,编译器可能只是发出一个警告,指出类没有完全实现协议。这可能导致运行时错误,因为消息发送机制依赖于方法签名的准确匹配。为了避免这种问题,在实现协议方法时,要仔细检查方法签名与协议声明是否一致。

(二)可选方法的调用

在调用遵循协议对象的可选方法时,需要先检查对象是否响应该方法。否则,如果对象没有实现该可选方法,会导致运行时错误。例如:

id<MyProtocol> object = [[SomeClass alloc] init];
if ([object respondsToSelector:@selector(optionalMethod)]) {
    [object optionalMethod];
}

这样可以确保在调用可选方法之前,对象确实实现了该方法,避免运行时崩溃。

(三)协议命名冲突

在大型项目中,可能会定义很多协议。如果不小心,可能会出现协议命名冲突的情况,即不同模块中定义了相同名称的协议。为了避免这种情况,可以采用命名空间的方式,例如在协议名称前加上模块名或项目名的前缀。例如,项目名为 MyProject,模块名为 UserModule,则协议名可以命名为 MyProjectUserModuleUserProtocol。这样可以降低命名冲突的概率,提高代码的可维护性。

(四)协议继承的复杂性

当协议存在多层继承关系时,可能会导致复杂性增加。例如,一个协议继承自另一个协议,而这个被继承的协议又继承自其他协议,这样可能会使遵循协议的类需要实现大量的方法,并且难以理清依赖关系。在设计协议继承结构时,要尽量保持简洁,避免过度复杂的继承关系。如果可能,将协议拆分成多个小的、功能单一的协议,让类有选择地遵循需要的协议,以降低复杂性。

通过注意这些常见问题和事项,可以在使用协议时编写出更健壮、可靠的代码,充分发挥协议在 Objective-C 编程中的强大功能。