Kotlin 开发模式
适用于构建健壮、高效、可维护应用程序的惯用 Kotlin 模式与最佳实践。
使用时机
- 编写新的 Kotlin 代码
- 审查 Kotlin 代码
- 重构现有的 Kotlin 代码
- 设计 Kotlin 模块或库
- 配置 Gradle Kotlin DSL 构建
工作原理
本技能在七个关键领域强制执行惯用的 Kotlin 约定:使用类型系统和安全调用运算符实现空安全;通过数据类的 val 和 copy() 实现不可变性;使用密封类和接口实现穷举类型层次结构;使用协程和 Flow 实现结构化并发;使用扩展函数在不使用继承的情况下添加行为;使用 @DslMarker 和 lambda 接收器构建类型安全的 DSL;以及使用 Gradle Kotlin DSL 进行构建配置。
示例
使用 Elvis 运算符实现空安全:
kotlin
fun getUserEmail(userId: String): String { val user = userRepository.findById(userId) return user?.email ?: "[email protected]"}使用密封类处理穷举结果:
kotlin
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>()}使用 async/await 实现结构化并发:
kotlin
suspend fun fetchUserWithPosts(userId: String): UserProfile = coroutineScope { val user = async { userService.getUser(userId) } val posts = async { postService.getUserPosts(userId) } UserProfile(user = user.await(), posts = posts.await()) }核心原则
1. 空安全
Kotlin 的类型系统区分可空和不可空类型。充分利用它。
kotlin
// Good: Use non-nullable types by defaultfun getUser(id: String): User { return userRepository.findById(id) ?: throw UserNotFoundException("User $id not found")}
// Good: Safe calls and Elvis operatorfun getUserEmail(userId: String): String { val user = userRepository.findById(userId) return user?.email ?: "[email protected]"}
// Bad: Force-unwrapping nullable typesfun getUserEmail(userId: String): String { val user = userRepository.findById(userId) return user!!.email // Throws NPE if null}2. 默认不可变性
优先使用 val 而非 var,优先使用不可变集合而非可变集合。
kotlin
// Good: Immutable datadata class User( val id: String, val name: String, val email: String,)
// Good: Transform with copy()fun updateEmail(user: User, newEmail: String): User = user.copy(email = newEmail)
// Good: Immutable collectionsval users: List<User> = listOf(user1, user2)val filtered = users.filter { it.email.isNotBlank() }
// Bad: Mutable statevar currentUser: User? = null // Avoid mutable global stateval mutableUsers = mutableListOf<User>() // Avoid unless truly needed3. 表达式体和单表达式函数
使用表达式体编写简洁、可读的函数。
kotlin
// Good: Expression bodyfun 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 as expressionfun statusMessage(code: Int): String = when (code) { 200 -> "OK" 404 -> "Not Found" 500 -> "Internal Server Error" else -> "Unknown status: $code"}
// Bad: Unnecessary block bodyfun isAdult(age: Int): Boolean { return age >= 18}4. 数据类用于值对象
使用数据类表示主要包含数据的类型。
kotlin
// Good: Data class with copy, equals, hashCode, toStringdata class CreateUserRequest( val name: String, val email: String, val role: Role = Role.USER,)
// Good: Value class for type safety (zero overhead at runtime)@JvmInlinevalue class UserId(val value: String) { init { require(value.isNotBlank()) { "UserId cannot be blank" } }}
@JvmInlinevalue class Email(val value: String) { init { require('@' in value) { "Invalid email: $value" } }}
fun getUser(id: UserId): User = userRepository.findById(id)密封类和接口
建模受限的层次结构
kotlin
// Good: Sealed class for exhaustive whensealed 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")}用于 API 响应的密封接口
kotlin
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}作用域函数
何时使用各个函数
kotlin
// let: Transform nullable or scoped resultval length: Int? = name?.let { it.trim().length }
// apply: Configure an object (returns the object)val user = User().apply { name = "Alice" email = "[email protected]"}
// also: Side effects (returns the object)val user = createUser(request).also { logger.info("Created user: ${it.id}") }
// run: Execute a block with receiver (returns result)val result = connection.run { prepareStatement(sql) executeQuery()}
// with: Non-extension form of runval csv = with(StringBuilder()) { appendLine("name,email") users.forEach { appendLine("${it.name},${it.email}") } toString()}反模式
kotlin
// Bad: Nesting scope functionsuser?.let { u -> u.address?.let { addr -> addr.city?.let { city -> println(city) // Hard to read } }}
// Good: Chain safe calls insteadval city = user?.address?.citycity?.let { println(it) }扩展函数
在不使用继承的情况下添加功能
kotlin
// Good: Domain-specific extensionsfun 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: Collection extensionsfun <T> List<T>.second(): T = this[1]
fun <T> List<T>.secondOrNull(): T? = getOrNull(1)
// Good: Scoped extensions (not polluting global namespace)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() }}协程
结构化并发
kotlin
// Good: Structured concurrency with coroutineScopesuspend 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 when children can fail independentlysuspend 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() }, ) }Flow 用于响应式流
kotlin
// Good: Cold flow with proper error handlingfun 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())}
// Good: Flow operatorsfun searchUsers(query: Flow<String>): Flow<List<User>> = query .debounce(300.milliseconds) .distinctUntilChanged() .filter { it.length >= 2 } .mapLatest { q -> userRepository.search(q) } .catch { emit(emptyList()) }取消与清理
kotlin
// Good: Respect cancellationsuspend fun processItems(items: List<Item>) { items.forEach { item -> ensureActive() // Check cancellation before expensive work processItem(item) }}
// Good: Cleanup with try/finallysuspend fun acquireAndProcess() { val resource = acquireResource() try { resource.process() } finally { withContext(NonCancellable) { resource.release() // Always release, even on cancellation } }}委托
属性委托
kotlin
// Lazy initializationval expensiveData: List<User> by lazy { userRepository.findAll()}
// Observable propertyvar name: String by Delegates.observable("initial") { _, old, new -> logger.info("Name changed from '$old' to '$new'")}
// Map-backed propertiesclass 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))接口委托
kotlin
// Good: Delegate interface implementationclass LoggingUserRepository( private val delegate: UserRepository, private val logger: Logger,) : UserRepository by delegate { // Only override what you need to add logging to 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"}") } }}DSL 构建器
类型安全构建器
kotlin
// Good: DSL with @DslMarker@DslMarkerannotation class HtmlDsl
@HtmlDslclass 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)
// Usageval page = html { head { title("My Page") } body { h1("Welcome") p("Hello, World!") }}配置 DSL
kotlin
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()
// Usageval config = serverConfig { host = "0.0.0.0" port = 443 ssl("/certs/cert.pem", "/certs/key.pem") database("jdbc:postgresql://localhost:5432/mydb", maxPoolSize = 20)}用于惰性求值的序列
kotlin
// Good: Use sequences for large collections with multiple operationsval result = users.asSequence() .filter { it.isActive } .map { it.email } .filter { it.endsWith("@company.com") } .take(10) .toList()
// Good: Generate infinite sequencesval 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()Gradle Kotlin DSL
build.gradle.kts 配置
kotlin
// Check for latest versions: https://kotlinlang.org/docs/releases.htmlplugins { 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}错误处理模式
用于领域操作的 Result 类型
kotlin
// Good: Use Kotlin's Result or a custom sealed classsuspend 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}
// Good: Chain resultsval displayName = createUser(request) .map { it.name } .getOrElse { "Unknown" }require, check, error
kotlin
// Good: Preconditions with clear messagesfun 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)}集合操作
惯用的集合处理
kotlin
// Good: Chained operationsval activeAdminEmails: List<String> = users .filter { it.role == Role.ADMIN && it.isActive } .sortedBy { it.name } .map { it.email }
// Good: Grouping and aggregationval 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 for map creationval usersById: Map<UserId, User> = users.associateBy { it.id }
// Good: Partition for splittingval (active, inactive) = users.partition { it.isActive }快速参考:Kotlin 惯用法
| 惯用法 | 描述 |
|---|---|
val 优于 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 | 在不使用继承的情况下重用实现 |
应避免的反模式
kotlin
// Bad: Force-unwrapping nullable typesval name = user!!.name
// Bad: Platform type leakage from Javafun getLength(s: String) = s.length // Safefun getLength(s: String?) = s?.length ?: 0 // Handle nulls from Java
// Bad: Mutable data classesdata class MutableUser(var name: String, var email: String)
// Bad: Using exceptions for control flowtry { val user = findUser(id)} catch (e: NotFoundException) { // Don't use exceptions for expected cases}
// Good: Use nullable return or Resultval user: User? = findUserOrNull(id)
// Bad: Ignoring coroutine scopeGlobalScope.launch { /* Avoid GlobalScope */ }
// Good: Use structured concurrencycoroutineScope { launch { /* Properly scoped */ }}
// Bad: Deeply nested scope functionsuser?.let { u -> u.address?.let { a -> a.city?.let { c -> process(c) } }}
// Good: Direct null-safe chainuser?.address?.city?.let { process(it) }请记住:Kotlin 代码应简洁但可读。利用类型系统确保安全,优先使用不可变性,并使用协程处理并发。如有疑问,让编译器帮助你。


