写出优雅的Kotlin代码:聊聊我认为的 "Kotlinic"(续)

本文是 写出优雅的Kotlin代码:聊聊我认为的 "Kotlinic" 的续文。讲讲我在日常代码中认为可以写的更“Kotlinic”的写法们。

善用"="号

这里的意思其实包含了两个方面:用好Kotlin表达式的返回值,以及"="号做返回值的函数

Kotlin表达式返回值

学过Kt的大家都知道,不同于Java,它的if,else,try这些是带返回值的。在很多地方,使用 = 加上一个表达式的返回值看着可能会更清晰一些。

也就是,把类似于下面这种的“Java”式写法

val weekday = 6 // 一周第几天,[1, 7]
var dayDescription = ""
when(weekday){
    in 1..5 -> dayDescription = "工作日"
    in 6..7 -> dayDescription = "周末"
    else -> dayDescription = "世界末日"
}

改成下面的“Kotlin”写法

val weekday = 6 // 一周第几天,[1, 7]
val dayDescription = when(weekday){
    in 1..5 -> "工作日"
    in 6..7 -> "周末"
    else -> "世界末日"
}

在try...catch的场合,这样的差异会更明显。比如一个非常简易的读文件例子(下面的代码仅可以读取小文件,请谨慎地实际使用)

val filePath = "d://学习资料/日语资料.txt"
var result = ""
var inputStream: FileInputStream? = null
try {
    inputStream = FileInputStream(File(filePath))
    result = inputStream.readBytes().decodeToString()
}catch (e: Exception){
    result = "读取失败!"
    e.printStackTrace()
}finally {
    try {
        inputStream?.close()
    }catch (e: IOException){
        e.printStackTrace()
    }
}

这是个典型的Java写法,用Kotlin的写法会简洁些

val result = try {
    File(filePath).inputStream().use {
        it.readBytes().decodeToString()
    }
}catch (e: Exception){
    e.printStackTrace()
    "读取失败"
}

这两处代码间主要有下面几处变化:

  • result的赋值直接由try...catch语句的返回值提供
  • 文件流的关闭由Closeable.use拓展函数内部处理,外部调用更简洁
  • 使用其他拓展函数避免了嵌套的对象创建

如果你对中间读取的错误不关心,可以使用下面的形式

val result = runCatching {
    File(filePath).inputStream().use {
        it.readBytes().decodeToString()
    }
} .getOrDefault("读取失败")

runCatching函数返回一个Result对象,其getOrDefault方法可以在出错时使用给定的默认值(其他几个get方法包括getOrNullgetOrThrowgetOrElse)。这样写起来更简洁

不过,上面的写法还是不够Kotlin。借助Kt提供的琳琅满目的拓展函数,其实上面的代码可以写成这样

val result = runCatching {
    File(filePath).readText()
} .getOrDefault("读取失败")

在不需要考虑buffer的情况下,流都不需要管啦 :)

函数返回

用"="写函数其实在官方的各种拓展函数里非常常见。有一点比较有趣的是,因为Unit在Kotlin里面也是一种普通的类型,所以即使函数什么也不返回(也就是返回Unit),也可以拿"="写。不过这一点就因人而异了,得看实际情况。

对于一些非"unit"返回值的简单函数,用"="显得清晰明了

比如上面的获取dayDescription

fun getDayDescripton(weekday: Int) = when(weekday){
    in 1..5 -> "工作日"
    in 6..7 -> "周末"
    else -> "世界末日"
}

比如打log时可能要输个分割线

 // 重复字符串
inline operator fun String.times(n: Int) = this.repeat(n)

val divider = "=" * 20 // ******************** 

比如上面的读短文本

fun File.readText(default: String) = runCatching {
    this.readText()
} .getOrDefault(default)

类似的例子很多很多,就不赘述了。

杂项

Collection

kt的集合可以说是很强大了,该有的不该有的它都给了。随便举几个例子吧

创建

我经常看到类似这样的代码

val list = arrayListOf<int>()
list.add(1)
list.add(2)
list.add(3)

嗯,很Java。实际上创建一个列表,Kt有更好的方法。

带初始参数的arrayListOf

val list = arrayListOf(1, 2, 3)

要是复杂一点呢?比如值为index的平方?

val list = List(3) { i -> i*i } 

基于其他对象创建?

val names = students.map{ it.name }

基于另一个列表中某些符合要求的创建?

// 及格的同学们
val passedStudents = students.filter{ it.grade >= 60 }

转字符串

比如:["a", "b", "c"] -> "a, b, c"

val string = listOf("a","b","c").joinToString{ it }

也可以设置前后缀、分隔符等

val string = listOf("a","b","c").joinToString(prefix = "[", postfix = "]") { it }
println(string) // [a, b, c] 

Kt的Collection有很多很好用的方法,此处就不赘述了,大家感兴趣的自己翻翻源代码便是。

代理

Kotlin 的 by 应该说用的不少,它对应的概念“Delegate”也是语法上相较Java特别的地方。常见的用处呢,最简单的就是by lazy延迟初始化;除此之外,利用它也能快速实现一个“懒汉式”的单例(饿汉式的就object

class DataManager {
    companion object {
        val IMPL by lazy {
            DataManager()
        }
    }
}

放Java的话,上述代码语义上类似于

if(IMPL != null)return IMPL;
synchronized(lock) {
    if(IMPL == null){
        IMPL = new DataManager();
    }
    return IMPL;
} 

如果不需要锁,还可以加上参数 lazy(LazyThreadSafetyMode.NONE)

配合协程

如果懒加载的内容是耗时操作,还可以配合上协程,实现异步的懒加载

 /*
 * 异步懒加载,by FunnySaltyFish
*
* @param T 要加载的数据类型
* @param scope 加载时的协程作用域
* @param block 加载代码跨
* @return Lazy<Deferred<T>>
*/
fun <T> lazyPromise(scope: CoroutineScope = MainScope(), block: suspend CoroutineScope.() -> T) =
    lazy {
        scope.async(start = CoroutineStart.LAZY) {
            block.invoke(this)
        }
    } 

使用的时候才去加载数据,而且可以异步加载。

比如

private suspend fun fetchData() : String {
    println("开始加载数据")
    delay(1000)
    println("加载完毕")
    return "成功"
}

val username by lazyPromise(viewModelScope) {
    fetchData()
}
val password by lazyPromise(viewModelScope) {
    fetchData()
} 

而具体使用这两个变量的方法为

suspend fun login() = withContext(Dispatchers.IO) {
    username.start()
    password.start()
    println("${username.await()} ${password.await()} 登陆成功!")
} 

最后在调用这个函数(比如点击事件)时

onClick = {
    scope.launch {
        viewModel.login()
    }
} 

没有值时会去异步加载这个值,输出如下:

开始加载数据
开始加载数据
加载完毕
加载完毕
成功 成功 登陆成功!

后面再调用就直接使用已经初始化好的值,输出如下

成功 成功 登陆成功!

代理属性

代理的一个常见用法估计就是代理各种东东了

Map

class People(val map: Map<String, Any?>){
    val name: String by map
    val age: Int by map
}

val people = People(mapOf("name" to "FunnySaltyFish", "age" to 20))
println("${people.name}: ${people.age}") // FunnySaltyFish: 20

Intent

// 接收另一个activity传来的数据
val fromEntrance by intentData<String>("entrance")

数据库

// User是某个数据库的表,name age是两列
val name by User.name
val age by User.age

至此本文也差不多结束了,林林总总写了我一个多星期,感觉也是一个挺奇妙的过程。最后,如果你觉得我的内容还不错的话,欢迎点个赞,这对我帮助很大!

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿

猜你喜欢

转载自juejin.im/post/7125992160691748871