Effective Objective-C 2.0 读书笔记——协议和分类

news/2025/2/22 10:59:49

Effective Objective-C 2.0 读书笔记——协议和分类

文章目录

  • Effective Objective-C 2.0 读书笔记——协议和分类
    • 分类中添加属性
    • 使用 “class-continuation分类” 隐藏实现细节
    • 通过协议提供匿名对象

分类中添加属性

尽管从技术上说,分类里也可以声明属性,但这 种做法还是要尽量避免。原因在于,除了class-continuation分类之外,其 他分类都无法向类中新增实例变量,因此,它们无法把实现属性所需的实例变量合成出来。

由于我们在编写功能较多的类时,一般都会给每一个代码开辟一个分类,让他负责一部分的功能,但是我们在编写分类时,尽量不要在分类之中添加对应属性,所有的属性都应该在主接口(main interface)

如果我们尝试在分类之中添加属性

@interface MyClass (MyCategory)
@property (nonatomic, strong) NSString *categoryProperty;
@end

上述代码 不会 生成 categoryProperty 的实例变量或存储实现。如果你尝试 self.categoryProperty = @"test";,会导致编译错误。

如果真的想要将在分类之中添加属性,那么这就需要用到我们的关联对象

#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 类创建一个分类,用以返回一个包含各个月份名称的字符串数组。初始做法是利用只读属性来实现:

@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),从而允许内部修改

  1. 在 .m 文件中声明类扩展

    在实现文件开头的 @interface 部分添加匿名类别,用于声明私有属性和方法:

    // MyClass.m
    
    #import "MyClass.h"
    
    @interface MyClass ()
    @property (nonatomic, strong) NSString *privateProperty; // 私有属性
    - (void)privateMethod; // 私有方法
    @end
    
  2. 在 @implementation 中实现私有属性和方法

    @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

@protocol EOCDatabaseConnection <NSObject>
- (void)connect;
- (void)disconnect;
- (BOOL)isConnected;
- (NSArray *)performQuery:(NSString *)query;
@end

这个协议规定了所有数据库连接对象必须实现的方法,不论它们的内部实现如何。

在需要返回数据库连接的类中,我们不返回具体的类,而是返回一个遵循该协议的对象。比如,一个数据库管理器的接口可以这样设计:

@interface EOCDatabaseManager : NSObject
+ (instancetype)sharedInstance;
- (id<EOCDatabaseConnection>)connectionWithIdentifier:(NSString *)identifier;
@end

其中,方法 connectionWithIdentifier: 返回的类型是 id<EOCDatabaseConnection>,这表明返回的对象必须遵循 EOCDatabaseConnection 协议,但调用者不必知道它具体是什么类。

假设有一个具体类 MyDatabaseConnection 实现了该协议:

@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 协议:

@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

调用者代码则只需依赖协议接口:

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 或其他类型。


http://www.niftyadmin.cn/n/5862144.html

相关文章

JAVA JUC 并发编程学习笔记(一)

文章目录 JUC进程概述对比 线程创建线程ThreadRunnableCallable 线程方法APIrun startsleep yieldjoininterrupt打断线程打断 park终止模式 daemon不推荐 线程原理运行机制线程调度未来优化 线程状态查看线程 同步临界区syn-ed使用锁同步块同步方法线程八锁 锁原理Monitor字节码…

如何用deepseek快速生成思维导图和流程图?

一起来看看md格式和mermaid格式&#xff0c;与deepseek的碰撞会产生怎样的魔法吧&#xff01; 1、md格式deepseek&#xff0c;快速生成思维导图 Markdown 是一种轻量级的标记语言&#xff0c;旨在以易读易写的纯文本格式编写文档&#xff0c;并能够轻松转换为结构化的 HTML&a…

Spring Boot (maven)分页4.0.1版本 专业版- 改

前言&#xff1a; 通过实践而发现真理&#xff0c;又通过实践而证实真理和发展真理。从感性认识而能动地发展到理性认识&#xff0c;又从理性认识而能动地指导革命实践&#xff0c;改造主观世界和客观世界。实践、认识、再实践、再认识&#xff0c;这种形式&#xff0c;循环往…

C++17中的std::scoped_lock:简化多锁管理的利器

文章目录 1. 为什么需要std::scoped_lock1.1 死锁问题1.2 异常安全性1.3 锁的管理复杂性 2. std::scoped_lock的使用方法2.1 基本语法2.2 支持多种互斥锁类型2.3 自动处理异常 3. std::scoped_lock的优势3.1 避免死锁3.2 简化代码3.3 提供异常安全保证 4. 实际应用场景4.1 数据…

qt:输入控件操作

1.单行输入框 QLineEdit &#xff0c;单行输入&#xff0c;但是不能换行 属性特点text输入框中的文本inputMask输入内容格式约束&#xff0c;限制用户输入的格式。maxLength最大长度&#xff0c;定义输入框允许的最大字符数。frame是否添加边框&#xff0c;默认为 true 显示边…

【GitHub】github学生认证,在vscode中使用copilot的教程

github学生认证并使用copilot教程 写在最前面一.注册github账号1.1、注册1.2、完善你的profile 二、Github 学生认证注意事项&#xff1a;不完善的说明 三、Copilot四、在 Visual Studio Code 中安装 GitHub Copilot 扩展4.1 安装 Copilot 插件4.2 配置 Copilot 插件&#xff0…

什么是超越编程(逾编程)(元编程?)

超越编程(逾编程)(元编程&#xff1f;)(meta-programming) 目录 1. meta- 的词源 2. 逾编程(meta-programming) 的直实含义 2.1 定义 2.2 说明 3. 翻译成“元编程”应该是一种错误 1. meta- 的词源 这是一个源自希腊语的构词元素&#xff0c;其有三种含义&#xff…

3D打印注塑件-省模具费90%的解决方案

"开模费用50万&#xff0c;首批订单才200件&#xff1f;" 这是许多制造企业的真实困境。传统注塑工艺动辄数周的开模周期和5-50万元的模具成本&#xff0c;让中小企业的产品迭代举步维艰。 在传统制造流程中&#xff0c;注塑件的生产往往需要高昂的模具开发费用和较…