《Effective Objective-C 2.0》读书笔记——协议与分类

目录

    • 第四章:协议与分类
      • 第23条:通过委托与数据源协议进行对象间通信
      • 第24条:将类的实现代码分散到便于管理的数个分类之中
      • 第25条:总是为第三方类的分类名称加前缀
      • 第26条:勿在分类中声明属性
      • 第27条:使用“class-continuation分类”隐藏实现细节(扩展,Extension)
      • 第28条:通过协议提供匿名对象


第四章:协议与分类

协议(protocol)和分类(category)是OC的两个重要的语言特性。理解并运用得当,可令代码易读、易维护且少出错。

第23条:通过委托与数据源协议进行对象间通信

可以通过委托(代理 delegate)与数据源(data source)协议进行对象间通信。
协议中可以定义方法和属性。
在协议中@optional@require关键字来指定协议方法是可选择实现的还是必须实现的,如果适用方没有实现@require方法那么编译器就会给出警告。

委托模式

何为代理?一种软件设计模式,iOS中@protocol形式体现,传递方式为一对一。

代理模式的主旨:定义一个委托协议,若对象想接受另一个对象(委托方)的委托,则需遵守该协议,以成为 “代理方”。而委托方则可以通过协议方法给代理方回传一些信息,也可以在发生相关事件时通知代理方。这样委托方就可以把应对某个行为的责任委托给代理方去处理了。

代理的工作流程:
委托方要求代理方把需要实现的方法全都定义在委托协议中;
代理方遵循协议并实现协议中的方法(若有返回值会返回给委托方);
委托方调用代理方遵从的协议方法。
请添加图片描述

delegate属性一般定义为weak以避免循环引用,代理方强引用委托方,委托方弱引用代理方。
如果要向外界公布一个类遵守某协议,那么就在接口中声明;如果这个协议是委托协议,那么就在类扩展中声明,因为该协议通常只会在类的内部使用。

对于@optional方法,在委托方法中调用时,需要先判断代理方是否能响应(是否实现了该方法),如果能响应才能给它发送协议消息,否则代理方可能没有实现该方法,调用时就会因找不到方法实现而导致Crash:

//最好先判断一下delegate是否存在提高执行效率
if (_delegate && [_delegate respondsToSelector:@selector(protocolOptionalMethod)]) {[_delegate protocolOptionalMethod];
}

数据源模式

委托模式的另一用法,旨在向类提供数据,所以也称“数据源模式”,其用协议定义一套接口,令某类经由该接口获取所需的数据。

在此模式中,信息从数据源流向委托方;
在常规模式中,信息从类(委托方)流向代理方(受委托方)。
请添加图片描述

通过UITableView就可以很好的理解Data Source和Delegate这两种模式:

  • 通过UITableViewDataSource协议获取要在列表中显示的数据;

  • 通过UITableViewDelegate协议来处理用户与列表的交互操作。

    @protocol UITableViewDataSource <NSObject>
    @required
    - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section;
    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
    @optional
    - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView; 
    ...
    @end@protocol UITableViewDelegate <NSObject, UIScrollViewDelegate>
    @optional
    -(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
    ...
    @end
    

性能优化

在实现委托模式和数据源模式时,如果协议方法时可选的,那么在调用协议方法时就需要判断其是否能响应。

if (_delegate && [_delegate respondsToSelector:@selector(networkFetcher:didReceiveData)]) {[_delegate networkFetcher:self didReceiveData:data];
}

如果我们需要频繁调用该协议方法,那么仅需要第一次判断是否能响应即可。以上代码可做性能优化,将代理方是否能响应某个协议方法这一信息存起来:

  1. 在委托方中嵌入一个含有位域(bitfield,又称“位段”、“位字段”)的结构体作为其实例变量,而结构体中每个位域则表示delegate对象是否实现了协议中的相关方法。该结构体就是用来缓存代理方是否能响应特定的协议方法的:

    @interface EOCNetworkFetcher () {struct {unsigned int didReceiveData      : 1;unsigned int didFailWithError    : 1;unsigned int didUpdateProgressTo : 1;} _delegateFlags;
    }
    

    有关位段的知识,看这篇文章:【C语言】位域(位段、位字段)。

  2. 重写delegate属性的setter方法,对_delegateFlags结构体里的标志进行赋值,实现缓存功能。

    - (void)setDelegate:(id<EOCNetworkFetcher>)delegate {_delegate = delegate;_delegateFlags.didReceiveData = [delegate respondsToSelector:@selector(networkFetcher:didReceiveData:)];_delegateFlags.didFailWithError = [delegate respondsToSelector:@selector(networkFetcher:didFailWithError:)];_delegateFlags.didUpdateProgressTo = [delegate respondsToSelector:@selector(networkFetcher:didUpdateProgressTo:)];
    }
    
  3. 这样每次调用delegate相关方法之前,就不用通过respondToSelector:方法来检测代理方法是否能响应特定协议方法了,而是直接查询结构体中的标志,提升了执行速度。

    if (_delegate && _delegateFlags.didReceiveData) {[_delegate networkFetcher:self didReceiveData:data];
    }
    

若有必要,可实现含有位段的结构体,将委托对象是否能响应相关协议方法这一信息缓存至其中。

第24条:将类的实现代码分散到便于管理的数个分类之中

该篇讲解了分类的一种使用场合:分解体积庞大的类文件,可以将一个类按功能拆解成多个模块,方便代码管理。

之所以要将类代码打散到分类中还有个原因,就是便于调试:对于某个分类中的所有方法来说,分类名称都会出现在其符号中。例如,addFriend:方法的符号名

-[EOCPerson(Firendship) addFriend:]

在调试器的回溯信息中,可以根据符号名精确定位到该方法所属的功能区(分类),这对于某些应该视为私有的方法来说更是极为有用。可以创建名为Private的分类,把这种方法全都放在里面。
这个分类里的方法一般只会在类或框架内部使用,而无须对外公布。这样一来,类的使用者有时可能会在查看回溯信息时发现private一词,从而知道不应该直接调用此方法了。这可算作一种编写“自我描述式代码”(self-documenting code)的办法。

第25条:总是为第三方类的分类名称加前缀

  • 向第三方类中添加分类时,总应给其名称加上你专用的前缀。
  • 向第三方类中添加分类时,总应给其中的方法名加上你专用的前缀。

——

  • 分类机制还通常用于向无源代码的既有类中新增功能。
  • 分类是运行时决议。何为运行时决议?Category 编译之后的底层结构是 struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息,这时候分类中的数据还没有合并到类中,而是在程序运行的时候通过 Runtime 机制将所有分类数据合并到类(类对象、元类对象)中去。这是分类最大的特点,也是分类和扩展的最大区别,扩展是在编译的时候就将所有数据都合并到类中去了。

——

  • 需要注意的是:

    1. 分类方法会 “覆盖” 同名的宿主类方法,如果使用不当会造成问题。方法被覆盖会导致执行结果和你预期的不同,且这种 bug 很难排查;
    2. 同名分类方法谁能生效取决于编译顺序,最后参与编译的分类中的同名方法会最终生效;
    3. 名字相同的分类会引起编译报错。
  • 为避免以上问题,可以以命名空间来区别各个分类的名称与分类中所定义的方法。而在 OC 中实现命名空间功能只有一个办法,就是给相关名称都加上某个共用的前缀。这样分类和宿主类中出现同名方法导致方法被 “覆盖” 的问题的几率就会小很多。
    向第三方类中添加分类时,更应该注意这个问题。

第26条:勿在分类中声明属性

分类中可以添加属性,但应避免这样做。
类扩展是编译时决议,在编译的时候就将扩展中的所有数据都合并到类中去了,所以扩展中添加属性没有任何问题。
分类是运行时决议,类的内存布局在编译时就已经确定,所以分类中无法添加实例变量,分类中添加的属性也不会自动生成实例变量以及setter和getter方法的实现(因为属性就是对实例变量的封装)。

如果把属性放在分类中:

@interface NSCalendar (EOC_Addtions)
@property (nonatomic, strong)NSArray* eoc_allMonths;
@end

编译器会报黄标:

请添加图片描述

警告为:属性的setter和getter方法没有实现。因为分类中的属性不会自动生成实例变量以及setter和getter方法的实现,这样外部调用该属性的存取方法就会Crash。有两种解决方式:

  1. 手动添加setter和getter方法的实现;
  2. 使用@dynamic告诉编译器,你会在运行时再提供这些方法的实现,以消除警告。你可以使用动态方法解析为这些方法动态添加方法实现。但如果你没有去处理的话,@dynamic就仅仅是消除了警告,如果外部调用了该属性的存取方法还是会Crash。

由于分类底层结构的限制,不能直接给Category添加成员变量,但是可以通过关联对象间接实现Category有成员变量的效果。【🚩详见第10条】
需要注意的是,存储关联对象时其内存管理语义需要与属性的一致。如果属性的内存管理语义更改,那么关联对象的关联策略也要修改,这是容易忽略的地方。

只读属性可以在分类中使用,我们为其实现getter方法。由于实现属性所需的全部方法(只读属性只需实现 getter 方法)都已实现,所以编译器就不会再为该属性自动合成实例变量,也不会发出警告。

类接口与类扩展是真正能够定义实例变量的地方,而属性只是定义实例变量及相关存取方法所用的 “语法糖”,所以也应该遵循同实例变量一样的规则。尽管分类中可以通过关联对象的手段来实现分类中可以添加实例变量的效果,但其目标在于扩展类的功能,而非封装数据。属性是用来封装数据的,所以在分类中可以定义存取方法,但尽量不要定义属性。

第27条:使用“class-continuation分类”隐藏实现细节(扩展,Extension)

OC动态消息系统的工作方式决定了其不可能实现真正的私有方法或私有示例变量,但我们最好还是只把确实需要对外公布的那部分内容公开,而把无须对外公布的方法及属性、实例变量声明在类扩展中。

隐藏变量
公共接口里本来就能定义实例变量,不过,把它们定义在 “扩展”或“实现块”(从语法上看,加在这两个地方是等效的,加在哪看个人喜好)中可以将其隐藏起来:

@interface EOCPerson() {EOCSuperSecretClass* _secretInstance;
}
@end@implementation EOCPerson {int _anotherInstanceVariable;
}
@end

即便在公共接口里将其标注为private,也还是会泄漏实现细节,假如说有个绝密类:

@class EOCSuperSecretClass;@interface EOCClass : NSObject {@privateEOCSuperSecretClass* _secretInstance;
}
@end

这样别人就会知道有个名叫EOCSuperSecretClass的类,可以将其类型改为id,但这样的话,在类内部使用此实例时,无法得到编译器的帮助,没必要只因为想对外界隐藏某个内容就放弃编译器的辅助检查功能。
这些在扩展中添加的实例变量并非真的私有,因为在运行期总可以调用某些方法绕过此限制,不够由于没有声明在公共头文件里,其隐藏程度更好。

扩展属性可读写性
如果一个属性在类的内部需要进行存取值,而对外只允许使用方进行取值。那么可以在类声明中将该属性的读写权限设置为readonly只读,而在类扩展中再次声明该属性并设置为readwrite可读写。

这样,封装在类中的数据就由实例本身来控制,而外部代码则无法修改其值,【🚩第18条】曾详述了这一话题。
请注意,若观察者正读取属性值而内部代码又在写入该属性时,则有可能引发竞争条件,合理使用同步条件【参见🚩第41条】能缓解此问题。

隐藏协议
若类所遵循的协议只应视为私有,比如委托协议和数据源协议,不会让该协议在公共接口中泄漏,那么该协议就可以在类扩展中去遵从。

#import "EOCSecretDelegate.h"
@interface EOCPerson() <EOCSecretDelegate>
// ...
@end

你可能会说,只要在接口文件中向前声明该协议就可以不引入定义该协议的头文件了,但这样做编译器会提示找不到协议的定义:

请添加图片描述

还是得找扩展来帮忙。

私有方法

类扩展中除了可以声明属性、实例变量,遵从协议,还可以声明私有方法。虽然现在编译器不强制要求我们在使用方法之前必须先声明,直接在扩展中实现即可。但其实在类扩展中声明一下私有方法还是有好处的,这样可以把类里所含的相关方法都统一描述于此,使代码可读性更高

第28条:通过协议提供匿名对象

  • 协议可在某种程度上提供匿名类型。具体的对象类型可以淡化成遵从某协议的 id 类型,协议里规定了对象所应实现的方法。
  • 使用匿名对象来隐藏类型名称(或类名)。
  • 如果具体类型不重要,重要的是对象能够响应(定义在协议里的)特定方法,那么可使用匿名对象来表示。

——

  • 可以通过协议提供匿名对象来隐藏类名,做法就是将对象声明为遵从某协议的id类型:id<protocol> object;

    1. 比如delegate属性,其声明为:

      @property(nonatomic, weak)id<EOCDelegate> delegate;
      

      委托方无须关心代理方的具体类型,只需代理方遵守委托协议并实现协议方法即可,这样委托方就可以向代理方发送协议消息了。

    2. 在字典中,键和值的标准内存管理语义分别是设置时拷贝设置时保留。因此NSMutableDictionary设置键值的方法为:
      请添加图片描述
      KeyType可以是不为nil的任何OC不可变类型,因此只要传入的对象遵守NSCopying协议字典就能向该对象发送拷贝消息了,而这个key参数就可以视为匿名对象。

  • 可以在运行期查出匿名对象所属类型,但这样做不好,因为匿名对象已经表明它的具体类型无关紧要了,你仅需要通过它来调用协议方法。

  • 使用匿名对象的情况:

    • 接口背后有多个不同的实现类,而你又不想指明具体使用哪个类。因为有时候这些类可能会变,有时候它们又无法容纳于标准的类继承体系中,因而不能以某个公共基类统一表示(比如UIButton)。
      这样就可以将这些对象所具备的方法定义在协议中,用 id 类型指代并遵守该协议,即可调用协议中的方法,而在运行期则会根据对象具体类型,调用具体的方法实现
    • 对象具体类型不重要,重要的是对象能够响应(定义在协议里的)特定方法。即便该对象类型是固定的你也可以这么做,以表示类型在此处不重要。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/329751.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

匝间冲击耐压试验仪产品介绍及工作原理

产品简介 武汉凯迪正大KD2684S匝间冲击耐压试验仪适用于电机、变压器、电器线圈等这些由漆包线绕制的产品。因漆包线的绝缘涂敷层本身存在着质量问题&#xff0c;以及在绕线、嵌线、刮线、接头端部整形、绝缘浸漆、装配等工序工艺中不慎而引起绝缘层的损伤等&#xff0c;都会造…

Docker Compose使用

Docker-Compose是什么 docker建议我们每一个容器中只运行一个服务,因为doker容器本身占用资源极少&#xff0c;所以最好是将每个服务单独分割开来&#xff0c;但是这样我们又面临了一个问题&#xff1a; 如果我需要同时部署好多个服务&#xff0c;难道要每个服务单独写Docker…

Midjourney应用场景、特点、生成图片带来影响

Midjourney是一个基于GPT-3.5系列接口开发的免费AI机器人&#xff0c;旨在提供多领域的智能对话服务。本文主要介绍Midjourney的应用场景、功能特点、图片生成后可以做什么&#xff1f; 一、Midjourney应用场景 Midjourney的应用场景相当广泛&#xff0c;以下是一些主要的适用…

Public Key Retrieval is not allowed解决

修改高级属性。 “Public Key Retrieval is not allowed” 错误是由于 MySQL 连接驱动程序的默认行为更改所引起的。在 MySQL 8.0 版本及更新版本中&#xff0c;默认情况下禁用了通过公钥检索用户密码的功能。 在旧版本的 MySQL 中&#xff0c;客户端连接到服务器时&#xf…

【Unity2D:C#Script】制作敌人

一、制作敌人预制体 1. 在场景面板中添加敌人&#xff0c;并创建预制体 2. 设置敌人的锚点在底部 二、为敌人添加碰撞体积 1. 添加Box Collider 2D、Rigidbody 2D组件 2. 调整轴心点位置、层级、碰撞体积大小、刚体类型、锁定z轴 Body Type&#xff08;刚体类型&#xff09;&…

网络的基础理解

文章目录 网络的基础认识 网络协议协议分层OSI七层模型TCP/IP 五层/四层 模型 网络的基础认识 先来看下面几个问题 什么是网络&#xff1f; 网络就是有许多台设备包括计算机单不仅限于计算机&#xff0c;这些设备通过相互通信所组成起来系统&#xff0c;我们称之为网络所以如…

【动态规划七】背包问题

目录 0/1背包问题 一、【模板】01背包 二、分割等和子集 三、目标和 四、最后一块石头的重量 II 完全背包问题 一、【模板】完全背包 二、零钱兑换 三、零钱兑换 II 四、完全平方数 二维费用的背包问题 一、一和零 二、盈利计划 似包非包 组合总和 卡特兰数 不…

Sui生态DeFi项目Cetus和Aftermath宣布启动孵化器

Sui DeFi中的去中心化交易所Cetus和Aftermath Finance联合Sui基金会宣布启动新的孵化器&#xff0c;为初创项目提供更多可行性途径。这两个DeFi项目在Sui上有着较长的历史&#xff0c;自去年一同与主网推出以来&#xff0c;目前在TVL方面位居前五。这两个项目的持久性和成功使它…

《Effective Objective-C 2.0》读书笔记——接口与API设计

目录 第三章&#xff1a;接口与API设计第15条&#xff1a;用前缀避免命名空间冲突第16条&#xff1a;提供“全能初始化方法”第17条&#xff1a;实现description方法第18条&#xff1a;尽量使用不可变对象第19条&#xff1a;使用清晰而协调的命名方式第20条&#xff1a;为私有方…

计算机网络协议

网络协议 基于TCP的应用层协议 POP3&#xff08;Post Office Protocol 3&#xff09;&#xff1a; 用于支持客户端远程管理服务器上的电子邮件。它支持**“离线”邮件处理**&#xff0c;即邮件发送到服务器上后&#xff0c;一旦邮件被POP3客户端下载到本地计算机&#xff0c;…

Redis --学习笔记

Redis简介 一个基于内存的key-value结构数据库。Redis 是互联网技术领域使用最为广泛的存储中间件 特点&#xff1a; 基于内存存储&#xff0c;读写性能高 适合存储热点数据&#xff08;热点商品、资讯、新闻&#xff09; 企业应用广泛 Redis默认端口号为6379 Redis是用…

Unity射击游戏开发教程:(24)创造不同的敌人

在这篇文章中,我们将讨论添加一个可以承受多次攻击的新敌人和一些动画来使事情变得栩栩如生。敌人没有任何移动或射击行为。这将有助于增强未来敌人的力量。 我们将声明一个 int 来存储敌人可以承受的攻击数量,并将其设置为 3。

力扣刷题---1748.唯一元素的和【简单】

题目描述 给你一个整数数组 nums 。数组中唯一元素是那些只出现 恰好一次 的元素。 请你返回 nums 中唯一元素的 和 。 示例 1&#xff1a; 输入&#xff1a;nums [1,2,3,2] 输出&#xff1a;4 解释&#xff1a;唯一元素为 [1,3] &#xff0c;和为 4 。 示例 2&#xff1a;…

NLP(16)--生成式任务

前言 仅记录学习过程&#xff0c;有问题欢迎讨论 输入输出均为不定长序列&#xff08;seq2seq&#xff09;自回归语言模型&#xff1a; x 为 str[start : end ]; y为 [start1 : end 1] 同时训练多个字&#xff0c;逐字计算交叉熵 encode-decode结构&#xff1a; Encoder将输…

微服务远程调用 RestTemplate

Spring给我们提供了一个RestTemplate的API&#xff0c;可以方便的实现Http请求的发送。 同步客户端执行HTTP请求&#xff0c;在底层HTTP客户端库(如JDK HttpURLConnection、Apache HttpComponents等)上公开一个简单的模板方法API。RestTemplate通过HTTP方法为常见场景提供了模…

从ES5迈向ES6:探索 JavaScript 新增声明命令与解构赋值的魅力

个人主页&#xff1a;学习前端的小z 个人专栏&#xff1a;JavaScript 精粹 本专栏旨在分享记录每日学习的前端知识和学习笔记的归纳总结&#xff0c;欢迎大家在评论区交流讨论&#xff01; ES5、ES6介绍 文章目录 &#x1f4af;声明命令 let、const&#x1f35f;1 let声明符&a…

【LeetCode】每日一题 2024_5_24 找出最具竞争力的子序列(栈,模拟,贪心)

文章目录 LeetCode&#xff1f;启动&#xff01;&#xff01;&#xff01;题目&#xff1a;找出最具竞争力的子序列题目描述代码与解题思路 每天进步一点点 LeetCode&#xff1f;启动&#xff01;&#xff01;&#xff01; 题目&#xff1a;找出最具竞争力的子序列 题目链接&a…

【Unity2D:C#Script】实现角色射击功能

一、创建子弹预制体 1. 创建子弹预制体 2. 调整图片大小、层级 二、为子弹添加碰撞体积 1. 添加Box Collider 2D、Rigidbody 2D组件 2. 锁定z轴 三、编辑敌人脚本 注&#xff1a;在以下代码中&#xff0c;只显示本章节新增的代码&#xff0c;省略原有的代码 1. 为敌人添加生…

一阶数字高通滤波器

本文的主要内容包含一阶高通滤波器公式的推导和数字算法的实现以及编程和仿真 1 计算公式推导 1.1.2 算法实现及仿真 利用python实现的代码如下&#xff1a; import numpy as np # from scipy.signal import butter, lfilter, freqz import matplotlib.pyplot as plt #2pifW…

免费分享一套微信小程序旅游推荐(智慧旅游)系统(SpringBoot后端+Vue管理端)【论文+源码+SQL脚本】,帅呆了~~

大家好&#xff0c;我是java1234_小锋老师&#xff0c;看到一个不错的微信小程序旅游推荐(智慧旅游)系统(SpringBoot后端Vue管理端)【论文源码SQL脚本】&#xff0c;分享下哈。 项目视频演示 【免费】微信小程序旅游推荐(智慧旅游)系统(SpringBoot后端Vue管理端) Java毕业设计…