ECC Kotlin 开发模式指南:从空安全到协程的惯用法实战手册
导读
本篇文章基于 ECC 仓库中的 kotlin-patterns 技能文档(位于 .kiro/skills/kotlin-patterns/SKILL.md,同时以同一名称托管于 skills/kotlin-patterns/SKILL.md),系统讲解构建健壮、高效、可维护 Kotlin 应用的惯用模式:涵盖空安全类型系统、不可变性与数据类、密封类穷尽分支、协程结构化并发、扩展函数、类型安全 DSL、委托与惰性序列,以及 Gradle Kotlin DSL 工程配置。文中所有概念均结合仓库内的工程级约束文件(如 rules/kotlin/patterns.md、skills/kotlin-coroutines-flows/SKILL.md)与代码评审规范交叉印证。读完你将获得一套可以直接照抄的可运行代码模板,并理解每条惯用法背后的取舍与反模式。
一、这套模式的定位:何时使用、如何运作
kotlin-patterns 技能文档把“惯用 Kotlin”收敛为七大关键领域的工程约束:
- 利用类型系统与安全调用运算符实现的空安全;
- 以
val与数据类copy()为核心的默认不可变; - 面向穷尽类型层级建模的密封类与密封接口;
- 基于协程与
Flow的结构化并发; - 以扩展函数实现的无继承能力扩展;
- 使用
@DslMarker与 lambda 接收者构造的类型安全 DSL; - 面向构建配置的 Gradle Kotlin DSL。
该技能的使用场景被界定为五类:编写新 Kotlin 代码、评审已有代码、重构存量代码、设计 Kotlin 模块/库,以及配置 Gradle Kotlin DSL 构建。这与仓库中 kotlin-reviewer 评审 Agent(见 agents/kotlin-reviewer.md)在架构评审时的关注点高度一致——该 Agent 将 !! 滥用、var 可用却用 val、非穷尽 when、对外暴露可变集合等列为需要上报的“Kotlin Idioms (MEDIUM)”检查项,恰好是本文讨论反模式的机器可执行版本。
二、空安全:让类型系统替你消灭 NPE
Kotlin 类型系统在编译期区分可空与不可空类型,这是与 Java 最大的生产力差异之一。正确姿势是默认使用不可空类型,仅在语义上确实可能缺失时才声明可空,并用安全调用 ?. 与 Elvis ?: 收口。
// Good: 使用不可空类型作为默认
fun getUser(id: String): User {
return userRepository.findById(id)
?: throw UserNotFoundException("User $id not found")
}
// Good: 安全调用与 Elvis 运算符
fun getUserEmail(userId: String): String {
val user = userRepository.findById(userId)
return user?.email ?: "unknown@example.com"
}
// Bad: 强解包可空类型
fun getUserEmail(userId: String): String {
val user = userRepository.findById(userId)
return user!!.email // Throws NPE if null
}
几条实践要点:
?:的右侧不一定是字面量,可以是表达式或抛出的异常,用于把“数据缺失”收敛为“明确的失败语义”;- 与 Java 互操作时,平台类型(platform type)的可空性不可信。仓库评审规范在 rules/kotlin/patterns.md 与反模式清单中均强调 Java 传值必须显式判空,
kotlin-reviewerAgent 也会对任何非空断言给出提示,推荐改用?.、?:、requireNotNull或checkNotNull; - 空安全最易被破坏的场景是“构造期校验缺失”,此时应配合
require/check前置条件函数(见后文错误处理章节)。
三、不可变优先:val、copy() 与不可变集合
在 Kotlin 中“能 val 就 val”,数据结构优先使用不可变集合,状态更新通过数据类 copy() 产生新实例而非原地修改。
// Good: 不可变数据
data class User(
val id: String,
val name: String,
val email: String,
)
// Good: 用 copy() 做转换
fun updateEmail(user: User, newEmail: String): User =
user.copy(email = newEmail)
// Good: 不可变集合
val users: List<User> = listOf(user1, user2)
val filtered = users.filter { it.email.isNotBlank() }
// Bad: 可变全局状态
var currentUser: User? = null // 尽量避免
val mutableUsers = mutableListOf<User>() // 仅在确有需要时使用
不可变约定不仅是代码风格问题,而是并发安全的基础设施:当它与 StateFlow 组合时(典型 UI 状态管理),对状态内的可变集合做原地 add 不会触发重组/订阅方感知。因此 rules/kotlin/patterns.md 的 Repository 层约定“suspend 函数返回 Result<T> 或自定义错误类型、用 Flow 暴露响应式流”,而 skills/kotlin-coroutines-flows/SKILL.md 的反模式清单明确写着:MutableStateFlow 中存放可变集合时必须用不可变拷贝更新,即 _state.update { it.copy(list = it.list + newItem) }。这也与 agents/kotlin-reviewer.md 中的 HIGH 级检查项“StateFlow holding mutable list”相互印证。
四、表达式体与单表达式函数:简洁而不失可读
能用表达式体就不要写块体,when 作为表达式时天然穷尽且返回值:
// Good: 表达式体
fun isAdult(age: Int): Boolean = age >= 18
fun formatFullName(first: String, last: String): String =
"$first $last".trim()
fun User.displayName(): String =
name.ifBlank { email.substringBefore('@') }
// Good: when 作为表达式
fun statusMessage(code: Int): String = when (code) {
200 -> "OK"
404 -> "Not Found"
500 -> "Internal Server Error"
else -> "Unknown status: $code"
}
// Bad: 不必要的块体
fun isAdult(age: Int): Boolean {
return age >= 18
}
注意上例把扩展函数(User.displayName())与表达式体结合使用,是第四、第六两个主题的自然交汇——详见后文扩展函数章节。
五、数据类与值类:值对象的两种建模层次
数据类自动获得 equals/hashCode/toString/componentN/copy,适合承载“以数据为主”的载体类型;而 @JvmInline 值类则在不引入运行时装箱开销的前提下提供类型安全包装。
// Good: 数据类——自带 copy、equals、hashCode、toString
data class CreateUserRequest(
val name: String,
val email: String,
val role: Role = Role.USER,
)
// Good: 值类——零运行时开销的类型安全包装
@JvmInline
value class UserId(val value: String) {
init {
require(value.isNotBlank()) { "UserId cannot be blank" }
}
}
@JvmInline
value class Email(val value: String) {
init {
require('@' in value) { "Invalid email: $value" }
}
}
fun getUser(id: UserId): User = userRepository.findById(id)
两个建模技巧值得专门说明:
- 值类 +
init约束:把合法性校验推进构造函数,让非法状态“构造不出来”,即所谓的“让非法状态不可表示”。Email构造时即验证@存在,后续所有使用方无需重复防御; - 值类的
init校验配合第 11 节的require使用,是前置条件风格最内聚的应用点。
六、密封类与密封接口:用穷尽分支管理受限类型层级
6.1 受限层级建模:Result 类型
对“成功 / 失败 / 加载中”这类受限状态集合,密封类是首选,因为它让 when 具备穷尽性(exhaustive)——编译器在新增子类型后强制所有分支同步更新。
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Failure(val error: AppError) : Result<Nothing>()
data object Loading : Result<Nothing>()
}
fun <T> Result<T>.getOrNull(): T? = when (this) {
is Result.Success -> data
is Result.Failure -> null
is Result.Loading -> null
}
fun <T> Result<T>.getOrThrow(): T = when (this) {
is Result.Success -> data
is Result.Failure -> throw error.toException()
is Result.Loading -> throw IllegalStateException("Still loading")
}
几点语义细节:
- 泛型参数声明为
out T(协变),配合Result<Nothing>让无数据变体可以安放在任何具体类型的层级下; data object Loading(Kotlin 1.9+ 的data object)保证Loading单例语义;- 把
when的每个分支都做成带名函数(getOrNull/getOrThrow),避免在业务代码里到处展开原始when。
6.2 密封接口建模 API 错误:错误即类型
仓库的 error-handling 惯例倾向“用类型表达错误,而不是用异常表达控制流”。密封接口可以把一整族 API 错误压缩为一个可穷尽的类型系统:
sealed interface ApiError {
val message: String
data class NotFound(override val message: String) : ApiError
data class Unauthorized(override val message: String) : ApiError
data class Validation(
override val message: String,
val field: String,
) : ApiError
data class Internal(
override val message: String,
val cause: Throwable? = null,
) : ApiError
}
fun ApiError.toStatusCode(): Int = when (this) {
is ApiError.NotFound -> 404
is ApiError.Unauthorized -> 401
is ApiError.Validation -> 422
is ApiError.Internal -> 500
}
模式亮点:通过 toStatusCode() 扩展函数把错误类型映射为 HTTP 状态码,映射逻辑内聚、无分支遗漏——这正是 kotlin-reviewer Agent 检查“when 对密封类型是否穷尽”想要在代码库里强制出的形态。
七、作用域函数:五种工具各司其职
let / apply / also / run / with 是 Kotlin 出镜率最高的五个函数,选择依据是返回什么与以何种方式接收对象:
// let: 转换可空值或作用域内结果
val length: Int? = name?.let { it.trim().length }
// apply: 配置对象(返回对象本身)
val user = User().apply {
name = "Alice"
email = "alice@example.com"
}
// also: 执行副作用(返回对象本身)
val user = createUser(request).also { logger.info("Created user: ${it.id}") }
// run: 以接收者执行代码块(返回块的结果)
val result = connection.run {
prepareStatement(sql)
executeQuery()
}
// with: run 的非扩展形式
val csv = with(StringBuilder()) {
appendLine("name,email")
users.forEach { appendLine("${it.name},${it.email}") }
toString()
}
记忆口诀可以概括为:想改对象配置用 apply,想记日志/埋点用 also,想在可空对象上取计算结果用 let,想复用接收者上下文并返回结果用 run,with 是脱离可空接收者的 run。
反模式:嵌套作用域函数会严重伤害可读性。
// Bad: 嵌套作用域函数
user?.let { u ->
u.address?.let { addr ->
addr.city?.let { city ->
println(city) // 难以阅读
}
}
}
// Good: 直接链式安全调用
val city = user?.address?.city
city?.let { println(it) }
八、扩展函数:不靠继承增加行为
扩展函数让“外部给类型加方法”成为可能,适合领域化工具、集合语义补充,以及把类型相关逻辑收拢在类内部(用私有扩展避免污染全局命名空间):
// Good: 领域化扩展
fun String.toSlug(): String =
lowercase()
.replace(Regex("[^a-z0-9\\s-]"), "")
.replace(Regex("\\s+"), "-")
.trim('-')
fun Instant.toLocalDate(zone: ZoneId = ZoneId.systemDefault()): LocalDate =
atZone(zone).toLocalDate()
// Good: 集合扩展
fun <T> List<T>.second(): T = this[1]
fun <T> List<T>.secondOrNull(): T? = getOrNull(1)
// Good: 局部作用域的扩展(不污染全局命名空间)
class UserService {
private fun User.isActive(): Boolean =
status == Status.ACTIVE && lastLogin.isAfter(Instant.now().minus(30, ChronoUnit.DAYS))
fun getActiveUsers(): List<User> = userRepository.findAll().filter { it.isActive() }
}
工程上推荐把扩展函数声明在“离使用者最近”的作用域:全局复用的放在顶层文件;只服务于某个类内部的放在该类的私有成员区。仓库里也能看到同样的组织风格——例如 rules/kotlin/patterns.md 用 operator fun invoke 把 UseCase 调用点压缩为一次直接调用(见第 12 节示例),而 agents/kotlin-reviewer.md 则明确反对“Java 风格静态工具类”,主张改用顶层函数。
九、协程与 Flow:结构化并发与冷流
9.1 coroutineScope vs supervisorScope
结构化并发的核心承诺是:子协程的生命周期被父作用域约束,取消会沿结构传播。
// Good: coroutineScope——任一子任务失败会取消整个作用域
suspend fun fetchUserWithPosts(userId: String): UserProfile =
coroutineScope {
val userDeferred = async { userService.getUser(userId) }
val postsDeferred = async { postService.getUserPosts(userId) }
UserProfile(
user = userDeferred.await(),
posts = postsDeferred.await(),
)
}
// Good: supervisorScope——子任务可独立失败
suspend fun fetchDashboard(userId: String): Dashboard =
supervisorScope {
val user = async { userService.getUser(userId) }
val notifications = async { notificationService.getRecent(userId) }
val recommendations = async { recommendationService.getFor(userId) }
Dashboard(
user = user.await(),
notifications = try {
notifications.await()
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
emptyList()
},
recommendations = try {
recommendations.await()
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
emptyList()
},
)
}
Dashboard 示例是仓库规约里反复出现的标准手法(可对照 skills/kotlin-coroutines-flows/SKILL.md 中 loadDashboard 的并行分解与 supervisorScope 示例):对可降级的子任务单独 try/catch,且必须先重抛 CancellationException——吞掉取消异常会破坏结构化取消语义。这是 kotlin-reviewer Agent 的 HIGH 级检查项,原文给出的对照是:
// BAD —— 吞掉了取消
try { fetchData() } catch (e: Exception) { log(e) }
// GOOD —— 保留取消传播
try { fetchData() } catch (e: CancellationException) { throw e } catch (e: Exception) { log(e) }
9.2 Flow:面向响应式数据流的冷流
flow { } 构造的是冷流——每次收集都会重新执行生产者代码,这使其天然适合“订阅数据库变化”“轮询”等场景。错误处理应在流内用 .catch 收敛:
fun observeUsers(): Flow<List<User>> = flow {
while (currentCoroutineContext().isActive) {
val users = userRepository.findAll()
emit(users)
delay(5.seconds)
}
}.catch { e ->
logger.error("Error observing users", e)
emit(emptyList())
}
fun searchUsers(query: Flow<String>): Flow<List<User>> =
query
.debounce(300.milliseconds)
.distinctUntilChanged()
.filter { it.length >= 2 }
.mapLatest { q -> userRepository.search(q) }
.catch { emit(emptyList()) }
搜索场景的管道 debounce → distinctUntilChanged → filter → mapLatest 是 UI 层输入流的标准打法,其中 mapLatest 保证只有最新一次查询结果会被下游接收。从仓库实现证据看,这一主题被独立沉淀为专门技能 skills/kotlin-coroutines-flows/SKILL.md,其中补充了三个本技能文档未展开、但工程上几乎必然遇到的知识点:
StateFlow化冷流:stateIn(scope, SharingStarted.WhileSubscribed(5_000), initialValue),最后一个订阅者离开后仍保持上游活跃 5 秒,可扛过配置变更而不重启数据源;- 一次性事件用
MutableSharedFlow:如 Snackbar、导航这类“只应消费一次”的事件不应放入会保留最新值的StateFlow; - 调度器选择:CPU 密集用
Dispatchers.Default,IO 密集用Dispatchers.IO(该调度器在 JVM/Android 可用;KMP 其他平台需用Default或经 DI 提供)。
9.3 取消与清理
长时间循环体必须在每个昂贵步骤前检查取消状态;资源获取后即便被取消也必须释放,且释放动作要放进 NonCancellable 上下文,否则 finally 里的挂起调用在取消时同样会被中断:
// Good: 尊重取消
suspend fun processItems(items: List<Item>) {
items.forEach { item ->
ensureActive() // 在昂贵工作前检查取消
processItem(item)
}
}
// Good: 用 try/finally 清理
suspend fun acquireAndProcess() {
val resource = acquireResource()
try {
resource.process()
} finally {
withContext(NonCancellable) {
resource.release() // 即使被取消也始终释放
}
}
}
十、委托:属性的懒加载、可观察与 Map 映射
10.1 属性委托
标准库内置三类高频委托:lazy(懒初始化)、Delegates.observable(变化监听)、by map(把属性映射到 Map 键值,常用于反序列化配置对象):
// 懒初始化
val expensiveData: List<User> by lazy {
userRepository.findAll()
}
// 可观察属性
var name: String by Delegates.observable("initial") { _, old, new ->
logger.info("Name changed from '$old' to '$new'")
}
// Map 支撑的属性
class Config(private val map: Map<String, Any?>) {
val host: String by map
val port: Int by map
val debug: Boolean by map
}
val config = Config(mapOf("host" to "localhost", "port" to 8080, "debug" to true))
10.2 接口委托:无继承地复用实现并局部增强
class LoggingUserRepository(
private val delegate: UserRepository,
private val logger: Logger,
) : UserRepository by delegate {
// 只需覆写需要加日志的方法
override suspend fun findById(id: String): User? {
logger.info("Finding user by id: $id")
return delegate.findById(id).also {
logger.info("Found user: ${it?.name ?: "null"}")
}
}
}
接口委托(by delegate)让“装饰器/代理”类只覆写需要增强的方法,其余方法零样板透传。这是仓库评审规范中“无继承复用”主题的标准落地方式之一。
十一、类型安全 DSL:@DslMarker 与 lambda 接收者
DSL 的精髓是 lambda 接收者 + 受限作用域:接收者类型把可调用成员限定在 DSL 语义之内,@DslMarker 阻止隐式外层接收者成员被误调用(从而避免内外层 DSL 成员串味)。
11.1 HTML 风格 DSL
@DslMarker
annotation class HtmlDsl
@HtmlDsl
class HTML {
private val children = mutableListOf<Element>()
fun head(init: Head.() -> Unit) {
children += Head().apply(init)
}
fun body(init: Body.() -> Unit) {
children += Body().apply(init)
}
override fun toString(): String = children.joinToString("\n")
}
fun html(init: HTML.() -> Unit): HTML = HTML().apply(init)
// 使用
val page = html {
head { title("My Page") }
body {
h1("Welcome")
p("Hello, World!")
}
}
11.2 配置型 DSL:Builder + 数据类收口
工程中更常见的是“配置 DSL”,其模式可拆为三步:Builder 持有可变中间态 → fun x(init: XBuilder.() -> Unit) 构建入口 → build() 产出不可变数据类。
data class ServerConfig(
val host: String = "0.0.0.0",
val port: Int = 8080,
val ssl: SslConfig? = null,
val database: DatabaseConfig? = null,
)
data class SslConfig(val certPath: String, val keyPath: String)
data class DatabaseConfig(val url: String, val maxPoolSize: Int = 10)
class ServerConfigBuilder {
var host: String = "0.0.0.0"
var port: Int = 8080
private var ssl: SslConfig? = null
private var database: DatabaseConfig? = null
fun ssl(certPath: String, keyPath: String) {
ssl = SslConfig(certPath, keyPath)
}
fun database(url: String, maxPoolSize: Int = 10) {
database = DatabaseConfig(url, maxPoolSize)
}
fun build(): ServerConfig = ServerConfig(host, port, ssl, database)
}
fun serverConfig(init: ServerConfigBuilder.() -> Unit): ServerConfig =
ServerConfigBuilder().apply(init).build()
// 使用
val config = serverConfig {
host = "0.0.0.0"
port = 443
ssl("/certs/cert.pem", "/certs/key.pem")
database("jdbc:postgresql://localhost:5432/mydb", maxPoolSize = 20)
}
仓库规约里也存在完全同构的应用:rules/kotlin/patterns.md 的“Builder Pattern with DSL”把 HttpClientConfig 交给 fun httpClient(block: HttpClientConfig.() -> Unit) 构建,从而让 baseUrl、timeout、interceptor { } 以声明式写法出现在调用点。也就是说,同样的模式既出现在本技能文档,也贯穿于仓库的架构规约,说明它是 Kotlin 生态“声明式 API”的事实标准。
十二、Sequence:多条链式操作的惰性求值
对元素量巨大且操作链条多的集合,普通 List 的每一步中间操作都会物化出整份集合;改用 asSequence() 后各操作以元素为单位纵向流水执行(filter→map→filter→take 各阶段按元素叠加在一个循环里),配合无限序列与 yield 可以表达“按需生成”的数据源:
// Good: 大集合 + 多操作时使用 sequence
val result = users.asSequence()
.filter { it.isActive }
.map { it.email }
.filter { it.endsWith("@company.com") }
.take(10)
.toList()
// Good: 无限序列
val fibonacci: Sequence<Long> = sequence {
var a = 0L
var b = 1L
while (true) {
yield(a)
val next = a + b
a = b
b = next
}
}
val first20 = fibonacci.take(20).toList()
选择依据的简化记忆:集合规模小或只有一两次操作时 List 的开销反而更低;链条长、数据量大、或存在 take 提前终止时换 Sequence 收益明显。
十三、Gradle Kotlin DSL:用类型安全配置替代 Groovy
build.gradle.kts 让构建脚本获得 IDE 补全、编译期校验与类型安全。技能文档给出了一套相对完整的服务端工程基线,覆盖 Ktor、Exposed、Koin、协程与 Kotest/MockK 测试栈。注意示例中的版本号是撰写该文档时的基线,落地时请对照官方版本发布页核对最新版本:
plugins {
kotlin("jvm") version "2.3.10"
kotlin("plugin.serialization") version "2.3.10"
id("io.ktor.plugin") version "3.4.0"
id("org.jetbrains.kotlinx.kover") version "0.9.7"
id("io.gitlab.arturbosch.detekt") version "1.23.8"
}
group = "com.example"
version = "1.0.0"
kotlin {
jvmToolchain(21)
}
dependencies {
// Ktor
implementation("io.ktor:ktor-server-core:3.4.0")
implementation("io.ktor:ktor-server-netty:3.4.0")
implementation("io.ktor:ktor-server-content-negotiation:3.4.0")
implementation("io.ktor:ktor-serialization-kotlinx-json:3.4.0")
// Exposed
implementation("org.jetbrains.exposed:exposed-core:1.0.0")
implementation("org.jetbrains.exposed:exposed-dao:1.0.0")
implementation("org.jetbrains.exposed:exposed-jdbc:1.0.0")
implementation("org.jetbrains.exposed:exposed-kotlin-datetime:1.0.0")
// Koin
implementation("io.insert-koin:koin-ktor:4.2.0")
// Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
// Testing
testImplementation("io.kotest:kotest-runner-junit5:6.1.4")
testImplementation("io.kotest:kotest-assertions-core:6.1.4")
testImplementation("io.kotest:kotest-property:6.1.4")
testImplementation("io.mockk:mockk:1.14.9")
testImplementation("io.ktor:ktor-server-test-host:3.4.0")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2")
}
tasks.withType<Test> {
useJUnitPlatform()
}
detekt {
config.setFrom(files("config/detekt/detekt.yml"))
buildUponDefaultConfig = true
}
配套评审视角(见 agents/kotlin-reviewer.md 的 Gradle & Build 检查项)提醒三件事:优先用 libs.versions.toml 版本目录集中管理版本号、删除未被使用的依赖、KMP 工程能写进 commonMain 的代码不要下沉到平台源集。
十四、错误处理:Result 与前置条件断言
14.1 用 Result 表达领域操作
可恢复的领域失败建议用 runCatching + 链式 map/getOrElse 表达,而不是抛异常:
suspend fun createUser(request: CreateUserRequest): Result<User> = runCatching {
require(request.name.isNotBlank()) { "Name cannot be blank" }
require('@' in request.email) { "Invalid email format" }
val user = User(
id = UserId(UUID.randomUUID().toString()),
name = request.name,
email = Email(request.email),
)
userRepository.save(user)
user
}
val displayName = createUser(request)
.map { it.name }
.getOrElse { "Unknown" }
该写法与第 6 节的自定义密封 Result 互为两种取舍:内置 Result 快捷但错误信息能力弱,自建密封层级穷尽但需维护更多类型。仓库规约(rules/kotlin/patterns.md)对 Repository 的约定是 suspend 函数返回 Result<T> 或自定义错误类型,与本节一致。
14.2 require 与 check:两种断言分工
fun withdraw(account: Account, amount: Money): Account {
require(amount.value > 0) { "Amount must be positive: $amount" } // 入参校验
check(account.balance >= amount) { "Insufficient balance: ${account.balance} < $amount" } // 状态校验
return account.copy(balance = account.balance - amount)
}
语义区分:require 校验调用方传入的参数,失败抛 IllegalArgumentException;check 校验当前对象/系统的内部状态,失败抛 IllegalStateException。二者都支持带消息的 lambda,可写可读的失败上下文。
十五、集合操作:用管道代替手写循环
filter→sortedBy→map 链、groupBy、associateBy、partition 几乎覆盖日常数据处理,且都能以解构接收结果:
// Good: 链式操作
val activeAdminEmails: List<String> = users
.filter { it.role == Role.ADMIN && it.isActive }
.sortedBy { it.name }
.map { it.email }
// Good: 分组与聚合
val usersByRole: Map<Role, List<User>> = users.groupBy { it.role }
val oldestByRole: Map<Role, User?> = users.groupBy { it.role }
.mapValues { (_, users) -> users.minByOrNull { it.createdAt } }
// Good: associate 建 Map
val usersById: Map<UserId, User> = users.associateBy { it.id }
// Good: partition 拆分
val (active, inactive) = users.partition { it.isActive }
注意 users.partition { ... } 用 Pair 解构同时拿到两个列表,避免二次遍历。对“性能敏感且链条长”的同类场景,记得切到第 12 节的 asSequence()。
十六、惯用法速查表
| 惯用法 | 说明 |
|---|---|
val over var |
优先不可变变量 |
data class |
值对象,自带 equals/hashCode/copy |
sealed class/interface |
受限类型层级 |
value class |
零开销的类型安全包装 |
表达式 when |
穷尽模式匹配 |
安全调用 ?. |
空安全的成员访问 |
Elvis ?: |
可空默认值 |
let/apply/also/run/with |
作用域函数 |
| 扩展函数 | 无需继承即可加行为 |
copy() |
数据类的不可变更新 |
require/check |
前置条件断言 |
协程 async/await |
结构化并发执行 |
Flow |
冷响应式流 |
sequence |
惰性求值 |
委托 by |
无继承复用实现 |
十七、反模式清单:评审一票否决项速览
// Bad: 强解包可空类型
val name = user!!.name
// Bad: 平台类型泄漏(来自 Java 的值未判空)
fun getLength(s: String) = s.length // 安全
fun getLength(s: String?) = s?.length ?: 0 // 对 Java 传值要判空
// Bad: 可变数据类
data class MutableUser(var name: String, var email: String)
// Bad: 用异常做控制流
try {
val user = findUser(id)
} catch (e: NotFoundException) {
// 预期内的情况不该走异常
}
// Good: 改用可空返回或 Result
val user: User? = findUserOrNull(id)
// Bad: 忽略协程作用域
GlobalScope.launch { /* 避免 GlobalScope */ }
// Good: 使用结构化并发
coroutineScope {
launch { /* 作用域受控 */ }
}
// Bad: 深度嵌套作用域函数
user?.let { u ->
u.address?.let { a ->
a.city?.let { c -> process(c) }
}
}
// Good: 直接空安全链
user?.address?.city?.let { process(it) }
这些反模式在仓库中被进一步细化为可按严重级别上报的检查项(见 agents/kotlin-reviewer.md 的检查清单):GlobalScope 使用、捕获 CancellationException、StateFlow 内存放可变集合、init{} 中无作用域收集 Flow、缺失 WhileSubscribed 等属于 HIGH 级——按评审规则只要出现即触发 block 阻断合入。可以据此搭建你自己的提交前 Kotlin 自检清单。
十八、把模式落到测试与工具链
模式文档讲的“应该怎么写”,最终要靠测试与静态分析锁住。仓库提供了配套约束:
- 测试技术栈(rules/kotlin/testing.md):KMP 用
kotlin.test,Android 用 JUnit 4/5;流与状态用 Turbine 的viewModel.state.test { awaitItem() ... }断言状态序列;协程用 kotlinx-coroutines-test 的runTest+TestScope,配合advanceUntilIdle()虚拟时间推进;网络层用 KtorMockEngine按路径返回桩响应;数据库用JdbcSqliteDriver(IN_MEMORY)/ Room 内存库; - 测试命名:反引号包裹的自然语言式用例名(如 `search with empty query returns all items`)是仓库明确推荐的风格;
- Fake 优先于 Mock:手写
FakeItemRepository(内部持MutableStateFlow,通过emit()注入数据)比 MockK 静态打桩更易读、更贴近真实行为; - 静态质量门禁:工程基线的
detekt插件(见第 13 节)配合config/detekt/detekt.yml统一风格,Kover 输出覆盖率,二者共同充当“让编译器与工具帮你把关”的落地手段。
结语:让编译器成为你的第一道防线
回到本文的起点:Kotlin 代码应该简洁但可读。可空类型 + 安全调用消灭了一整类 NPE;val、data class、不可变集合与 copy() 让状态迁移显式化;密封类型把错误建模为可穷尽的类型而非随手抛出的异常;协程的 coroutineScope/supervisorScope 与 Flow 提供了可取消、可清理的并发模型;@DslMarker 与 lambda 接收者让配置 API 声明化;Gradle Kotlin DSL 让构建脚本同样享受类型安全。当你对取舍感到不确定时,记住技能文档的结语——尽量让编译器帮你:用不可空类型消灭空指针、用穷尽 when 消灭漏分支、用结构化并发消灭悬挂协程。这套技能在 ECC 仓库中作为工程规约被 kotlin-reviewer 等 Agent 直接消费,也值得在任何 Kotlin/Android/KMP 工程中作为编码与评审的准绳。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00