Effective Objective-C 2.0 读书笔记------协议和分类
文章目录
- [Effective Objective-C 2.0 读书笔记------协议和分类](#Effective Objective-C 2.0 读书笔记——协议和分类)
-
- 在分类中添加属性
- [使用 "class-continuation分类" 隐藏实现细节](#使用 “class-continuation分类” 隐藏实现细节)
- 通过协议提供匿名对象
在分类中添加属性
尽管从技术上说,分类里也可以声明属性,但这 种做法还是要尽量避免。原因在于,除了class-continuation
分类之外,其 他分类都无法向类中新增实例变量,因此,它们无法把实现属性所需的实例变量合成出来。
由于我们在编写功能较多的类时,一般都会给每一个代码开辟一个分类,让他负责一部分的功能,但是我们在编写分类时,尽量不要在分类之中添加对应属性,所有的属性都应该在主接口(main interface)
如果我们尝试在分类之中添加属性
objc
@interface MyClass (MyCategory)
@property (nonatomic, strong) NSString *categoryProperty;
@end
上述代码 不会 生成 categoryProperty
的实例变量或存储实现。如果你尝试 self.categoryProperty = @"test";
,会导致编译错误。
如果真的想要将在分类之中添加属性,那么这就需要用到我们的关联对象
objc
#import <objc/runtime.h>
@interface MyClass (MyCategory)
@property (nonatomic, strong) NSString *categoryProperty;
@end
@implementation MyClass (MyCategory)
static const void *CategoryPropertyKey = &CategoryPropertyKey; // 关联对象的 key
// getter 方法
- (NSString *)categoryProperty {
return objc_getAssociatedObject(self, CategoryPropertyKey);
}
// setter 方法
- (void)setCategoryProperty:(NSString *)categoryProperty {
objc_setAssociatedObject(self, CategoryPropertyKey, categoryProperty, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
书中的原话:
这样做可行,但不太理想。要把相似的代码写很多遍,而且在内存管理问题上容易出错, 因为我们在为属性实现存取方法时,经常会忘记遵从其内存管理语义。比方说,你可能通过属性特质 (attribute ) 修改了某个属性的内存管理语义 。 而此时还要记得,在设置方法中也得修改设置关联对象时所用的内存管理语义才行。所以说,尽管这个做法不坏,但笔者并不推荐。
虽然我们不建议在分类中直接使用属性,但是我们可以视情况而定,选择使用只读的属性
假设我们要为 NSCalendar
类创建一个分类,用以返回一个包含各个月份名称的字符串数组。初始做法是利用只读属性来实现:
objc
@interface NSCalendar (EOC_Additions)
@property (nonatomic, strong, readonly) NSArray *eoc_allMonths;
@end
@implementation NSCalendar (EOC_Additions)
- (NSArray *)eoc_allMonths {
if ([self.calendarIdentifier isEqualToString:NSGregorianCalendar]) {
return @[@"January", @"February", @"March", @"April",
@"May", @"June",
@"July", @"August",
@"September", @"October", @"November", @"December"];
} else if (/* other calendar identifiers */) {
/* return months for other calendars */
}
return nil;
}
@end
使用 "class-continuation分类" 隐藏实现细节
class-continuation
其实就是不命名的分类,没有名字的分类自然没办法在其他地方被应用,这就引申出我们使用它的场景------我们在编程的过程之中只需要对外公布的那部分内容公开,那么不公开的就会被放在class-continuation分类
之中,这个class-continuation
有什么具体作用呢?
隐藏实现细节:当我们希望将某些属性,方法或者协议名字设为私有,只对类内部使用时,使用类扩展可以将这些声明放在 .m 文件中,避免在头文件中暴露。
读写属性与只读属性的分离 :我们前面的读书笔记有写过,在公开接口中将属性声明为只读(readonly
),而在类扩展中重新声明为读写(readwrite
),从而允许内部修改
-
在 .m 文件中声明类扩展
在实现文件开头的
@interface
部分添加匿名类别,用于声明私有属性和方法:objc// MyClass.m #import "MyClass.h" @interface MyClass () @property (nonatomic, strong) NSString *privateProperty; // 私有属性 - (void)privateMethod; // 私有方法 @end
-
在 @implementation 中实现私有属性和方法
objc@implementation MyClass - (instancetype)init { self = [super init]; if (self) { // 可以初始化私有属性 _privateProperty = @"初始值"; } return self; } - (void)privateMethod { NSLog(@"调用了私有方法,privateProperty = %@", self.privateProperty); } // 公开方法可能会调用私有方法 - (void)publicMethod { [self privateMethod]; } @end
- 私有属性 :上例中的
privateProperty
在 MyClass 的头文件中没有声明,因此外部无法访问或修改它。 - 私有方法 :
privateMethod
仅在实现文件中可见,确保了内部实现的封装性。
通过协议提供匿名对象
协议定义了一系列方法,遵从此协议的对象应该实现它们,我们可以用协议把自己所写的API 之中的实现细节隐藏起来,将返回的对象设计为遵从此协议的纯i d类型。这样的话,想要隐藏的类名就不会出现在API 之中了。若是接又背后有多个不同的实现类,而你又不想指明具体使用哪个类,那么可以考虑用这个办法------因为有时候这些类可能会变,有时候它们又无法容纳于标准的类继承体系中,因而不能以某个公共基类来统一表示。
就是说,一个方法返回的对象只暴露其所遵循的协议(接口),而隐藏了具体的实现类。调用者只知道它能响应协议中定义的方法,而不必关心其背后的具体类型。
书中给出的例子:首先定义一个协议,该协议描述了需要实现的接口。例如,书中给出的数据库连接的例子中,可以定义一个协议 EOCDatabaseConnection
:
objc
@protocol EOCDatabaseConnection <NSObject>
- (void)connect;
- (void)disconnect;
- (BOOL)isConnected;
- (NSArray *)performQuery:(NSString *)query;
@end
这个协议规定了所有数据库连接对象必须实现的方法,不论它们的内部实现如何。
在需要返回数据库连接的类中,我们不返回具体的类,而是返回一个遵循该协议的对象。比如,一个数据库管理器的接口可以这样设计:
objc
@interface EOCDatabaseManager : NSObject
+ (instancetype)sharedInstance;
- (id<EOCDatabaseConnection>)connectionWithIdentifier:(NSString *)identifier;
@end
其中,方法 connectionWithIdentifier:
返回的类型是 id<EOCDatabaseConnection>
,这表明返回的对象必须遵循 EOCDatabaseConnection
协议,但调用者不必知道它具体是什么类。
假设有一个具体类 MyDatabaseConnection
实现了该协议:
objc
@interface MyDatabaseConnection : NSObject <EOCDatabaseConnection>
@property (nonatomic, assign) BOOL connected;
@end
@implementation MyDatabaseConnection
- (void)connect {
NSLog(@"MyDatabaseConnection: 正在连接数据库...");
self.connected = YES;
}
- (void)disconnect {
NSLog(@"MyDatabaseConnection: 断开数据库连接...");
self.connected = NO;
}
- (BOOL)isConnected {
return self.connected;
}
- (NSArray *)performQuery:(NSString *)query {
NSLog(@"MyDatabaseConnection: 执行查询: %@", query);
// 这里只返回模拟的数据
return @[@"结果1", @"结果2", @"结果3"];
}
@end
数据库管理器的实现中可以根据标识符返回合适的连接对象,而调用者只关心它是否符合 EOCDatabaseConnection
协议:
objc
@implementation EOCDatabaseManager
+ (instancetype)sharedInstance {
static EOCDatabaseManager *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[EOCDatabaseManager alloc] init];
});
return instance;
}
- (id<EOCDatabaseConnection>)connectionWithIdentifier:(NSString *)identifier {
// 根据 identifier 可以决定返回哪种具体实现
MyDatabaseConnection *connection = [[MyDatabaseConnection alloc] init];
[connection connect]; // 自动建立连接
return connection;
}
@end
调用者代码则只需依赖协议接口:
objc
int main(int argc, const char * argv[]) {
@autoreleasepool {
EOCDatabaseManager *dbManager = [EOCDatabaseManager sharedInstance];
id<EOCDatabaseConnection> connection = [dbManager connectionWithIdentifier:@"MainDB"];
if ([connection isConnected]) {
NSLog(@"数据库已连接");
NSArray *results = [connection performQuery:@"SELECT * FROM users"];
NSLog(@"查询结果: %@", results);
[connection disconnect];
} else {
NSLog(@"数据库连接失败");
}
}
return 0;
}
在这里,调用者只知道 connection
符合 EOCDatabaseConnection
协议,不需要关心它内部是 MyDatabaseConnection
或其他类型。