前言
- 已经在线上应用采用java和kotlin混编半年多,基本上逻辑代码全部采用kotlin进行实现。
- 使用kotlin从最开始的排斥、不屑到现在的完全适应、习惯,经历了很多变化。
- 这篇文章主要聊一聊kotlin开发过程中的一些反思以及个人认为的“最佳实践”。
- 这不是入门教程。
所谓的空指针安全
-
相信大多数开发者都和我一样,最开始听到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等
未完
网友评论