Kotlin 老手怎么写代码?

如果你是一名安卓开发者,你很可能喜爱 Kotlin 语言。它简洁、安全,改变了我们以往使用 Java 的开发方式。

最新的跨平台 Compose,已经完全基于 Kotlin 开发了。

那么,如何像一个 Kotlin 老手写代码呢?

或者当你去面试,跟面试官说,我是个十年老手!

下面,我会展示一些 Kotlin 用法,让你的代码更简洁、更易读、更高效,让你看起来像一个写了十年 Kotlin 代码的老手。

Inline + reified

还记得 Java 的类型擦除吗?

如果将 List<User> 这样的泛型类型传递给一个函数,在运行时,该函数只知道它接收到的是一个 List

因为类型擦除的关系,在实际运行的时候,你是无法知道一个泛型的确切类型。

所以,下面这段代码,是无法编译的:

Kotlin 复制代码
// 判断 obj 是否是 T 类型
fun <T> isType(obj: Any) = obj is T

Kotlin 中带有实化类型参数的内联函数巧妙地解决了这个问题。

将一个函数标记为inline,你告诉编译器将函数代码直接复制到调用点,再加上 reified 关键字,类型信息会在调用点保留下来,从而在运行时可以访问。

上述代码稍作修改:

Kotlin 复制代码
// inline + reified
inline fun <reified T> isType(obj:Any) = obj is T 

// 判断是不是 String 类型
println(isType<String>("Kotlin"))
// 判断是不是 Number 类型
println(isType<Number>(2))

我们甚至可以使用泛型去创建实例:

Kotlin 复制代码
inline fun <reified T> newOne(): T {
    return T::class.java.newInstance()
}

val a:String = newOne() // 创建一个新的 String

这对于 JSON 序列化等场景而言,非常方便:

Kotlin 复制代码
// 再也不需要在使用 Gson 的时候,传递 class 了。
inline fun <reified T> Gson.fromJson(json: String): T =
    this.fromJson(json, T::class.java)
// 优雅而简洁
val user = gson.fromJson<User>("""{"name": "Prakash"}""")

这在构建类型安全的 API 以及在使用诸如 GsonMoshi 等依赖反射的库时减少样板代码方面非常有用。

扩展

我们都喜欢扩展函数。能够向现有类添加新函数而无需继承它们,这是 Kotlin 的强大功能之一。

例如 Kotlin 官方提供的:

Kotlin 复制代码
str.isNullOrBlank()

该方法可以判断字符串是否为 null、为空字符,或者仅由空白字符组成。

但你知道吗,除了扩展函数以外,还有扩展属性。

扩展属性允许你给任何类"添加"新的只读属性。它们的行为与普通属性无异,但其值是通过一个 getter 方法来计算的。

Kotlin 复制代码
// 向String类添加一个"firstChar"属性 
val String.firstChar: Char get() = this[0] 

// 像使用原生属性一样使用它 
println("Kotlin".firstChar) // 输出:K

这对于创建简洁实用的逻辑非常理想。相比于像 String.getFirstChar() 这样的函数调用,使用更具可读性的属性访问 String.firstChar 让你的代码更具语义性,尤其是在为无法修改的第三方类或框架类添加简单计算值时更是如此 。

这在 Compose 中比较常见,Compose 中可以这样使用尺寸:

Kotlin 复制代码
3.dp
5.sp

我只能说,还有谁!

作用域函数

你肯定用过 letapplyrunwithalso。但很容易将它们视为可以互换的。它们真正的强大之处在于依据其特定意图来使用它们。每一个函数都旨在以尽可能简洁的方式处理一种常见的编码模式。

apply 为例。它的作用是配置一个对象。它在一个对象上运行一段代码块,并返回对象本身。这对于设置复杂对象或构建器来说非常合适。

让我们将它与 also 结合使用,also 用于在不改变对象的情况下执行其它操作(比如日志记录或调试)。

Kotlin 复制代码
data class Config(var host: String = "", var port: Int = 0)

val config = Config().apply {
    host = "localhost"
    port = 8080
}.also {
    // it 指的是当前的 Config 对象
    println("Config created: $it")

使用恰当的作用域函数能让你的代码意图一目了然。

例如使用 apply 进行配置,使用 let 对变量进行空安全操作,同时也可用于执行其它操作。

这就像是为每项小任务都准备了专门的工具。

如果在 IO 操作中使用相关作用域函数,代码会非常方便:

Kotlin 复制代码
// 注意,use 并不是作用域函数。它能自动 try-close 流
File("").inputStream().use { ios -> 
    val os = File("").outputStream().use { os ->
        var len = 0
        val bytes = ByteArray(1024)
        val byteBuffer = (File("").writer())
        // 使用 also 给 len 赋值,同时还能判断读操作是否可用,一举两得
        while (ios.read(bytes).also { len = it } > -1) { 
            os.write(bytes, 0, len)
        }
    }
}

操作符重载

这一点可能会让你感觉像是在重写语言本身,而这正是它强大的原因。

运算符重载允许你为自己的自定义类型定义诸如 +-* 甚至数组访问 [] 等标准运算符的含义。

你无需编写 a.add(b),只需写 a + b 即可。这使得诸如向量数学运算、矩阵运算或自定义数据结构相关的代码变得极其直观且易于阅读。

Kotlin 复制代码
data class Vec2(val x: Int, val y: Int) {
    // 定义 '+' 运算符对 Vec2 的操作
    operator fun plus(other: Vec2) = Vec2(x + other.x, y + other.y)
    // 定义 '*' 运算符对 Vec2 的操作
    operator fun times(times: Int): Vec2 {
        return Vec2(x * times, y * times)
    }
}

val a = Vec2(1, 2)
val b = Vec2(3, 4)
// 这段代码现在简洁又直观
val c = a + b 
println(c) // Vec2(x = 4, y = 6) 
println(c * 4) // Vec2(x=16, y=24)

如果经过深思熟虑后使用,这能够产生极具表现力的 API 和 DSL,使用起来会感觉非常自然。

本地函数

有时你有一段逻辑,你想写一个辅助方法,而该辅助方法其实只在一个函数内部使用。在 Java 中,你可能会把它设为类的私有方法。但这仍然会用一个用途非常有限的函数使类的作用域变得杂乱。因为通常情况下,能提出方法的,多半是在其他地方能够复用的,而如果只在一个函数内部复用,提一个类的私有方法,多少有点大材小用了。

Kotlin 允许你在其他函数内部声明函数。

这是一个实现封装的出色工具。辅助函数只存在于需要它的地方,不会意外地从其他地方被调用。它还可以访问外部函数的局部变量,这能进一步简化操作。

Kotlin 复制代码
fun validateAndSaveUser(user: User) {

    // 这个函数只存在于validateAndSaveUser内部
    fun isValidEmail(email: String): Boolean {
        return email.isNotBlank() && email.contains("@")
    }

    if (isValidEmail(user.email)) {
        // 保存用户
    }
}

我通常在 Compose 中这么用

Kotlin 复制代码
@Composable
fun HomePage(
    entry: NavBackStackEntry,
    vm: HomeViewModel = viewModel(),
    scope: CoroutineScope = rememberCoroutineScope(),
) {
    
    fun gotoHomeScreen() {
       // 跳转到 home tab
    }

    fun previewWork(ui: WorksUIItem) {
        // 预览
    }

    fun goSettingPage() {
        // 去另一个页面
    }

    BackHandler {
        gotoHomeScreen()
    }
    
    HomeTitleBar(
        index = homePager.currentPage,
        onSettingPage = ::goSettingPage,
        onPreview = ::previewWork
    )

}

这是一个简单的特性,它可以帮助你将复杂的函数分解成更小的、可管理的且完全封装的部分,而不会污染类的命名空间。

解构

这是一种一次性将对象解包为多个变量的简洁方法。它开箱即用地适用于数据类、二元组、三元组,甚至映射项。

不用这样写:

kotlin 复制代码
data class User(val name: String, val age: Int)

val user = getUser()
val name = user.name
val age = user.age

你可以这样写:

kotlin 复制代码
val (name, age) = getUser()

Kotlin 中自带的 Pair,就支持这种写法:

Kotlin 复制代码
val pair = Pair("Alice", 30)
val (name, age) = pair // 解构声明
println("Name: $name, Age: $age")

中缀表达式

Kotlin 的中缀表达式是一种特殊的函数调用方式,它允许你用更接近自然语言或数学表达式的语法来调用函数,使代码更清晰易读。

当你想创建一个 Pair 的时候,你可以使用两种写法:

Kotlin 复制代码
val annaBook = Pair("Anna's diary", 30.49f) // 普通的构建方法
val book = "Anna's diary" to 30.49f // 使用中缀表达式

当然,我们也可以让自己编写的类支持中缀表达式:

Kotlin 复制代码
// 一个简单的配置
class Config {
    var host: String = ""
    var port: Int = 0
    var password: String = ""
}

// 扩展函数,用于设置配置项
infix fun Config.host(value: String) {
    this.host = value
}

infix fun Config.port(value: Int) {
    this.port = value
}

infix fun Config.password(value: String) {
    this.password = value
}

fun main() {
    
    val cfg = Config().also { // 使用 also 来集体配置
        it host "127.0.0.1"
        it port 8987
    }

    cfg password "Pe*45U3n^bIha" // 单独配置密码
}

如果你在 MVI 架构模式中,对 Redux 方法应用了中缀表达式方法,那么你就会写出表达力极强的代码:

Kotlin 复制代码
enum class Button {
    Home,Back
}

sealed interface Intent

data object Refresh : Intent
data class LogIn(val token: String) : Intent

class MainViewModel {
    infix fun clicked(button: Button) {
        // TODO
    }
    infix fun want(intent: Intent) {
        // TODO
    }
}

fun main() {
    val i = MainViewModel()
    
    i clicked Home // 我点击了 Home 按键
    i want Refresh // 我想刷新页面
}

总结

跳出常规用法,充分利用 Kotlin 的强大特性,善用这些强大工具,你的代码不仅实用,更将优雅而富有表现力,宛如 Kotlin 老手的手笔。

相关推荐
Nayuta1 小时前
字节跳动「移动 OS 部门」招聘安卓工程师,AI+OS 方向
android
00后程序员张1 小时前
iOS 应用上架常见问题与解决方案,多工具组合的实战经验
android·ios·小程序·https·uni-app·iphone·webview
恋猫de小郭2 小时前
Flutter 小技巧之有趣的 UI 骨架屏框架 skeletonizer
android·前端·flutter
张风捷特烈4 小时前
鸿蒙纪·Flutter卷#03 | 从配置证书到打包发布
android·flutter·harmonyos
技术liul15 小时前
使用安卓平板,通过USB数据线(而不是Wi-Fi)来控制电脑(版本1)
android·stm32·电脑
扛麻袋的少年15 小时前
7.Kotlin的日期类
开发语言·微信·kotlin
_祝你今天愉快17 小时前
Android FrameWork - 开机启动 & Init 进程 初探
android
2501_9160074717 小时前
iOS App 上架实战 从内测到应用商店发布的全周期流程解析
android·ios·小程序·https·uni-app·iphone·webview
TimeFine17 小时前
Android 邮件发送日志
android