Kotlin中的by关键字不敢用?不会用?用不透?

828 阅读6分钟

写这篇文章的原因

发现组内很多写kotlin的人对by 这个关键字 都不太会用,最多也就是 by lazy 来findview一下。 但是网上一搜 关于by的文章 很多,我发现这些文章都有一个毛病 就是只讲了 这个by的 语法 对应的api 怎么用。 却从来没有一个人讲清楚 为什么要用by? 什么时候用by? 其实这些问题的根源就是 很多人对 代理模式 没有搞清楚,本质上只要你弄懂了代理模式,这个by的所有问题也就迎刃而解了。所以今天这篇文章就从 java版本的代理模式入手,帮你捋清楚kotlin中的by!

这段代码有问题吗?

public class UserController {
    //假设这来自依赖注入
    DataReportCollector dataReportCollector;


    public void login(String account,String password){
        long startTime=System.currentTimeMillis();
        //省略 真正登录的代码
        long endTime=System.currentTimeMillis();
        dataReportCollector.recordRequest(endTime-startTime);
    }

    public void register(String account,String password){
        long startTime=System.currentTimeMillis();
        //省略 真正注册的代码
        long endTime=System.currentTimeMillis();
        dataReportCollector.recordRequest(endTime-startTime);
    }

}

简单来说,我们有个userController 里面有基本的登录注册功能,然后我们还对登录注册 增加了性能统计的埋点。统计了这2个接口的时间。 从业务功能上来说 没有问题。但在写法上有2个问题:

1.性能统计的代码 和我们的登录注册代码耦合在一起,以后 性能统计的代码要变更 会非常麻烦,我们还需要跑到userController里面来对照修改, 2.性能统计和 用户的登录注册代码 本质上不是一个东西,违反了 职责单一 原则。

第一种改进方式

首先定义一个接口

public interface IUserController {
    void login(String account, String password);

    void register(String account, String password);
}

然后实现我们的 用户 功能

class UserController2 implements IUserController {

    @Override
    public void login(String account, String password) {
        //省略 登录逻辑
    }

    @Override
    public void register(String account, String password) {
        //省略注册逻辑
    }
}

最后 实现我们的代理类

class UserController2Proxy implements IUserController {


    private UserController2 userController2;

    public UserController2Proxy(UserController2 userController2) {
        this.userController2 = userController2;
    }

    //依赖注入
    DataReportCollector dataReportCollector;

    @Override
    public void login(String account, String password) {
        long startTime = System.currentTimeMillis();
        userController2.login(account, password);
        long endTime = System.currentTimeMillis();
        dataReportCollector.recordRequest(endTime - startTime);
    }

    @Override
    public void register(String account, String password) {
        long startTime = System.currentTimeMillis();
        userController2.register(account, password);
        long endTime = System.currentTimeMillis();
        dataReportCollector.recordRequest(endTime - startTime);
    }
}

实际使用的时候 我们就使用这个proxy 类就可以了。 你看这样是不是 就简单的 完成了我们解耦的目的了?

有没有更简单的改进方法?

上面那个方法其实 有个缺点, 就是如果 userController 的代码 不受你控制,你无法改变他的代码,他又没有 一个IUser的接口 那怎么办呢? 其实也是有办法的。

class UserControllerProxy extends UserController {
    //假设这来自依赖注入
    DataReportCollector dataReportCollector;

    @Override
    public void login(String account, String password) {
        long t1 = System.currentTimeMillis();
        super.login(account, password);
        long t2 = System.currentTimeMillis();
        dataReportCollector.recordRequest(t2 - t1);
    }

    @Override
    public void register(String account, String password) {
        long t1 = System.currentTimeMillis();
        super.register(account, password);
        long t2 = System.currentTimeMillis();
        dataReportCollector.recordRequest(t2 - t1);

    }
}

只要extends 原来的UserController 就可以了。 这种写法 就可以解决 源代码不受你控制的情况了, 当然在有选择的情况下,还是建议 优先选择 使用接口的方式 来完成 代理模式,因为 时刻谨记 组合大于继承

Kotlin 完成代理模式

我们有了java 版本的 代理模式经验以后 来完成kotlin的 就容易多了。

class KUserProxy(private val userController: UserController2) : IUserController {    
    //依赖注入                                                                           
    lateinit var dataReportCollector: DataReportCollector                            
    override fun login(account: String?, password: String?) {                        
        //这里的代码就省略了 和 register 是差不多的                                                 
    }                                                                                
                                                                                     
    override fun register(account: String?, password: String?) {                     
        val t1 = System.currentTimeMillis()                                          
        userController.register(account, password)                                   
        dataReportCollector.recordRequest(System.currentTimeMillis() - t1)           
    }                                                                                
}                                                                                    
                                                                                     

这个就是kotlin 版本的实现,也很简单, 有的人会问了, 哎呀 你烦死了 这个代理模式我已经会了 但是这和今天的by 关键字 有啥关系呢? 继续看,这次我们使用了by 关键字

class KUserProxy(private val userController: UserController2) : IUserController by userController {   
    //依赖注入                                                                                            
    lateinit var dataReportCollector: DataReportCollector                                             
    override fun register(account: String?, password: String?) {                                      
        val t1 = System.currentTimeMillis()                                                           
        userController.register(account, password)                                                    
        dataReportCollector.recordRequest(System.currentTimeMillis() - t1)                            
    }                                                                                                 
}                                                                                                     
                                                                                                      

这里解释一下 上面的程序, 我们使用了by 关键字以后 那么所有IUserController里面的 接口方法 在默认的情况下 都是使用了 userController的实现, 也就是使用了 我们传进去的 UserController2这个对象的实现。

那么有人又要问了,什么叫默认情况?

默认情况就是默认情况啊,比如上面的例子中,我们重写了register函数,这个就是非默认情况了,因为你重写了,所以register这个方法使用的就是你 重写里面的代码,是包含数据统计的。 但是你会发现 这里我们没有重写login方法, 那login方法默认实际运行的时候使用的就是UserController2的login代码了。

到这里有人又觉得奇怪了,好吧,看样子似乎有一点明白了,但是我仍旧没有感觉到你这个kotlin的 by 关键字方便在哪里啊?我们代理模式最终肯定还是要修改方法的,你要修改方法 不还是要重写方法么?不管你有没有使用by关键字 都得要重写方法啊,除非你想代理某个方法。

问题的关键就在这里了: 在实际开发中 我们的接口 往往可能不止一个方法,可能会有多个方法, 但我们在实际使用代理模式的时候,我们往往 不需要代理全部的方法,可能10个方法里面 只有2个方法 我们想代理用一下,其他都保持默认。 这种情况下,kotlin的 by关键字就很有用了,你使用了一个by关键字,其他默认的8个方法你都不需要管了,只需要重写你想代理的2个方法就可以。 但是如果你用java的代码来写,你需要将这10个方法都实现一遍,这种时候就很麻烦了。

想明白这个,你再看下 刚才那段kotlin代码的反编译代码 你对by关键字的理解 就超过8成了,剩下两成 无非是熟悉一下 by关键字的具体用法。

kotlin中的属性委托

有了上面的基础,我们再来学习属性委托 就简单许多了。 这里举几个简单的例子吧

class Delegate : ReadWriteProperty<Any, String> {                                               
    override fun getValue(thisRef: Any, property: KProperty<*>): String {                       
        //不管实际的值是什么 我只返回这个固定的字符串                                                                
        return "delegate string"                                                                
    }                                                                                           
                                                                                                
    override fun setValue(thisRef: Any, property: KProperty<*>, value: String) {                
        //打印一下 每次set的值                                                                          
        println("delegate get value$value")                                                     
    }                                                                                           
                                                                                                
}                                                                                               
class Dto {                                                                                     
    var p1: String by Delegate()                                                                
}                                                                                               
                                                                                                                                                                                               

实际使用一下;

                   
 var dto = Dto()   
 dto.p1 = "wuyue"  
 println(dto.p1)   

看下执行结果,有了前面代理模式的铺垫 要理解这个就不难了。

这种写法实际上比我们 自己重写get和set方法 逻辑上要清晰的多。

如果是val的话,因为值是不可变的,所以对应的就是readOnly了

我们还可以加一些监听,很方便的实现观察者模式

 var p2: String by Delegates.observable("要给对象一个默认值 否则第一次oldValue取不到"){                 
     property, oldValue, newValue ->                                                   
     println("property$property oldValue=$oldValue newValue=$newValue")                
 }                                                                                     

Delegates 还有一些其他的语法糖 比如 Delegates.null vetoable 等等 ,这些东西 随便搜搜 网上都有 我就不过多叙述了,反正你们理解了 代理模式, 理解这些东西就很容易。

by关键字 在android中 几个方便的使用小case

实现双击退出功能

 private var backPressTime by Delegates.observable(0L) { pre, old, new ->
        if (new - old < 2000) {
            finish()
        } else {
            //show toast 再按返回键就退出
        }

    }

    override fun onBackPressed() {
        backPressTime = System.currentTimeMillis()
    }


再举个例子 我们平常写代码的时候 经常要用到 SharedPreferences 对吧,每次对一个值 get set的时候 都要定义一个key,其实还是蛮麻烦的,有了by 这个关键字,我们轻松的 就可以解决这个问题了 让使用 sp就和 我们在dto中 取值 或者set值 是一样的

object SharedPreferencesUtils {
    //设置一个单例的 preferences
    var preferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(BaseApplication.getApplication())

    var appName by SharedPreferenceDelegates.string("defaultValue")

}

private object SharedPreferenceDelegates {
    fun string(defaultValue: String = "") = object : ReadWriteProperty<SharedPreferencesUtils, String> {
        override fun getValue(thisRef: SharedPreferencesUtils, property: KProperty<*>): String {
            return thisRef.preferences.getString(property.name, defaultValue) ?: ""
        }

        override fun setValue(thisRef: SharedPreferencesUtils, property: KProperty<*>, value: String) {
            thisRef.preferences.edit().putString(property.name, value).apply()
        }

    }
}