Kotlin 基础回顾:变量、函数与类

说实话,刚接触离线优先架构时,我也以为自己只要会写几行 Kotlin 就能直接开干。结果写第一个 ViewModel 就被 val / var 的选择坑了,函数参数的默认值也忘了怎么传……嗯,这些基础就是地基,地基不稳,后面做 Room 和 Flow 时一定会出问题。咱们花一章速刷一遍,我边讲边踩坑。

Kotlin 基础三要素 Kotlin 基础 变量 var / val 函数 fun / 默认参数 类 class / data / object 类型推断 可空类型 ? 命名参数 Lambda 简化 数据类 伴生对象

1. 变量:val 与 var 的选择哲学

你先想想看,一个变量在它的生命周期里,值到底变不变?如果不变,就该用 val。我见过很多刚转 Kotlin 的同事,所有地方全写 var,结果代码里一堆不必要的 setter,还容易出 bug。我自己在写 Room 的 Entity 时,所有字段一律用 val,只有状态字段才会用 var。

关键字含义推荐场景
val只读引用(类似 Java 的 final)UI 状态、配置、常量
var可变引用临时计数器、用户输入、网络响应
📌 我的小技巧:创建一个对象后,先用 val 声明,编译报错了再改成 var。这样能强迫自己思考“为什么要变”。

类型推断

Kotlin 的 val name = "张三" 会自动推断为 String。但有些情况我会显式声明类型——比如在 Composable 函数里返回 State 对象时,显式类型能提高阅读性。直接看例子:

// 类型推断简洁
val message = "Hello"

// 显式声明更清晰(尤其当右侧表达式复杂时)
val count: Int = computeCount()
⚠️ 我曾经踩过的坑:用 var list = mutableListOf<String>() 然后到处 .add,结果被多个线程同时操作,数据混乱。后来统一改用 val list = listOf(...) 避免修改,状态管理改用 StateFlow。记住:不可变优先。

2. 函数:从普通方法到高阶函数

函数在 Kotlin 里就是一等公民。这不仅意味着你能把它塞进变量、传进参数,还意味着你能写出非常干净的 DSL 风格代码。来看看我经常在日常中用的写法:

声明与默认参数

// 默认参数 + 命名参数,调用时特别灵活
fun greet(name: String, prefix: String = "你好") {
    println("$prefix, $name")
}

// 调用方式 1:全部默认(必须传 name)
greet("小明")

// 调用方式 2:命名参数跳过中间
greet(name = "小红", prefix = "早上好")

为什么要用命名参数? 当我们有 4 个以上参数时,按位置传参很容易传错顺序。我个人习惯:参数超过 3 个或者有默认参数,一律用命名参数调用。这在写 Composable 时尤其常用——Text(text = "内容", style = MaterialTheme.typography.body1)。

单表达式函数

// 当函数体只有一行时,可以省略花括号和 return
fun double(x: Int): Int = x * 2

// 甚至还能结合扩展函数
fun Int.square(): Int = this * this

Lambda 与高阶函数

离线优先架构里大量用到 Flow、collect、map 等操作,本质上都是高阶函数。记住一个原则:如果 Lambda 是函数的最后一个参数,可以提到圆括号外面。

// 标准写法
list.filter({ it > 0 })

// 简化写法(lambda 为最后一个参数)
list.filter { it > 0 }

// 如果函数只有一个 lambda 参数,连圆括号都能省
observable.collect { value ->
    // 处理 value
}
🔍 关键点:在 Compose 或 Flow 的链式调用中,写好 Lambda 能让代码像流水线一样清晰。你写出来的 Repository 层几乎全是 fun getAll(): Flow<List<Item>> 这种形式,配合 map / filter 直接玩出花。

3. 类:构造、数据类与伴生对象

类在 Kotlin 里比 Java 简洁得多。我接触过的离线优先项目里,90% 的“实体类”都用 data class,因为 equals / hashCode / toString 自动生成,配合 Room 的 @Entity 省大事了。

主构造函数与属性

// 直接在类声明里定义属性
class User(val id: Int, var name: String)

你看,没有 getter / setter 的啰嗦。如果需要在初始化时做一些校验,可以用 init 块:

class User(val id: Int, var name: String) {
    init {
        require(id > 0) { "id 必须大于 0" }
    }
}

数据类

数据类是离线优先的核心工具之一。因为 Room 的 Entity 需要用数据类, Kotlin Serialization 也基于数据类。来看看一个标准的离线实体:

data class Task(
    val id: Int,
    val title: String,
    val isCompleted: Boolean = false,
    val createdAt: Long = System.currentTimeMillis()
)
⚠️ 注意:数据类的属性最好全部用 val。如果你用了 var,equals / hashCode 的结果会随属性变化而变化,这在 Room 或者 Set 里会引发诡异问题。我前阵子就因为一个 var 字段导致 RecyclerView 的 DiffUtil 失效……

伴生对象与静态成员

Kotlin 没有 static 关键字。用 companion object 代替。我习惯用它来存放工厂方法、常量或者数据库列名:

class User private constructor(val id: Int) {
    companion object {
        const val TABLE_NAME = "users"
        
        fun fromJson(json: String): User {
            // 反序列化逻辑
            return User(1)
        }
    }
}

// 调用方式看起来和静态方法一样
User.fromJson("...")
User.TABLE_NAME

另外,如果你只需要一个全局单例,用 object 关键字更简洁:

object AppDatabase {
    const val VERSION = 1
    fun getInstance() = this
}

4. 小总结:基础决定建筑高度

咱们这章讲的不多,但都是每天写代码时用的东西。变量选 val 还是 var、函数怎么传默认参数、类怎么定义数据模型——这些选择直接决定了你的代码是整洁还是乱糟糟。我在写离线优先的 Repository 层时,80% 的代码都是 fun + data class + val 的组合。把这些练熟了,后面的章节你会感觉很顺。

📋 快速自查
  1. 你的实体类是不是全部用 data class 了?
  2. 函数默认参数有没有用命名参数调用?
  3. 有没有地方可以用 val 代替 var?

好了,基础回顾结束。接下来每一章都会在这个地基上往上盖楼,做好准备。