Kotlin修炼指南(五)—Delegates
委托,是一种比较常见的设计模式,通常采用接口或者抽象类的方式来实现,在Java代码中,一般使用接口来进行封装,而在kotlin中,可以通过委托机制来实现更加方便的委托模式。
Kotlin中的委托分为两种——类委托与属性委托,其中属性委托,是Kotlin非常强大的一个语法糖,借助这个功能,我们可以消除很多重复的模板代码,将Kotlin的代码榨干到极致。
类委托
下面我们先通过一个简单的例子来了解下什么是类委托,以及类委托的具体作用。
类委托入门
在一般的业务开发中,我们经常会遇到这样的场景——一个业务功能,有多种实现,通过接口来封装具体的业务方法,通过实现接口来完成不同实现,这样的场景有很多,使用Kotlin来实现这一功能,步骤如下。
第一步:创建接口约束,抽象业务场景。例如下面这个数据持久化的例子,我们通过接口定义了三个数据操作方法。
interface IDataPersistence { fun addData() fun delData() fun queryData() }
第二步:创建委托的实现,实现约束接口。数据持久化有多种不同的实现方式,下面这就是简单的两种,一种是通过SQL进行持久化,另一种是通过SharedPreferences进行持久化。
class SQL : IDataPersistence { override fun addData() { Log.d("xys", "addData with SQL") } override fun delData() { Log.d("xys", "delData with SQL") } override fun queryData() { Log.d("xys", "queryData with SQL") } } class SharedPreferences : IDataPersistence { override fun addData() { Log.d("xys", "addData with SharedPreferences") } override fun delData() { Log.d("xys", "delData with SharedPreferences") } override fun queryData() { Log.d("xys", "queryData with SharedPreferences") } }
第三步:调用约束接口,即业务方调用,但不用考虑具体的实现。类委托的语法格式是,<类>:<约束接口> by <实现类的实例>,即通过by关键字,将接口的实现,委托给一个具体的实例来作为自己的实现。
class MyDB(private val delegate: IDataPersistence) : IDataPersistence by delegate
使用方式与Java代码通过接口来实现基本一致,即在类初始化的时候,传入具体的实现类即可。
// val myDB = MyDB(SQL()) val myDB = MyDB(SharedPreferences()) myDB.addData() myDB.delData() myDB.queryData()
在Kotlin的类委托机制中,调用方和业务实现方,都需要实现约束接口,调用方只需要传入不同类型的业务实现方式,即可通过约束调用具体的实现。这一点看上去好像并没有比Java方便多少,但是在Kotlin中,在某些简单的场景下,实际上是可以省略掉实现类的,直接通过对委托实现的重写来实现委托接口,代码如下所示。
class MyDB(private val delegate: IDataPersistence) : IDataPersistence by delegate { override fun addData() {} override fun delData() {} override fun queryData() {} }
再简单一点,如果你不用传入多种不同的实例,可以在构造方法中去掉默认参数,直接在by关键字后面添加具体的接口实现,还是上面的例子,代码如下所示。
class MyDB : IDataPersistence by SQL() 调用: MyDB().addData()
通过委托,可以在不影响继承(MyDB可以继承其它类)的情况下,通过委托,使用指定接口中的方法。
类委托的原理
通过反编译Kotlin实现的代码,可以很方便的了解Kotlin内部是如何通过Java代码来实现委托机制的。
实际上就是在调用者内部创建一个实现者的变量,在实现的接口方法中,变量调用该方法,从而实现调用,一切都只是语法糖而已,Kotlin帮你简化了代码。
类委托的使用场景
通过类委托机制,可以很方便的实现多态。这是类委托最重要的使用场景,通过接口定义来实现多态性,同时使用by关键字来简化Java中接口实现的冗余代码,下面的这个简单的例子,就是一个最好的说明。
class RedSquare : Shape by Square(), Color by Red() { fun draw() { print("draw Square with Red") } }
另外,委托还可以用于在不修改原来代码及架构的基础上,对原有功能扩展或者修改。例如我们要对MutableList类拓展一个函数,如果是Java代码,或者不使用委托的Kotlin代码,你必须实现List接口中的所有函数,虽然你未作修改,只是单纯的传递调用,但是需要为这个拓展写很多无用的代码,而使用委托,则完全不用处理这些冗余,代码如下所示。
class NewList(private val list: MutableList<String>) : MutableList<String> by list { fun newFunction() {} }
Kotlin会自动在编译时帮你添加其它接口方法的默认实现。
属性委托
属性委托指的是一个类的某个属性值不是在类中直接进行定义,而是将其委托给一个代理类,从而实现对该类的属性统一管理,属性委托的一般格式如下所示。
val/var <属性名>: <类型> by <表达式>
在前面的讲解中,类委托,委托的是接口中指定的方法,而属性委托,则委托的是属性的get、set方法,属性委托实际上就是将get、set方法的逻辑委托给一个单独的类来进行实现(对于val属性来说,委托的是getValue方法,对于var属性来说,委托的是setValue和getValue方法)。
属性委托在那些需要对属性的get、set方法复用逻辑的场景下,是非常方便的,下面通过一个简单的例子来演示下属性委托机制。
首先,我们定义一个var属性,并将其委托给MyDelegate类,即将get和set方法进行了交接托管,因此,MyDelegate类需要重写getValue和setValue方法,为其提供新的返回值和逻辑,代码如下所示。
var delegateProp by MyDelegate() class MyDelegate { operator fun getValue(thisRef: Any?, property: KProperty<*>): String { return "MyDelegate get $thisRef ${property.name}" } operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { Log.d("xys", "MyDelegate set $value $thisRef ${property.name}") } } 调用: Log.d("xys", delegateProp) delegateProp = "abc" out: com.yw.demo D/xys: MyDelegate get com.yw.demo.MainActivity@595c528 delegateProp com.yw.demo D/xys: MyDelegate set abc com.yw.demo.MainActivity@595c528 delegateProp
这样处理之后,我们在使用delegateProp这个属性的时候,就会自动拓展MyDelegate中的处理。
不过呢,这样写起来太麻烦,MyDelegate中的方法都需要手动来实现,所以Kotlin提供了两个接口来帮助开发者实现。
所以上面的代码可以简写成下面这样。
class MyDelegate : ReadWriteProperty<Any, String> { override fun setValue(thisRef: Any, property: KProperty<*>, value: String) { Log.d("xys", "MyDelegate set $value $thisRef ${property.name}") } override fun getValue(thisRef: Any, property: KProperty<*>): String { return "MyDelegate get $thisRef ${property.name}" } }
属性委托使用场景
那么这东西有什么用呢,下面举个例子。
逻辑封装
例如对参数进行encode的操作。
object Prop { var encodeProp: String by EncodeProperty("init") } class EncodeProperty(var value: String) : ReadWriteProperty<Prop, String> { override fun getValue(thisRef: Prop, property: KProperty<*>): String { return "get encode prop output $value" } override fun setValue(thisRef: Prop, property: KProperty<*>, value: String) { this.value = value Log.d("xys", "save encode prop $value") } } 调用: Prop.encodeProp = "xuyisheng" Log.d("xys", Prop.encodeProp)
参数依然是那个参数变量,但是对它的处理被外包出去,交给了EncodeProperty来进行处理,这里的实现就是业务需要的encode操作,将来如果encode操作有改动,那么只需要修改EncodeProperty即可。也就是说,我们将encode的具体逻辑进行了封装,这样便于拓展和维护。
消除模板代码
再来看下面这个例子,Person类中有两个属性,分别修改了set方法,用于添加一些逻辑,代码如下所示。
class Person { var firstName: String = "" set(value) { field = value.toLowerCase() } var lastname: String = "" set(value) { field = value.toLowerCase() } } 调用: val person = Person() person.firstName = "XU" person.lastname = "YISHENG" println("${person.firstName} ${person.lastname}")
但是这里的两个属性的set方法,要处理的逻辑基本是一样的,即对字母做小写,所以我们对这个操作进行抽取,设置一个委托,代码如下所示。
class FormatDelegate : ReadWriteProperty<Any?, String> { private var formattedString: String = "" override fun getValue(thisRef: Any?, property: KProperty<*>): String { return formattedString } override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { formattedString = value.toLowerCase() } }
这个委托做的事情,和在前面的代码中set的逻辑是一样的。那么这个时候,就可以对Person类进行改造了,代码如下所示。
class Person { var firstName: String by FormatDelegate() var lastname: String by FormatDelegate() }
这样就将同样的set操作的逻辑,封装在了FormatDelegate中,从而实现了模板代码的消除。
抽象属性委托的一般步骤
从上面的例子我们可以发现,其实只要是对属性的get、set方法有操作的地方,几乎都可以使用属性委托来简化,对于这种操作,开发者一般会经历下面几个过程。
青铜:抽取公共函数,在处理时对属性进行调用
黄金:重新属性的get、set函数,将逻辑封装
王者:使用属性委托,将逻辑抽取出来
下面再通过一个实例,来演示下这个步骤。我们以Fragment的启动方式为例来讲解,经常有写类似的代码来处理Fragment的启动。
const val PARAM1 = "param1" const val PARAM2 = "param2" class DemoFragment : Fragment() { private var param1: Int? = null private var param2: String? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) arguments?.let { args -> param1 = args.getInt(PARAM1) param2 = args.getString(PARAM2) } } companion object { fun newInstance(param1: Int, param2: String): DemoFragment = DemoFragment().apply { arguments = Bundle().apply { putInt(PARAM1, param1) putString(PARAM2, param2) } } } }
首先,我们可以通过Kotlin的set、get函数进行改写,将arguments的填充,放到属性的get、set函数内部,代码如下所示。
class DemoFragment : Fragment() { private var param1: Int? get() = arguments?.getInt(PARAM1) set(value) { value?.let { arguments?.putInt(PARAM1, it) } } private var param2: String? get() = arguments?.getString(PARAM2) set(value) { arguments?.putString(PARAM2, value) } companion object { fun newInstance(param1: Int, param2: String): DemoFragment = DemoFragment().apply { this.param1 = param1 this.param2 = param2 } } }
但是我们还是要为每个属性写重复的代码,特别是当属性很多的时候,每个属性都要写重复的put、get函数,所以,下面使用委托对这个逻辑再进行一次封装,代码如下所示。
class DemoFragment : Fragment() { private var param1: Int by FragmentArgumentDelegate() private var param2: String by FragmentArgumentDelegate() companion object { fun newInstance(param1: Int, param2: String): DemoFragment = DemoFragment().apply { this.param1 = param1 this.param2 = param2 } } } @Suppress("UNCHECKED_CAST") class FragmentArgumentDelegate<T : Any> : ReadWriteProperty<Fragment, T> { override fun getValue(thisRef: Fragment, property: KProperty<*>): T { val key = property.name return thisRef.arguments?.get(key) as T } override fun setValue(thisRef: Fragment, property: KProperty<*>, value: T) { val arguments = thisRef.arguments val key = property.name arguments?.put(key, value) } } fun <T> Bundle.put(key: String, value: T) { when (value) { is Boolean -> putBoolean(key, value) is String -> putString(key, value) is Int -> putInt(key, value) is Short -> putShort(key, value) is Long -> putLong(key, value) is Byte -> putByte(key, value) is ByteArray -> putByteArray(key, value) is Char -> putChar(key, value) is CharArray -> putCharArray(key, value) is CharSequence -> putCharSequence(key, value) is Float -> putFloat(key, value) is Bundle -> putBundle(key, value) is Parcelable -> putParcelable(key, value) is Serializable -> putSerializable(key, value) else -> throw IllegalStateException("Type of property $key is not supported") } }
这里要注意的是,Bundle没有提供单个属性的put拓展,所以我们需要自己实现一个。
通过上面的这些操作,就将Fragment参数传递的代码简化到了只有一行,其它任何的Fragment传参,都可以使用这个委托。
委托操作实例
最后,再介绍一些官方推荐的委托使用场景。
内置委托函数
Kotlin系统库提供了很多有用的委托,这些都内置在Delegate库中。
延迟属性lazy
属性委托,可以是一个表达式,借助这个特性,可以实现属性的延迟加载,即在第一次访问的时候进行初始化。
private val lazyProp: String by lazy { Log.d("xys", "表达式只会执行一次") "执行后赋值给lazyProp" } Log.d("xys", lazyProp) Log.d("xys", lazyProp) out: D/xys: 表达式只会执行一次 D/xys: 执行后赋值给lazyProp D/xys: 执行后赋值给lazyProp
要注意的是,lazy表达式中的代码,只会在第一次初始化的时候调用,之后就不会调用了,所以这里log只打印了一次。
观察属性observable
Delegates.observable可以非常方便的帮助我们实现观察者模式,代码如下所示。
var observableProp: String by Delegates.observable("init value 0") { property, oldValue, newValue -> Log.d("xys", "change: $property: $oldValue -> $newValue ") } Log.d("xys", observableProp) observableProp = "change value"
当属性值发生改变的时候,就会通知出来。
借助观察属性,可以很方便的实现时间差的判断,例如连续back退出的功能,代码如下所示。
private var backPressedTime by Delegates.observable(0L) { pre, old, new -> if (new - old < 2000) { finish() } else { Toast.makeText(this, "再按一次返回退出", Toast.LENGTH_SHORT).show() } } override fun onBackPressed() { backPressedTime = System.currentTimeMillis() }
条件观察属性vetoable
vetoable 与 observable一样,可以观察属性值的变化,不同的是,vetoable可以通过处理器函数来决定属性值是否生效,代码如下所示。
var vetoableProp: Int by Delegates.vetoable(0){ _, oldValue, newValue -> // 如果新的值大于旧值,则生效 newValue > oldValue }
SharedPreferences操作简化
前面我们提到了,只要是涉及到get、set方法的使用的场景,几乎都可以使用委托来进行优化,再拓展一下,凡是对属性有进行读写操作的,都可以使用委托来进行优化,例如我们在Android中比较常用的SharedPreferences操作,大部分情况下,都会抽取工具类,类似下面这样进行调用。
PreferencesUtil.getInstance().putBoolean(XXXXX, false);
下面通过委托,我们可以将一个普通属性的读写进行代理,代理到通过SP读写,这样我们在代码中对这个属性的读写,实际上是将其代理到SP中,代码如下所示。
@Suppress("UNCHECKED_CAST") class PreferenceDelegate<T>(private val context: Context, private val propName: String, private val defaultValue: T) : ReadWriteProperty<Any, T> { private val sharedPreferences: SharedPreferences by lazy { context.getSharedPreferences("SP_NAME", Context.MODE_PRIVATE) } override fun setValue(thisRef: Any, property: KProperty<*>, value: T) { value?.let { putSPValue(propName, value) } } override fun getValue(thisRef: Any, property: KProperty<*>): T { return getSPValue(propName, defaultValue) ?: defaultValue } private fun <T> getSPValue(name: String, defaultValue: T): T? = with(sharedPreferences) { val result = when (defaultValue) { is String -> getString(name, defaultValue) is Int -> getInt(name, defaultValue) is Long -> getLong(name, defaultValue) is Float -> getFloat(name, defaultValue) is Boolean -> getBoolean(name, defaultValue) else -> null } result as T } private fun <T> putSPValue(name: String, value: T) = with(sharedPreferences.edit()) { when (value) { is Long -> putLong(name, value) is String -> putString(name, value) is Int -> putInt(name, value) is Boolean -> putBoolean(name, value) is Float -> putFloat(name, value) else -> null } }?.apply() } 使用: var valueInSP: String by PreferenceDelegate(this, "test", "init") Log.d("xys", valueInSP) valueInSP = "new value" Log.d("xys", valueInSP) out: D/xys: init D/xys: new value
通过上面的操作,我们在使用SharedPreferences的时候,只需要对某个要操作的属性使用by进行标记,将其委托给PreferenceDelegate即可,这样表面上好像是在操作一个String,但实际上,已经是对SharedPreferences的操作了。
在下面这个lib中,对很多场景下的委托进行了封装,大家可以参考下它的实现。
https://github.com/fengzhizi715/SAF-Object-Delegate
向大家推荐下我的网站 https://xuyisheng.top/ 点击原文一键直达
专注 Android-Kotlin-Flutter 欢迎大家访问
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
微软承认五月累积更新导致用户无法登陆Teams和Outlook等应用
本月补丁星期二活动日中,微软面向尚处于支持状态的 Windows 10 功能更新发布了累积更新,为蓝牙以及其他核心组件提供了安全修复。不过,部分用户反馈称安装五月累积更新之后,无法正常登录 Microsoft Teams 和 Outlook 应用。目前微软已经承认这个问题,并承诺会尽快修复。 微软承认在安装五月累积更新之后,用户在连接到 Teams、Outlook 和 OneDrive for Business 时可能面临登录问题。在日志中写道:“受影响的用户可能会看到以下错误:‘我们很抱歉,我们遇到了一个问题’。在某些情况下,用户在尝试连接或者登录的时候会出现‘80080300’错误”。微软表示该错误只影响安装 KB5003169 累积更新的 Windows 10 Version 1909 功能更新。 微软承认:“目前的遥测结果表明,一小部分用户无法连接到各种 Microsoft 365 桌面客户端”。如果你经常收到这个错误,微软建议手动重新启动计算机。这个错误特别令人沮丧,因为它将有效地阻止你访问你的 Microsoft Teams 和其他 Office 365 客户端。有些人只需...
- 下一篇
mysql5.6升级及mysql无密码登录
mysql5.6升级 mysql5.6的升级可以分为以下几个步骤: 安全关闭正在运行的MySQL实例 把/usr/local/mysql 的连接由MySQL5.6更改为MySQL5.7 启动MySQL实例,查看是否是MySQL5.7版本 使用mysql_upgrade命令升级系统表 首先:停止当前运行的MySQL实例,然后做如下操作 更改之后启动MySQL实例: [root@test3 local]# service mysqld start Starting MySQL.. SUCCESS! [root@test3 local]# netstat -lntp Active Internet connections (only servers) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 31179/sshd tcp 0 0 :::22 :::* LISTEN 31179/sshd tcp 0 0 :::3000 ...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Mario游戏-低调大师作品
- CentOS6,CentOS7官方镜像安装Oracle11G
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- Windows10,CentOS7,CentOS8安装Nodejs环境
- Docker安装Oracle12C,快速搭建Oracle学习环境
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- CentOS8编译安装MySQL8.0.19
- MySQL8.0.19开启GTID主从同步CentOS8
- CentOS8安装Docker,最新的服务器搭配容器使用
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7