我正在参与「启航方案」
重视 Kotlin 的大多数开发中或许都是 Android 开发者吧,大家根本也都是慢慢从 Java 逐步迁移到 Kotlin。
得益于 Kotlin 与 Java 之间杰出的互通性,有的时分或许咱们写代码仍是比较随性的,尤其是依旧依照自己过去写 Java 的编程习惯,书写 Kotlin 代码。
但实际上 Kotlin 与 Java 之间编码风格仍是有很大的差异的,你的代码或许仍是 Java 的咖啡味。现在请你“暂时”忘掉 Java 编码规范,放下成见,看一下 Kotlin 有哪些有趣之处。
空判别
你大概早就听腻了 Kotlin 的空安全,可是你在代码里是否还在写if (xx != null)
这样满是咖啡味的代码呢?
现在把你的空判别代码都删去掉吧。运用 ?.
安全调用来操作你的目标。
// before
fun authWechat() {
if (api != null) {
if (!api.isWXAppInstalled) {
ToastUtils.showErrorToast("您还未装置微信客户端")
return
}
val req = SendAuth.Req()
req.scope = "snsapi_userinfo"
req.state = "none"
api.sendReq(req)
}
}
这段代码粗略看没什么问题吧,判别 IWXAPI
实例是否存在,存在的话判别是否装置了微信,未装置就 toast 提示
可是更契合 Kotlin 滋味的代码可所以这样的
// after
fun authWechat(callbackContext: CallbackContext?) {
val api = DsApplication.getDsInstance().wxapi
api?.takeIf { it.isWXAppInstalled }?.let {
// do something else
it.sendReq(
SendAuth.Req().apply {
scope = "snsapi_userinfo"
state = "none"
}
)
} ?: api?.run { ToastUtils.showErrorToast("您还未装置微信客户端") }
}
运用?.
安全调用合作 ?:
Elvis 表达式,能够覆盖悉数的空判别场景,再合作 takeIf
函数,能够让你的代码愈加易读(字面意思上的)
上述代码用文字表达其实便是:
可空目标?.takeIf{是否满意条件}?.let{不为空&满意条件时履行的代码块} ?: run { 为空|不满意条件履行的代码块 }
这样是不是愈加契合语义呢?
写在前面:关于代码风格的额定补充
鉴于这一段引起了一些谈论,特意将这段阐明移到前面,你假如有任何不同的观念,请先阅览完下面的阐明。
友爱的谈论我是欢迎的,可是请不要在谈论区输出情绪,不友爱的谈论我会直接删去。
1. 关于链式函数调用、函数式编程
#每天一个知识点# 之前我在 ⌈你或许一直在kt文件中写Java代码⌋ 中介绍过一些关于对目标非空判别然后调用的写法,有的jym表达了反对定见,认为这样写代码十分难以了解、是“屎山”代码,我不得不解释一下为什么要这样写。
kotlin是支撑多编程范式的,而高阶函数、函数是一等公民这种思维,都是函数式编程的重要思维。在文中我介绍效果域时简略介绍的
let
、apply
、run
、with
、also
这几个函数都是kotlin内置的高阶函数,运用它们本便是为了遵从函数式编程。函数式编程思维中有一个重要的理念,用我的了解表达的话:函数是数据变形的进程。假如你用过 RxJava,你应该能深刻了解这一点,在 RxJava 中有很多的中心操作符(链式函数调用),每一个中心操作符其实都是在对Observable
进行数据上的变形、或发生副效果。例如:
val other = obj?.let{}?.run{}?.takeIf{}
,这样的代码并不是什么屎山代码,而是一个十分典型的数据(目标)的变形进程,一个可空目标obj
,通过let
函数进行本身数据处理回来值又被run
函数接纳并处理(之前说过run
函数一般用于映射),最终发生的目标在通过takeIf
条件判别,契合条件则采用,不契合条件则为null,并且中心的每个环节都是空安全的、都能够随时被空中止履行进程。仅仅在沸点中三言两语或许无法让你彻底了解这种思维理念,我仍是之前对那位jy的回复:假如你对多个高阶函数链式调用觉得很难阅览、了解,或许是你不适合这种编程范式,而非这种写法不好,大可不必强求自己接纳这种编程范式。
2.关于运用?
空判别
?.
表明的是非空目标的传递,其传递路径能够被空中止,它与if-else
的流操控并不抵触,假如你的代码涉及到非空目标传递就用?.
。这一设计思维个人猜想来自 Haskell 中的包装类型,不同的是 Kotlin 没有运用一个详细的类型类来完成这一效果,而是直接从语法层进行了近似的功能完成,在kotlin中运用?
是一种规范的写法。假如你写过JS前端项目,对这种写法就不会有这么多的疑问。别的在 Java 中尽管没有从语法层支撑链式空判别,可是在 Java8 中引入的
Optional
类,便是用来完成这一效果的。其实Optional
类十分像 Haskell 中的 Monad。
效果域
仍是上面的例子,实例化一个req
目标
val req = SendAuth.Req()
req.scope = "snsapi_userinfo"
req.state = "none"
更有 Kotlin 滋味的代码应该是:
SendAuth.Req().apply {
scope = "snsapi_userinfo"
state = "none"
}
运用apply{}
函数能够帮咱们轻松的初始化目标,或许装备参数,它更好的组织了代码结构,明确了这个闭包处于某个目标的效果域内,所有的操作都是针对这个目标的。
在 Kotlin 的顶层函数中,提供了数个效果域函数,包括上文中的 let
函数,他们迥然不同,详细的运用其实更多看编码风格的取舍,例如在我司咱们有如下约好:
-
apply{}
用于写,修正、装备目标 -
with(obj){}
用于读,读取目标的字段,用于赋值给其他变量with()
能够显式的切换效果域,咱们常将它用于某个大的闭包内,完成部分的效果域切换,并且仅用作读时无需考虑效果域的入参命名问题 (多个嵌套的效果域函数往往会带来
it
的抵触) -
let{}
用于合作?.
用于非空安全调用,安全调用目标的函数 -
run{}
履行代码块、目标映射run
函数是有回来值的,其回来值是block
块的最终一行,所以它具备目标映射的能力,行将当时效果域映射为别的的目标 -
also{}
读目标,另作他用
当出现超越两行的同一目标运用,无论是读、写,咱们就应该考虑运用效果域函数,规范组织咱们的代码,使之更具有可读性。
这几个函数其实效果效果能够相互转换,故而这只关乎编码风格,而无关对错之分。
?:
Elvis 表达式
非空赋值
尽管说在 Kotlin 中可空目标,运用 ?.
能够轻松的安全调用,可是有的时分咱们需求一个默许值,这种状况咱们就需求用到 ?:
Elvis 表达式。
例如:
val name: String = getName() ?: "default"
假如 getName()
回来的是一个 String?
可空目标,当他为空时,通过 ?:
Elvis 表达式直接给予一个默许值。
合作 takeIf{}
完成特别的三元表达式
总所周知,kotlin 中没有三元表达式 条件 ? 真值 : 假值
,这一点其实比较惋惜,或许是因为 ?
被用作了空表达。
在kotlin 中咱们假如需求一个三元表达该怎么做呢?if 条件 真值 else 假值
,这样看起来也很简洁明晰。
还有一种比较特别的状况,便是咱们判别逻辑,实际上是这个目标是否满意什么条件,也便是说既要空判别,又要条件判别,回来的真值呢又是目标本身。
这种状况代码或许会是这样的:
fun getUser(): User? = null
fun useUser(user: User) {}
// 从一个函数中获得了可空目标
val _userNullable = getUser()
// 判别非空+条件,回来目标或许构造不契合条件的值
val user = if (_userNullable != null && _userNullable.user == "admin") {
_userNullable
} else {
User("guess")
}
//运用目标
useUser(user)
这个句子假如咱们将if-else
塞到 useUser()
函数中作为三元也不是不能够,可是看起来就比较乱了,并且咱们也不得不运用一个暂时变量_userNullable
。
假如咱们运用 ?:
Elvis 表达式 合作 takeIf{}
能够看起来更为高雅的表达
fun getUser(): User? = null
fun useUser(user: User) {}
// 运用`?:` Elvis 表达式简化的写法
useUser(getUser()?.takeIf { it.user == "admin" } ?: User("guest"))
这看起来就像是一个特别的三元 真值.takeIf(条件) ?: 假值
,在这种语义表达下,运用?:
Elvis 表达式起到了简化代码,明晰语义的效果。
提早回来
当然 ?:
Elvis 表达式还有许多其他用处,例如代码块的提早回来
fun View.onClickLike(user: String?, isGroup: Boolean = false) = this.setOnClickListener {
user?.takeUnless { it.isEmpty() } ?: return@setOnClickListener
StatisticsUtils.onClickLike(this.context, user, isGroup)
}
这儿咱们对入参进行了非空判别与字符长度判别,在?:
Elvis 表达式后提早 return
避免了后续代码被履行,这很高雅也更契合语义。
这儿不是说不能用 if
判别,那样尽管能够完成相同效果,可是额定增加了一层代码块嵌套,看起来不够整齐明晰。
这些运用本质上都是利用了 ?:
Elvis 表达式的特性,即前者为空时,履行后者。
运用函数目标
许多时分咱们的函数会被复用,或许作为参数传递,例如在 Android 一个点击事件的函数或许会被多次复用:
// before
btnA.setOnClickListener { sendEndCommand() }
btnB.setOnClickListener { sendEndCommand() }
btnC.setOnClickListener { sendEndCommand() }
例如这是三个不同帧布局中的三个结束按钮,他们对于的点击事件是同一个,这样写其实也没什么问题,可是他不够 Kotlin 味,咱们能够进一步改写
btnA.setOnClickListener(::sendEndCommand)
btnB.setOnClickListener(::sendEndCommand)
btnC.setOnClickListener(::sendEndCommand)
运用 ::
双冒号,将函数作为函数目标直接传递给一个接纳函数参数的函数(高阶函数),这对于很多运用高阶函数的链式调用场合愈加明晰明晰,也愈加函数式。
ps:这儿需求留意函数签名要对应,例如setOnClickListener
的函数签名是View->Unit
,故而咱们要修正函数与之共同
@JvmOverloads
fun sendEndCommand(@Suppress("UNUSED_PARAMETER") v: View? = null) {
}
运用 KDoc
你还在用 BugKotlinDocument 这样的插件帮你生成函数注释么?你的函数注释看起来是这样的么?
/**
* 获取悉数标题的正确率,x:标题序号,y:正确率数值(float)
* @param format ((quesNum: Int) -> String)? 格局化X轴label文字
* @param denominator Int 核算正确率运用的分母
* @return BarData?
*/
这样的注释看起来没什么问题,也能正确的定位到代码中的参数,但实际上这是 JavaDoc ,并不是 KDoc,KDoc运用的是相似 Markdown 语法,咱们能够改写成这样:
/**
* 获取悉数标题的正确率的BarData,其间,x:标题序号,y:正确率数值(float)。
* [format] 默许值为null,用于格局化X轴label文字,
* [denominator] 除数,作为核算正确率运用的分母,
* 回来值是直接能够用在BarChart中的[BarData]。
*/
KDoc 十分强大,你能够运用 “` 在注释块中写示例代码,或许JSON格局
例如:
/**
* 运用json填充视图的默许完成,有必要遵从下面的数据格局
* ```json
* [
* {"index":0,"answer":["对"]},
* {"index":1,"answer":["错"]},
* {"index":2,"answer":["对"]},
* ]
* ```
* [result] 有必要是一个JSONArray字符串
*/
在AS中他会被折叠成十分美观的注释块:
写在最终
文章最终咱们看一段 ”Java“ 代码与 Kotlin 代码的比照吧:
// before
override fun onResponse(
call: Call<AvatarPathResult?>,
response: Response<AvatarPathResult?>
) {
val avatarPathResult = response.body()
if (avatarPathResult != null) {
val status = avatarPathResult.status
if (status == 200) {
val data = avatarPathResult.data
MMKVUtils.saveAvatarPath(data)
} else {
MMKVUtils.saveAvatarPath("")
}
} else {
MMKVUtils.saveAvatarPath("")
}
}
// after
override fun onResponse(
call: Call<AvatarPathResult?>,
response: Response<AvatarPathResult?>,
) {
with(response.body()) {
MMKVUtils.saveAvatarPath(this?.data?.takeIf { status == 200 } ?: "")
}
}
鉴于有些同学对本文的观念有一些疑问,这儿我贴上 JetBrains 官方开发的 Ktor 项目中对各种语法糖运用的统计(根据 main 分支,23-6-9)
句子 | 计数 | 补白 |
---|---|---|
if.*!= null |
331 | 非空判别 |
if.*== null |
216 | 空判别 |
.let {} |
1210 | let效果域 |
?.let {} |
441 | ?非空 |
.apply {} |
469 | apply效果域 |
?.apply {} |
11 | ?非空 |
run {} |
37 | run效果域 |
with\(.*\) \{ |
219 | with效果域 |
.also{} |
119 | also效果域 |
?: |
1066 | Elvis |
?. |
1239 | ?.非空调用 |
\?\..*\?\. |
134 | ?.单行运用两次(非空传递) |
.takeIf |
54 | 链式判别 |
\.takeIf.*\?: |
13 | 链式判别合作Elvis |
.takeUnless |
2 | 链式判别(很少用) |
这个项目能够说很能代表 JetBrains 官方对 Kotlin 语法的一些观点与标准了吧,前文咱们也说了,怎么取舍只关乎编码风格,而无关对错之分。
用 Java 风格是错的吗?那天然不是,仅仅明显空判别与安全调用两者比较,安全调用更契合 Kotlin 的风格。
重复的写目标名是过错的么?天然也不是,仅仅运用 apply
更高雅更 Kotlin。