Kotlin where 关键字
最后修改于 2025 年 4 月 19 日
Kotlin 的泛型类型系统允许使用多个需求来约束类型参数。where 关键字指定这些约束。本教程通过实际示例深入探讨了 where 关键字。
基本定义
Kotlin 中的 where 关键字将多个约束应用于泛型类型参数。当一个类型参数必须满足多个条件时,会使用它。约束可以包括类/接口实现和其他类型关系。
使用 where 的单个约束
where 的最简单用法是将一个约束应用于类型参数。这等效于使用冒号语法,但演示了基本结构。
package com.zetcode
interface Printable {
fun print()
}
class Document : Printable {
override fun print() = println("Printing document")
}
fun <T> printItem(item: T) where T : Printable {
item.print()
}
fun main() {
val doc = Document()
printItem(doc) // Output: Printing document
}
这里我们定义了一个泛型函数 printItem,它要求其类型参数 T 实现 Printable 接口。where 子句强制执行此约束。Document 类满足此要求。
对单个类型的多个约束
当您需要对一个类型参数进行多个约束时,where 关键字就会发挥作用。此示例要求一个类型实现两个接口。
package com.zetcode
interface Serializable {
fun serialize(): String
}
interface Deserializable {
fun deserialize(data: String)
}
class Config : Serializable, Deserializable {
override fun serialize() = "Config data"
override fun deserialize(data: String) = println("Loading: $data")
}
fun <T> processData(item: T) where T : Serializable, T : Deserializable {
val data = item.serialize()
item.deserialize(data)
}
fun main() {
val config = Config()
processData(config) // Output: Loading: Config data
}
processData 函数要求其类型参数 T 同时实现 Serializable 和 Deserializable 接口。Config 类满足这些要求,因此我们可以使用 Config 实例调用该函数。
对多个类型参数的约束
where 可以在单个声明中约束多个类型参数。当类型需要以特定方式相互关联时,这很有用。
package com.zetcode
interface Producer<out T> {
fun produce(): T
}
interface Consumer<in T> {
fun consume(item: T)
}
fun <T, U> transform(
producer: Producer<T>,
consumer: Consumer<U>
) where T : U, U : Number {
val item = producer.produce()
consumer.consume(item)
}
class IntProducer : Producer<Int> {
override fun produce() = 42
}
class NumberConsumer : Consumer<Number> {
override fun consume(item: Number) = println("Consumed: $item")
}
fun main() {
transform(IntProducer(), NumberConsumer()) // Output: Consumed: 42
}
此示例显示了对两个类型参数 T 和 U 的约束。T 必须是 U 的子类型,并且 U 必须是 Number 的子类型。transform 函数只能使用满足这些关系的类型调用。
带有 where 约束的类
where 关键字也可以与类声明一起使用,以约束它们的类型参数。这确保了所有类方法都可以访问受约束的类型。
package com.zetcode
interface Identifiable {
val id: String
}
interface Timestamped {
val timestamp: Long
}
class Repository<T>(private val items: List<T>) where T : Identifiable, T : Timestamped {
fun findById(id: String): T? = items.find { it.id == id }
fun getRecent(): List<T> {
val now = System.currentTimeMillis()
return items.filter { now - it.timestamp < 3600000 }
}
}
data class LogEntry(
override val id: String,
override val timestamp: Long,
val message: String
) : Identifiable, Timestamped
fun main() {
val logs = listOf(
LogEntry("1", System.currentTimeMillis() - 1000, "Started"),
LogEntry("2", System.currentTimeMillis() - 7200000, "Old entry")
)
val repo = Repository(logs)
println(repo.findById("1")?.message) // Output: Started
println(repo.getRecent().size) // Output: 1
}
Repository 类要求其类型参数 T 同时实现 Identifiable 和 Timestamped 接口。这允许类方法安全地访问 id 和 timestamp 属性。LogEntry 满足这些约束。
组合类和函数约束
当类和函数都有 where 约束时,它们会组合起来创建更严格的类型要求。这提供了对泛型类型的精细控制。
package com.zetcode
interface Named {
val name: String
}
interface Priced {
val price: Double
}
class Store<T> where T : Named {
private val items = mutableListOf<T>()
fun addItem(item: T) = items.add(item)
fun <U> findCheaperThan(maxPrice: Double): List<U>
where U : T, U : Priced {
return items.filterIsInstance<U>().filter { it.price <= maxPrice }
}
}
data class Product(
override val name: String,
override val price: Double
) : Named, Priced
fun main() {
val store = Store<Named>()
store.addItem(Product("Laptop", 999.99))
store.addItem(Product("Mouse", 25.50))
val affordable = store.findCheaperThan<Product>(100.0)
println(affordable.map { it.name }) // Output: [Mouse]
}
Store 类将 T 约束为 Named,而其 findCheaperThan 方法进一步要求 U 既是 T 的子类型又实现 Priced 接口。这确保了我们可以在过滤结果中访问 name 和 price 属性。
递归类型约束
where 子句可以表示递归类型约束,其中类型参数必须以特定方式与自身相关联。这对于比较操作很有用。
package com.zetcode
interface Comparable<in T> {
fun compareTo(other: T): Int
}
fun <T> max(a: T, b: T): T where T : Comparable<T> {
return if (a.compareTo(b) >= 0) a else b
}
data class Version(val major: Int, val minor: Int) : Comparable<Version> {
override fun compareTo(other: Version): Int {
return when {
major != other.major -> major - other.major
else -> minor - other.minor
}
}
}
fun main() {
val v1 = Version(2, 5)
val v2 = Version(2, 7)
println(max(v1, v2)) // Output: Version(major=2, minor=7)
}
max 函数要求 T 实现 Comparable<T> 接口,这意味着 T 的实例可以与其他 T 的实例进行比较。Version 通过实现 compareTo 方法来满足此约束。
复杂的多类型关系
where 最强大的用途是建立多个类型参数之间的复杂关系。此示例显示了一个具有多个约束的数据处理器。
package com.zetcode
interface Entity<ID> {
val id: ID
}
interface Repository<E, ID> where E : Entity<ID>, ID : Comparable<ID> {
fun save(entity: E): E
fun findById(id: ID): E?
}
data class User(
override val id: String,
val name: String
) : Entity<String>
class UserRepository : Repository<User, String> {
private val storage = mutableMapOf<String, User>()
override fun save(entity: User): User {
storage[entity.id] = entity
return entity
}
override fun findById(id: String) = storage[id]
}
fun main() {
val repo = UserRepository()
val user = repo.save(User("123", "Alice"))
println(repo.findById("123")?.name) // Output: Alice
}
此示例表明 E 必须是 Entity<ID> 并且 ID 必须是 Comparable<ID>。UserRepository 使用 String 作为 ID 和 User 作为 E 来实现此接口。String 是可比较的,User 实现了 Entity<String>。
where 子句的最佳实践
- 用于多个约束: 当您需要对类型参数进行多个约束时,
where关键字最有价值。 - 提高可读性: 与内联约束相比,
where通常使复杂的泛型声明更具可读性。 - 考虑类型安全: 设计良好的约束在编译时而不是运行时捕获类型错误。
- 平衡灵活性: 除非您的实现需要,否则避免过度约束类型。
- 记录约束: 复杂的约束应该有详细的文档,以帮助其他开发人员了解需求。
来源
本教程深入介绍了 Kotlin 的 where 关键字,展示了如何将多个约束应用于泛型类型参数。我们探索了从简单到复杂类型关系的各种场景。正确使用约束可以使您的泛型代码更具类型安全性,同时保持灵活性。
作者
列出 所有 Kotlin 教程。