Kotlin coerceIn 使用详解

3 阅读5分钟

coerceIn 是 Kotlin 标准库中的一个扩展函数,用于将一个值限制在指定的范围内。如果值在范围内,则返回该值;如果小于最小值,则返回最小值;如果大于最大值,则返回最大值。

基本语法

// 限制在最小值和最大值之间
fun <T : Comparable<T>> T.coerceIn(minimumValue: T, maximumValue: T): T

// 限制在一个范围内
fun <T : Comparable<T>> T.coerceIn(range: ClosedRange<T>): T

基本用法

1. 数值范围限制

fun main() {
    // 基本整数限制
    val x = 10
    val clamped1 = x.coerceIn(1, 5)  // 10 > 5,所以返回 5
    println(clamped1)  // 输出: 5
    
    val clamped2 = x.coerceIn(20, 30)  // 10 < 20,所以返回 20
    println(clamped2)  // 输出: 20
    
    val clamped3 = x.coerceIn(8, 12)  // 10 在范围内,返回 10
    println(clamped3)  // 输出: 10
    
    // 浮点数限制
    val pi = 3.14
    val clampedPi = pi.coerceIn(0.0, 1.0)  // 3.14 > 1.0,返回 1.0
    println(clampedPi)  // 输出: 1.0
    
    // 字符限制
    val char = 'g'
    val clampedChar = char.coerceIn('a', 'f')  // 'g' > 'f',返回 'f'
    println(clampedChar)  // 输出: f
}

2. 使用范围对象

fun main() {
    val value = 15
    
    // 使用范围对象
    val range1 = 10..20
    val result1 = value.coerceIn(range1)
    println(result1)  // 输出: 15
    
    val range2 = 1..5
    val result2 = value.coerceIn(range2)
    println(result2)  // 输出: 5
    
    val range3 = 20..30
    val result3 = value.coerceIn(range3)
    println(result3)  // 输出: 20
    
    // 使用 until(半开区间,需要先转换为闭区间)
    val halfOpen = 10 until 20  // 10..19
    val closedRange = 10..19
    println(value.coerceIn(closedRange))  // 输出: 15
}

实际应用场景

1. UI 开发中的边界检查

data class View(val x: Int, val y: Int, val width: Int, val height: Int) {
    fun moveTo(newX: Int, newY: Int, screenWidth: Int, screenHeight: Int): View {
        // 确保视图不会移出屏幕
        val clampedX = newX.coerceIn(0, screenWidth - width)
        val clampedY = newY.coerceIn(0, screenHeight - height)
        
        return this.copy(x = clampedX, y = clampedY)
    }
}

fun main() {
    val screen = View(0, 0, 800, 600)
    val dialog = View(0, 0, 200, 150)
    
    // 尝试移动到屏幕外
    val moved = dialog.moveTo(700, 500, screen.width, screen.height)
    println("Dialog position: (${moved.x}, ${moved.y})")
    // 输出: Dialog position: (600, 450)
    // 因为 700 > (800-200=600),500 > (600-150=450)
}

2. 游戏开发

data class GameCharacter(
    val name: String,
    var health: Int,
    var position: Position
) {
    data class Position(var x: Int, var y: Int)
    
    fun move(dx: Int, dy: Int, worldBounds: IntRange) {
        position.x = (position.x + dx).coerceIn(worldBounds)
        position.y = (position.y + dy).coerceIn(worldBounds)
    }
    
    fun takeDamage(damage: Int) {
        health = (health - damage).coerceIn(0, 100)
    }
    
    fun heal(amount: Int) {
        health = (health + amount).coerceIn(0, 100)
    }
}

fun main() {
    val player = GameCharacter("Hero", 80, GameCharacter.Position(5, 5))
    val worldBounds = 0..10
    
    // 移动 - 不会超出边界
    player.move(3, 7, worldBounds)
    println("Position: (${player.position.x}, ${player.position.y})")
    // 输出: Position: (8, 10) 而不是 (8, 12)
    
    // 受到伤害
    player.takeDamage(90)
    println("Health: ${player.health}")  // 输出: Health: 0
    
    // 治疗
    player.heal(120)
    println("Health: ${player.health}")  // 输出: Health: 100
}

3. 数据验证和规范化

data class UserInput(
    val username: String,
    val age: Int,
    val score: Double
) {
    companion object {
        private val AGE_RANGE = 0..150
        private val SCORE_RANGE = 0.0..100.0
        private val USERNAME_LENGTH_RANGE = 3..20
    }
    
    fun validate(): ValidatedUserInput {
        val validatedAge = age.coerceIn(AGE_RANGE)
        val validatedScore = score.coerceIn(SCORE_RANGE)
        val validatedUsername = if (username.length in USERNAME_LENGTH_RANGE) {
            username
        } else {
            // 截断或处理用户名长度
            username.take(USERNAME_LENGTH_RANGE.last)
        }
        
        return ValidatedUserInput(validatedUsername, validatedAge, validatedScore)
    }
}

data class ValidatedUserInput(
    val username: String,
    val age: Int,
    val score: Double
)

fun main() {
    val input1 = UserInput("john_doe", 25, 85.5)
    val validated1 = input1.validate()
    println(validated1)  // 输出: ValidatedUserInput(username=john_doe, age=25, score=85.5)
    
    val input2 = UserInput("jo", 200, 150.0)  // 不合规的数据
    val validated2 = input2.validate()
    println(validated2)  
    // 输出: ValidatedUserInput(username=jo, age=150, score=100.0)
}

4. 进度和百分比计算

class ProgressTracker(private val totalSteps: Int) {
    private var currentStep: Int = 0
    
    fun progress(percent: Int): Float {
        // 确保百分比在 0-100 之间
        val validPercent = percent.coerceIn(0, 100)
        return validPercent / 100f
    }
    
    fun advance(steps: Int) {
        currentStep = (currentStep + steps).coerceIn(0, totalSteps)
    }
    
    fun retreat(steps: Int) {
        currentStep = (currentStep - steps).coerceIn(0, totalSteps)
    }
    
    val progressPercentage: Int
        get() = (currentStep * 100 / totalSteps).coerceIn(0, 100)
}

fun main() {
    val tracker = ProgressTracker(50)
    
    tracker.advance(30)
    println("Progress: ${tracker.progressPercentage}%")  // 输出: Progress: 60%
    
    tracker.advance(40)  // 尝试超过总量
    println("Progress: ${tracker.progressPercentage}%")  // 输出: Progress: 100%
    
    tracker.retreat(100)  // 尝试低于 0
    println("Progress: ${tracker.progressPercentage}%")  // 输出: Progress: 0%
    
    val progress = tracker.progress(150)  // 超过 100%
    println("Progress: $progress")  // 输出: Progress: 1.0
}

5. 颜色值处理

data class Color(val r: Int, val g: Int, val b: Int) {
    init {
        require(r in 0..255 && g in 0..255 && b in 0..255) {
            "颜色值必须在 0-255 之间"
        }
    }
    
    companion object {
        fun fromValues(red: Int, green: Int, blue: Int): Color {
            // 使用 coerceIn 确保颜色值在有效范围内
            val safeRed = red.coerceIn(0, 255)
            val safeGreen = green.coerceIn(0, 255)
            val safeBlue = blue.coerceIn(0, 255)
            return Color(safeRed, safeGreen, safeBlue)
        }
        
        fun darken(color: Color, amount: Int): Color {
            val newRed = (color.r - amount).coerceIn(0, 255)
            val newGreen = (color.g - amount).coerceIn(0, 255)
            val newBlue = (color.b - amount).coerceIn(0, 255)
            return Color(newRed, newGreen, newBlue)
        }
        
        fun lighten(color: Color, amount: Int): Color {
            val newRed = (color.r + amount).coerceIn(0, 255)
            val newGreen = (color.g + amount).coerceIn(0, 255)
            val newBlue = (color.b + amount).coerceIn(0, 255)
            return Color(newRed, newGreen, newBlue)
        }
    }
}

fun main() {
    // 创建颜色,自动处理边界
    val color1 = Color.fromValues(100, 200, 300)  // 300 会被限制为 255
    println(color1)  // 输出: Color(r=100, g=200, b=255)
    
    val darker = Color.darken(color1, 150)  // 尝试减少 150
    println(darker)  // 输出: Color(r=0, g=50, b=105)
    
    val lighter = Color.lighten(color1, 200)  // 尝试增加 200
    println(lighter)  // 输出: Color(r=255, g=255, b=255)
}

高级用法

1. 自定义比较类型

// 自定义可比较类型
data class Temperature(val celsius: Double) : Comparable<Temperature> {
    override fun compareTo(other: Temperature): Int {
        return celsius.compareTo(other.celsius)
    }
    
    companion object {
        val WATER_FREEZING = Temperature(0.0)
        val WATER_BOILING = Temperature(100.0)
        val HUMAN_BODY = Temperature(37.0)
    }
}

fun main() {
    val currentTemp = Temperature(25.0)
    val comfortableRange = Temperature(20.0)..Temperature(26.0)
    
    val adjustedTemp = currentTemp.coerceIn(comfortableRange)
    println("舒适温度: ${adjustedTemp.celsius}°C")  // 输出: 舒适温度: 25.0°C
    
    val hotTemp = Temperature(40.0)
    val safeTemp = hotTemp.coerceIn(Temperature.WATER_FREEZING, Temperature.WATER_BOILING)
    println("安全温度: ${safeTemp.celsius}°C")  // 输出: 安全温度: 40.0°C
}

2. 与集合操作结合

fun main() {
    val values = listOf(-10, 5, 15, 25, 35, 45)
    val range = 0..30
    
    // 将列表中的所有值限制在范围内
    val clampedValues = values.map { it.coerceIn(range) }
    println(clampedValues)  // 输出: [0, 5, 15, 25, 30, 30]
    
    // 计算平均值,但先限制每个值
    val average = values
        .map { it.coerceIn(0, 100) }
        .average()
    println("限制后的平均值: $average")  // 输出: 限制后的平均值: 26.666666666666668
}

3. 链式调用

class RangeSlider(private var value: Double, private val range: ClosedRange<Double>) {
    fun adjust(amount: Double): RangeSlider {
        value = (value + amount).coerceIn(range)
        return this
    }
    
    fun multiply(factor: Double): RangeSlider {
        value = (value * factor).coerceIn(range)
        return this
    }
    
    fun getValue(): Double = value
}

fun main() {
    val slider = RangeSlider(50.0, 0.0..100.0)
    
    val result = slider
        .adjust(30.0)    // 80.0
        .multiply(1.5)   // 120.0 → 限制为 100.0
        .adjust(-150.0)  // -50.0 → 限制为 0.0
        .getValue()
    
    println("滑块值: $result")  // 输出: 滑块值: 0.0
}

注意事项和边界情况

1. 范围有效性检查

fun main() {
    val value = 10
    
    // 范围必须有效(最小值 ≤ 最大值)
    try {
        val result = value.coerceIn(20, 10)  // 最小值 > 最大值
    } catch (e: IllegalArgumentException) {
        println("错误: ${e.message}")  
        // 输出: 错误: Cannot coerce value to an empty range: maximum 10 is less than minimum 20.
    }
    
    // 正确的做法是确保范围有效
    val min = 20
    val max = 10
    if (min <= max) {
        val result = value.coerceIn(min, max)
    } else {
        // 处理无效范围
        println("范围无效: $min > $max")
    }
}

2. 空范围处理

fun main() {
    val value = 5
    
    // 单点范围(最小值 = 最大值)
    val singlePointRange = 5..5
    val result1 = value.coerceIn(singlePointRange)
    println(result1)  // 输出: 5
    
    // 如果值不在单点范围内
    val result2 = 10.coerceIn(singlePointRange)
    println(result2)  // 输出: 5
}

3. 性能考虑

// coerceIn 是内联函数,性能开销很小
// 但在循环中大量使用时,可以考虑手动内联

fun processBatch(values: List<Int>): List<Int> {
    val min = 0
    val max = 100
    
    // 方式1:使用 coerceIn(推荐,代码清晰)
    return values.map { it.coerceIn(min, max) }
    
    // 方式2:手动内联(在极端性能要求时考虑)
    // return values.map { if (it < min) min else if (it > max) max else it }
}

// 对于自定义类型,实现 Comparable 接口后即可使用 coerceIn
data class CustomValue(val score: Int) : Comparable<CustomValue> {
    override fun compareTo(other: CustomValue): Int = score.compareTo(other.score)
}

最佳实践

1. 防御性编程

class Configuration {
    // 使用 coerceIn 确保配置值始终有效
    var brightness: Int = 50
        set(value) {
            field = value.coerceIn(0, 100)
        }
    
    var volume: Int = 50
        set(value) {
            field = value.coerceIn(0, 100)
        }
    
    var temperature: Int = 22
        set(value) {
            field = value.coerceIn(16, 28)
        }
}

fun main() {
    val config = Configuration()
    
    config.brightness = 120  // 会被限制为 100
    println("亮度: ${config.brightness}")  // 输出: 亮度: 100
    
    config.temperature = 10  // 会被限制为 16
    println("温度: ${config.temperature}")  // 输出: 温度: 16
}

2. 结合 when 表达式

fun categorizeScore(rawScore: Int): String {
    val score = rawScore.coerceIn(0, 100)
    
    return when (score) {
        in 90..100 -> "优秀"
        in 80 until 90 -> "良好"
        in 60 until 80 -> "及格"
        else -> "不及格"
    }
}

fun main() {
    println(categorizeScore(95))   // 输出: 优秀
    println(categorizeScore(120))  // 输出: 优秀(限制为 100)
    println(categorizeScore(-10))  // 输出: 不及格(限制为 0)
}

3. 扩展函数增强

// 为数字类型添加扩展函数
fun Int.coerceInPercentage(): Int = this.coerceIn(0, 100)

fun Double.coerceInZeroToOne(): Double = this.coerceIn(0.0, 1.0)

// 为颜色通道值添加扩展
fun Int.coerceInColorChannel(): Int = this.coerceIn(0, 255)

// 使用
fun main() {
    val percentage = 150.coerceInPercentage()
    println("百分比: $percentage%")  // 输出: 百分比: 100%
    
    val alpha = 1.5.coerceInZeroToOne()
    println("透明度: $alpha")  // 输出: 透明度: 1.0
    
    val red = 300.coerceInColorChannel()
    println("红色通道: $red")  // 输出: 红色通道: 255
}

总结

  • coerceIn 是一个简单但强大的函数,用于将值限制在指定范围内
  • 支持所有实现了 Comparable 接口的类型(IntDoubleChar、自定义类型等)
  • 有两种使用方式:直接指定最小/最大值,或使用 ClosedRange 对象
  • 适用于:UI 边界检查、游戏开发、数据验证、进度计算等多种场景
  • 注意处理无效范围(最小值 > 最大值时会抛出异常)
  • 结合扩展函数可以创建更语义化的代码