我正在参与「启航方案」

重视 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是支撑多编程范式的,而高阶函数、函数是一等公民这种思维,都是函数式编程的重要思维。在文中我介绍效果域时简略介绍的letapplyrunwithalso 这几个函数都是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中他会被折叠成十分美观的注释块:

你可能一直在kt文件中写Java代码



写在最终

文章最终咱们看一段 ”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。