美文网首页
kotlin实践及反思

kotlin实践及反思

作者: wgyscsf | 来源:发表于2019-06-14 23:30 被阅读0次

    前言

    1. 已经在线上应用采用java和kotlin混编半年多,基本上逻辑代码全部采用kotlin进行实现。
    2. 使用kotlin从最开始的排斥、不屑到现在的完全适应、习惯,经历了很多变化。
    3. 这篇文章主要聊一聊kotlin开发过程中的一些反思以及个人认为的“最佳实践”。
    4. 这不是入门教程。

    所谓的空指针安全

    • 相信大多数开发者都和我一样,最开始听到kt的介绍就是空指针安全,包括google的IO大会也在说这个特性。声明一个String类型的成员变量大概有以下几种方式,不知道大家平时用的是哪一种?

            var a: String = ""
            var b: String? = null
            lateinit var c: String
            val d: String by lazy {
                "xxx"
            }
      
    • a个人感觉这是不规范的(当然确实有实际意义的默认值除外),这个默认值没有任何意义,仅仅试了实现语法上的“不为空”,方便使用而已。至少我不会这样去使用。不要为了语法方便随意给默认值

    • b这是一种可空类型的String字符串,当我们要给它赋值的时候,直接就可以赋值,很方便。但是当我们要用它的时候就比较麻烦了,必须b?.xxx的形式调用String的实例方法。可以保证空指针安全,但是这样是有问题的!比如现在有一个严格的计算一个人的账户资金的问题,大概形式如下sumFund1()。本来计算资金是一个非常严谨的问题,从user到account以及fund全部都不为null的,如果出现任何一个为null,说明逻辑有严重问题,但是这一种形式可能就会把这个问题隐藏掉,非常不易于排查。sumFund2()则是主动抛出这个异常,指明具体错误原因,上层catch到这个错误上报即可。xxx?.xxx?.xxx形式一时爽,出了问题难排查

        var mUser: User? = null
        fun sumFund1() {
            val user = mUser
            val fund = user?.mAccount?.mFund
            val sum = fund?.plus(3)
            println(sum)
        }
        
        fun sumFund2() {
            val user = mUser ?: throw NullPointerException("user is null")
            val account = user.mAccount ?: throw NullPointerException("account is null")
            val fund = account.mFund ?: throw NullPointerException("fund is null")
            val sum = fund + 3
            println(sum)
        }
      
    • c这是一种kt给开发者自己做空指针校验的操作符。你可以声明一个不即时初始化的成员变量,什么时候初始化开发者自己做决定,但是你要用之前没有初始化,kt会毫不留情的给你抛出一个运行时的kotlin.UninitializedPropertyAccessException: lateinit property xxx has not been initialized的错误。lateinit感觉就像DataType?=null的对立面一样,它可以让我们的程序更加严谨,同时也对开发者对数据的处理要求更高。个人是比较喜欢这一种操作成员变量的方式,这种方式可以明显避免程序中?满天飞。能用lateinit不用?=null,严格保证程序逻辑严谨性

    • d延迟加载不可变成员参数。d的获取和前面几种方式有明显的不同,是val也就是不可变成员参数。不可变参数有很多好处,我们可以方便大胆操作不会有任何多线程问题。并且在kt中实现了延迟加载,更加方便了开发者使用。简直是“最佳实践”!当然,不可变参数有很多应用上的局限,还得可变参数去实现。能用val不用var,能用lateinit不用?=null,避免?满天飞

    • 总结:kt给数据类型分区“可空”数据类型和“不可空”数据类型,并且全部都是标准的数据类型,没有java中的类似于int、long这种基本数据类型,统一了数据类型。让我们在使用的时候更加安全,但是在实际开发的时候很容易滥用。应该根据不同场景采用不同的方式(默认值、可空、延迟加载、懒加载等等)去初始化参数,并且应该明白各种方式的优缺点。

    Smart cast to 'xxx' is impossible, because 'xxx' is a mutable property that could have been changed by this time

    • 这一种错误在开发中很常见,大致形式如下testSmartCast1,而testSmartCast2则是允许的

         var mUser2: User? = null
            //报错
            fun testSmartCast1() {
                if (mUser2 == null) throw NullPointerException("mUser2 is null,please check!")
                val account = mUser2.mAccount
            }
        
        //不报错
        fun testSmartCast2() {
                val user = mUser2
                if (user == null) throw NullPointerException("mUser2 is null,please check!")
                val account = user.mAccount
            }
      
    • 为什么testSmartCast1报错,我不是已经判空了吗?因为多线程!kt不知道你这个方法是否会在多线程中调用,如果确实在多线程中调用,确实有null的可能不是吗?

    • 那为什么testSmartCast2没有问题?这就涉及到java基本功了,局部变量user指向mUser2,再去操作user。这个时候即使mUser2在其它线程中进行修改,也不会影响user。这种场景也很容易模拟,模拟如下

        //点击
        an_b_smartCast2.setOnClickListener {
                    mUser2 = User()
                    testSmartCast2()
                    mUser2 = null
                }
        
        //fun
        fun testSmartCast2() {
                val user = mUser2
                Log.d(TAG, "testSmartCast2:${Thread.currentThread()}, $user,$mUser2")
                Thread {
                    Thread.sleep(3000)
                    Log.d(TAG, "testSmartCast2:${Thread.currentThread()}, $user,$mUser2")
                }.start()
            }
         
        //输出
        04-20 17:45:13.830 6601-6601/com.gy.myapplication D/NpeActivity: testSmartCast2:Thread[main,5,main], com.gy.myapplication.User@946ae6b,com.gy.myapplication.User@946ae6b
        04-20 17:45:16.832 6601-6626/com.gy.myapplication D/NpeActivity: testSmartCast2:Thread[Thread-376,5,main], com.gy.myapplication.User@946ae6b,null
      
    • 现在想想我们之前用java写的代码是不是有类似的存在逻辑上的不严谨???kt会在语言层面保证你操作对象的空指针安全。另外,在一些java最佳实践的书中,也比较推荐的在方法中不直接操作成员变量,而是赋值给一个局部变量,再去操作对应的局部变量。

    彻底化的函数式编程

    • 不知道你有没有这样的感觉,lamdba用着不爽。阅读性差应该是最大的原因。

        //java 匿名函数  
        findViewById(R.id.af_b_btn1).setOnClickListener(new View.OnClickListener() {
                            @Override
                            public void onClick(View v) {
                                Log.d(TAG, "onClick1: ");
                            }
                        });
        //java lamdba
        findViewById(R.id.af_b_btn2).setOnClickListener(v -> Log.d(TAG, "onClick2: "));
                 
        //kt 匿名函数
        af_b_btn1.setOnClickListener(object : View.OnClickListener {
                            override fun onClick(v: View?) {
                                Log.d(TAG, "onClick1: ")
                            }
                        })
                        
        //kt lamdba
        af_b_btn1.setOnClickListener { Log.d(TAG, "onClick2: ") }
      
    • 至少最开始我接触函数式编程的时候是这样的感觉的,非常排斥。看了很多相关文章,为啥这玩意这么火?各个语言都在用,java从8开始也引入这玩意,kotlin从开始都支持。 后来就逐渐接受,并喜欢上。

    • 阅读性差:所谓的阅读性差,个人感觉可能是java代码写多了,更加注重“过程”。为什么这么说?比如上面的点击事件,看java的匿名方式,我知道参数(View)、处理过程(这里其实就是简单回调方法)、以及返回参数(void)。可谓标准的一个java方法。其实可以再简单点,我们只需要关注开始和结束,简化中间过程,简单化。

    • 举个例子,在一个字符串中过滤出可转化为int类型的再转化为int类然后过滤出>3,最后求和。下面是采用原始的java自己写算法和采用kt的内置函数式去当作数据流处理(jdk8开始java也有对应数据流)。用标准的数据流处理方,可以看出函数式有更加清晰的特点。

        //java sum
        findViewById(R.id.af_b_btn3).setOnClickListener(v -> {
                    String[] list = new String[]{"1", "2", "a", "..", "3", "--", "4", "5", "6"};
                    int sum = 0;
                    for (String s : list) {
                        try {
                            int i = Integer.parseInt(s);
                            if (i <= 3) continue;
                            sum += i;
                        } catch (NumberFormatException e) {
                            e.printStackTrace();
                        }
                    }
                    Log.d(TAG, "initListener: " + sum);
                });
        
        //kt fun        
        af_b_btn3.setOnClickListener {
                    val list = listOf("1", "2", "a", "..", "3", "--", "4", "5", "6")
                    val sum = list.filter { isInt(it) }.map { it.toInt() }.filter { it > 3 }.sum()
                    Log.d(TAG, ": $sum")
                }
        
         private fun isInt(it: String): Boolean {
                return try {
                    it.toInt()
                    true
                } catch (e: NumberFormatException) {
                    e.printStackTrace()
                    false
                }
            }
      
    • 有接触过后端数据库的会知道查询select * from user where id="xxx"。开发者只用采用特定语句告诉数据库“我想要查询用户xxx的信息,具体你怎么查(具体查询算法细节)我不管”。其实调用后端的API也是类似,只需要给出指定API及参数,就可以获取到对应信息,简化或者忽略中间过程

    • kt有更加彻底的函数式编程。用过Rxjava的都知道,如果不采用lamdba,写这玩意会崩溃,各种回调,能写很多很多模板代码。可能因为历史原因,java的lamdba比较繁琐一点,相对而言kt会简单很多。看下面功能一样并且都采用lamdba的例子:

        @SuppressLint("CheckResult")
            private void sumFun() {
                String[] list = new String[]{"1", "2", "a", "..", "3", "--", "4", "5", "6"};
                List<String> strings = Arrays.asList(list);
                Observable
                        .fromIterable(strings).filter(s -> isInt(s))
                        .map(Integer::parseInt)
                        .filter(integer -> integer > 3)
                        .toList()
                        .map(integers -> sum(integers))
                        .flatMapObservable((Function<Integer, ObservableSource<Integer>>) integer ->
                                Observable.just(integer))
                        .subscribe(
                                it -> Log.d(TAG, ": " + it),
                                t -> Log.e(TAG, ": " + t.getLocalizedMessage()),
                                () -> Log.d(TAG, ":compelt ")
                        );
            }
        
        //kt
        @SuppressLint("CheckResult")
            private fun sumFun() {
                val list = listOf("1", "2", "a", "..", "3", "--", "4", "5", "6")
                list
                    .toObservable()
                    .filter { isInt(it) }
                    .map { it.toInt() }
                    .filter { it > 3 }
                    .toList()
                    .map { it.sum() }
                    .flatMapObservable { Observable.just(it) }
                    .subscribe(
                        { Log.d(TAG, ": $it") },
                        { Log.e(TAG, ": " + it.localizedMessage) },
                        { Log.d(TAG, ":compelt ")
                    })
            }
      

    run、with、let等

    未完

    相关文章

      网友评论

          本文标题:kotlin实践及反思

          本文链接:https://www.haomeiwen.com/subject/ucxvfctx.html