Kotlin开发笔记:使用委托进行拓展

Kotlin开发笔记:使用委托进行拓展

在这里插入图片描述

导言

在OO语言(面向对象)中,我们经常会用到委托或者代理的思想。委托和代理在乍一看很相似,其实其各有各的侧重点,这里我引用ChatGpt的回答:

委托(Delegation)和代理(Proxy)虽然有相似之处,但在面向对象编程中有一些区别。

  • 职责分配:
    委托:委托是一种将对象的一部分职责转交给另一个对象来处理的方式。原始对象将某些任务委托给另一个对象,但是仍然保持对委托对象的控制。
    代理:代理是一种通过提供一个代替对象来控制访问。代理对象通常具有与被代理对象相同的接口,客户端代码可以通过代理对象来间接访问被代理对象。
    目的:
  • 委托:委托用于实现代码的模块化和职责分离,将一部分功能委托给其他对象处理,以达到更好的代码组织和可维护性。
  • 代理:代理用于控制访问,可以用于实现懒加载、安全性控制、远程访问等。代理对象可以在客户端和被代理对象之间添加额外的逻辑,如缓存、权限验证等。
    关系类型:
  • 委托:委托通常涉及到两个具体的对象,一个是原始对象,另一个是被委托的对象,它们可以属于不同的类。
  • 代理:代理通常有三个主要组成部分:客户端、代理对象和被代理对象。代理对象扮演中间人的角色,控制客户端访问被代理对象。

虽然委托和代理在某些情况下可能会有重叠,但它们的重点和使用方式是不同的。
委托更关注职责分离和模块化,而代理更关注控制访问和添加额外的逻辑。在实际编程中,选择使用委托还是代理取决于具体的需求和设计目标。

通过前面的介绍我们应该对代理和委托的概念和区别有了一定的认识。不过本篇文章并不是来探讨委托和代理之间的关系,而是来简单介绍Kotlin中的委托语法相关知识的。

使用Kotlin的by来进行委托

在Java中并没有专门的语法来帮助我们实现委托,而Kotlin中则十分贴心地提供了by关键字,通过这个关键字我们可以要求编译器生成粗略的代码来帮助我们实现委托。接下来给出一个最简单的例子来介绍by关键字的用法。

首先我们先定义一个Worker接口来定义打工人的职责:

interface Worker {fun work()fun takeVacation()
}

该接口有work和takeVacation(不存在的)两个方法。接下来定义两个类来实现这个接口:

class JavaProgramer : Worker{override fun work() {println("... write JavaCode ...")}override fun takeVacation() {println("JavaProgramer relax")}
}class CSharpProgramer : Worker{override fun work() {println("...writer CSharpCode ...")}override fun takeVacation() {println("CSharpProgramer relax")}
}

最后,我们还想要定义一个Manager类来管理所有的Worker,这种情况下我们就可以使用到Kotlin中的by关键字进行委托:

class Manager():Worker by JavaProgramer()//委托语法

是的,只需要这一行Manager就实现了委托,这种情况下我们调用Manager来调用方法最终就会路由到JavaProgramer的一个默认生成的实例中运行:

fun main() {val del = Manager()del.work()
}

最后的结果就是:
在这里插入图片描述
其实这样说可能不太清楚,我们来仔细分析一下class Manager():Worker by JavaProgramer()这行代码,首先类名后面用冒号跟上Worker接口的意思正是Manager类需要实现Worker接口,后面的JavaProgramer()代码就会自动生成一个JavaProgramer的实例,最后通过by连接,意思就是Manager类将会委托后面生成的这个JavaProgramer实例来实现Worker接口。

上面例子的局限

上面的这个例子的局限也十分明显,那就是由于委托的JavaProgramer实例是隐性生成的,所以我们就丢失了对委托的引用,这种情况下我们在这个Manager类就无法再次委托隐式生成的JavaProgramer来进行一些操作了。

委托给一个参数

上面我们提出了上面例子的局限性,不过只要理解了我们在上面分析的那一行代码的语法,我们可以很简单地避免上面的局限性。很显然要解决这个问题需要我们保留对被委托方的引用:

class Manager2(val mWorker: Worker):Worker by mWorker{fun fun1(){mWorker.work()}
}

在这段代码中我们保留了对被委托方的引用,通过幕后生成的mWorker字段存储了被委托方,后面的by mWorker一句表明这个Manager2类将会委托mWorker字段来实现Worker接口。

不要用var来修饰持有委托的字段

上面的代码中我们用val变量持有了传入的委托实例,当然编译器也是允许我们使用var变量来持有委托实例的,不过这样做存在风险。具体来说class Manager2(val mWorker: Worker):Worker by mWorker 实际上存储了两个对mWorker的引用,一个就是通过val生成的幕后字段,还有一个就是通过by生成的包装类中持有的委托引用。当我们用val修饰时不会有什么问题,但是如果当我们用var修饰就会存在隐患

比如我们这样写:

class Manager2(var mWorker: Worker):Worker by mWorker{fun change(){if(mWorker is JavaProgramer){mWorker = CSharpProgramer()}else{mWorker = JavaProgramer()}}fun showWorker(){println(mWorker.javaClass.simpleName)}
}

里面定义的change方法将会更改成员变量中的mWorker但是无法修改by语句的委托实例,我们运行一下这段代码查看结果:

fun main() {val del = Manager2(JavaProgramer())del.work()del.takeVacation()del.showWorker()del.change()del.work()del.takeVacation()del.showWorker()
}

最后结果为:
在这里插入图片描述
这里虽然成员变量中持有的Worker发生了变化,但是委托的Worker依旧是一开始创建的Worker,它无法被修改,这样就会造成语义的不清晰,所以说我们尽量不要用var变量来持有被委托的实例。

取消部分委托

当我们的委托方类没有和接口中的方法名一致的方法时,将不会有什么大问题,但是如果委托方中有一个方法实现了接口中的一些方法时就会和委托产生冲突。换句话说,如果我们只想要委托给一个实例实现接口中的部分方法时就需要处理掉一些冲突。比如我们在之前的例子中进行修改,如果Manager的代码如下就会产生冲突:

class Manager():Worker by JavaProgramer(){fun takeVacation(){ println("Manager Relax...")}
}//委托语法

这种情况下被委托方中的takeVacation方法就和Manager类中的takeVacation方法有了冲突,编译器无法决定是该调用被委托方的方法还是Manager中的方法。这时就需要在我们不需要进行代理的方法前加上override修饰符,如下:

class Manager():Worker by JavaProgramer(){override fun takeVacation(){ //解决方法冲突--取消委托println("Manager Relax...")}
}//委托语法

这种语法我们可以理解为单个方法取消委托,也可以理解为Manager类在实现接口的方法。总而言之,我们这样表达后冲突就不复存在了,当我们调用Manager类的takeVacation方法时就会调用Manager自身的方法而不是进行委托。

实现多个委托

上面的情况我们介绍的都是一个类委托给一个类实现,实际上一个类也可以委托给多个类实现多个接口,不过这种情况下可能会产生一些冲突需要我们手动处理。接下来我们修改Worker接口并且新增一个Assistant接口:

interface Worker {fun work()fun takeVacation()fun FishingTime()
}interface Assistant{fun doChores()fun FishingTime()
}

这样我们这两个接口就会有一个重叠的方法,现在我们创建一个类来实现多个委托:

class Manager3(val mWorker: Worker,val mAssistant: Assistant):Worker by mWorker,
Assistant by mAssistant{}

这样写将会产生报错,因为这两个接口有一个重叠的方法,用两个委托的话编译器将无法确定Manager3需要委托哪一个类来实现FishingTime方法,这里解决冲突的方法就是使用取消委托的方式,用override进行修饰决定到底需要哪一个委托:

class Manager3(val mWorker: Worker,val mAssistant: Assistant):Worker by mWorker,
Assistant by mAssistant{override fun FishingTime() {mWorker.FishingTime()mAssistant.FishingTime()}
}

这样就解决了冲突,同时实现了一个委托类委托给多个类实现的效果。

内置的标准委托

Lazy委托

在这里Lazy委托也可以被理解为懒加载,即只有在真正需要时才对一个函数进行调用,以达到节省开销延时加载计算的效果。比如说在布尔逻辑表达式中现在的大部分语言都有短路求值的特性,如果在表达式之前对表达式的求值足以产生结果,则跳过表达式的执行。比如:

fun workwork():String{println("execute")return "work work"
}fun main() {val msg = workwork()var shouldWork = falseif(shouldWork && msg != null){println("work day")}else{println("relax")}
}

在这种情况下显然就违反了短路原则,产生了额外的开销,运行结果是:
在这里插入图片描述
虽然msg未被使用,但是还是因为msg的赋值语句产生了额外的开销,这显然不是我们想要的。我们当然可以将赋值移到&&运算符之后,不过kotlin针对这种情况提供了lazy委托,让我们对上面的例子进行修改:

fun main() {val msg by lazy { workwork() } var shouldWork = falseif(shouldWork && msg != null){println("work day")}else{println("relax")}
}

这里我们用lazy委托包装了workwork函数,现在再来看运行结果:
在这里插入图片描述
额外的开销消失了,很神奇。这就是Kotlin中的lazy委托。Lazy委托后面接收一个lambda表达式,这样在我们需要用到委托方(在这里即为msg变量)时才会执行,否则将不会被执行,也就是说它是按需执行的。一旦对lambda中表达式求值,委托将记住结果,以后对该值的请求将接受保存的值而不是重新计算lambda表达式。

在默认情况下,lazy函数同步lambda表达式的运行,因此最多只有一个线程运行它。另外,Kotlin中的lazy委托只能用于val(不可变)变量,而不能用于var(可变)变量。这是因为lazy委托的特性与惰性求值相关,适用于只需要初始化一次并且后续不会再变化的情况。

Observable委托

接下来介绍的是Observable委托,看名字就知道这个委托和观察者模式密不可分。实际上也是这样。Observable委托将对关联的变量或者属性的修改进行拦截,发生修改时委托将调用我们用observable函数注册的事件处理程序上。

事件处理程序将接受三个类型为KProperty的参数,这些参数保存关于属性,旧值和新值的元数据,但是不返回任何值。我们直接用例子来说明:

fun main() {var count by observable(0){property, oldValue, newValue ->println("参数是:$property,旧值是:$oldValue,新值是:$newValue")}count++count++count++
}

这里我们用observable委托将count参数给委托了,observable括号中的0代表的是初始值,也就是count一开始为0,而当我们对count进行修改时就会触发后面的lambda表达式,我们来看看运行结果:
在这里插入图片描述
显然我们对count进行修改时就触发了这段lambda表达式,达到了观察的效果,感觉和JetPack中的LiveData也很相似。

vetoable委托

接下来介绍的是vetoable委托,和observable委托不同的是vetoable将返回一个Boolean类的值,如果返回true代表同意修改,否则就是拒绝修改。一旦拒绝修改,被委托的变量也就将停止修改,比如说:

fun main() {var count by vetoable(0){property, oldValue, newValue ->println("参数是:$property,旧值是:$oldValue,新值是:$newValue")oldValue < newValue}count++count--count--
}

这里我们对上面的例子稍作修改,lambda表达式最后一行的oldValue < newValue就是vetoable委托的最后返回值,当新值大于旧值时才同意修改,所以可以预见的是count–将不会生效,我们来看运行结果:
在这里插入图片描述
可以看到,后面两次修改果然没有生效。

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

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

相关文章

在C中使用Socket实现多线程异步TCP消息发送

目录 基础知识开始实现主要函数说明结束语 在本篇文章中&#xff0c;我们会探讨如何在C语言中使用socket来实现多线程&#xff0c;异步发送TCP消息的系统。虽然C标准库并没有原生支持异步和多线程编程&#xff0c;但是我们可以结合使用POSIX线程&#xff08;pthread&#xff09…

Java解决四大查找(一)

Java解决四大查找 一.线性查找1.1 题目1.2 思路分析1.3 代码演示 二.二分查找(双指针法)2.1 题目2.2 思路分析(图解加文字)2.3 代码演示 一.线性查找 1.1 题目 在数组{1&#xff0c;8&#xff0c;1024&#xff0c;521&#xff0c;1889}中查找数字8&#xff0c;如果有&#xff…

地址解析协议-ARP

ARP协议 无论网络层使用何种协议&#xff0c;在实际网络的链路上传输数据帧时&#xff0c;最终必须使用硬件地址 地址解析协议&#xff08;Address Resolution Protocol&#xff0c;ARP&#xff09;&#xff1a;完成IP地址到MAC地址的映射&#xff0c;每个主机都有一个ARP高速缓…

【数据结构】二叉树篇| 纲领思路02+刷题

博主简介&#xff1a;努力学习的22级计算机科学与技术本科生一枚&#x1f338;博主主页&#xff1a; 是瑶瑶子啦每日一言&#x1f33c;: 所谓自由&#xff0c;不是随心所欲&#xff0c;而是自我主宰。——康德 目录 一、前言二、刷题1、翻转二叉树 2、二叉树的层序遍历✨3、 二…

线性代数再回顾

最近&#xff0c;在深度学习线性代数&#xff0c;之前大一的时候学过线性代数&#xff0c;但那纯属于是应试用的&#xff0c;考试一考完&#xff0c;啥都忘了&#xff0c;也说出不出个所以然&#xff0c;所以&#xff0c;在B站的MIT的线性代数以及3blue1brown线性代数的本质中去…

git命令使用

君子拙于不知己,而信于知己。——司马迁 清屏&#xff1a;clear 查看当前面板的路径&#xff1a;pwd 查看当前面板的文件&#xff1a;ls 创建文件夹&#xff1a;mkdir 文件夹名 创建文件&#xff1a;touch 文件名 删除文件夹&#xff1a;rm -rf 文件夹名 删除文件&#xff1a;r…

Remote Sensing,2023 | 基于SBL的分布式毫米波相干雷达成像的高效实现

Remote Sensing,2023 | 基于SBL的分布式毫米波相干雷达成像的高效实现 注1&#xff1a;本文系“无线感知论文速递”系列之一&#xff0c;致力于简洁清晰完整地介绍、解读无线感知领域最新的顶会/顶刊论文(包括但不限于 Nature/Science及其子刊; MobiCom, Sigcom, MobiSys, NSDI…

爬虫IP时效问题:优化爬虫IP使用效果实用技巧

目录 1. 使用稳定的代理IP服务提供商&#xff1a; 2. 定期检测代理IP的可用性&#xff1a; 3. 配置合理的代理IP切换策略&#xff1a; 4. 使用代理IP池&#xff1a; 5. 考虑代理IP的地理位置和速度&#xff1a; 6. 设置合理的请求间隔和并发量&#xff1a; 总结 在爬虫过…

python知识:什么是字符编码?

前言 嗨喽&#xff0c;大家好呀~这里是爱看美女的茜茜呐 我们的MySQL使用latin1的默认字符集&#xff0c; 也就是说&#xff0c;对汉字字段直接使用GBK内码的编码进行存储&#xff0c; 当需要对一些有汉字的字段进行拼音排序时&#xff08;特别涉及到类似于名字这样的字段时…

Docker网络与资源控制

一、Docker 网络实现原理 Docker使用Linux桥接&#xff0c;在宿主机虚拟一个Docker容器网桥(docker0)&#xff0c;Docker启动一个容器时会根据Docker网桥的网段分配给容器一个IP地址&#xff0c;称为Container-IP&#xff0c;同时Docker网桥是每个容器的默认网关。因为在同一宿…

Oracle外部表ORACLE_LOADER方式加载数据

当数据源为文本或其它csv文件时&#xff0c;oracle可通过使用外部表加载数据方式&#xff0c;不需要导入可直接查询文件内的数据。 1、如下有一个文件名为&#xff1a;test1.txt 的数据文件。数据文件内容为&#xff1a; 2、使用sys授权hr用户可读写 DATA_PUMP_DIR 目录权限&a…

探索未来:元宇宙与Web3的无限可能

随着科技的奇迹般发展&#xff0c;互联网已经成为了我们生活的不可分割的一部分。然而&#xff0c;尽管它的便利性和普及性带来了巨大的影响&#xff0c;但我们仍然面临着传统互联网体验的诸多限制。 购物需要不断在实体店与电商平台间切换&#xff0c;教育依然受制于时间与地…

Unity如何把游戏导出成手机安装包

文章目录 前言使用环境步骤添加场景构建APK 前言 本文章主要演示了&#xff0c;如何将制作好的游戏&#xff0c;导出成APK&#xff0c;安装到手机上。 使用环境 Unity2022。 步骤 首先打开你的项目&#xff0c;然后选择菜单栏的“File” > “Build Settings…”&#xf…

QMainwindow窗口

QMainwindow窗口 菜单栏在二级菜单中输入中文的方法给菜单栏添加相应的动作使用QMenu类的API方法添加菜单项分隔符也是QAction类 工具栏添加工具栏在状态栏中添加控件工具栏添加其他类型的工具工具栏的属性添加多个工具栏使用窗口添加使用代码添加 状态栏常用API在状态栏显示信…

NeuralNLP-NeuralClassifier的使用记录(一),训练预测自己的【英文文本多分类】

NeuralNLP-NeuralClassifier的使用记录&#xff0c;训练预测自己的英文文本多分类 NeuralNLP-NeuralClassifier是腾讯开发的一个多层多分类应用工具&#xff0c;支持的任务包括&#xff0c;文本分类中的二分类、多分类、多标签&#xff0c;以及层次多标签分类。支持的文本编码…

C语言库函数之 qsort 讲解、使用及模拟实现

引入 我们在学习排序的时候&#xff0c;第一个接触到的应该都是冒泡排序&#xff0c;我们先来复习一下冒泡排序的代码&#xff0c;来作为一个铺垫和引入。 代码如下&#xff1a; #include<stdio.h>void bubble_sort(int *arr, int sz) {int i 0;for (i 0; i < sz…

面试热题(最大子数组和)

给你一个整数数组 nums &#xff0c;请你找出一个具有最大和的连续子数组&#xff08;子数组最少包含一个元素&#xff09;&#xff0c;返回其最大和。 子数组 是数组中的一个连续部分。 输入&#xff1a;nums [-2,1,-3,4,-1,2,1,-5,4] 输出&#xff1a;6 解释&#xff1a;连续…

免费批量ppt转pdf?一个方法教你完美转换

随着科技的不断发展&#xff0c;电子文档的使用越来越普遍。在商业、教育和个人领域&#xff0c;我们经常需要将PPT文件转换为PDF格式&#xff0c;以便更方便地共享和存档。幸运的是&#xff0c;现在有许多在线工具和软件可以帮助我们轻松地完成免费批量ppt转pdf。下面将介绍一…

【Linux】模拟实现linux的shell

#include <stdio.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include <sys/wait.h> #include <sys/types.h> #define NUM 1024 #define SIZE 32 #define SEP " " int main() {//保存输入后的字符串char …

Blazor前后端框架Known-V1.2.12

V1.2.12 Known是基于C#和Blazor开发的前后端分离快速开发框架&#xff0c;开箱即用&#xff0c;跨平台&#xff0c;一处代码&#xff0c;多处运行。 Gitee&#xff1a; https://gitee.com/known/KnownGithub&#xff1a;https://github.com/known/Known 概述 基于C#和Blazo…