kotlin实践及反思

前言

  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 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 ->
                              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实践及反思)