You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Kotlin数据类主构造函数中初始化需预处理的属性?

解决Kotlin Data Class中有理数归一化的初始化问题

问题根源

你原来的代码无法编译,是因为data class的val属性是只读的,初始化完成后就不能在init块中重新赋值。而data class要求主构造函数的属性必须是val或var,不能移除val修饰符。

正确解决方案

核心思路是在初始化data class的只读属性前,先完成输入参数的校验和归一化计算,再将处理后的值传入主构造函数。推荐使用「私有主构造+伴生对象工厂方法」的方式,既保持data class的不可变性,又能灵活处理参数预处理:

import java.math.BigInteger

data class Rational private constructor(
    val numerator: BigInteger,
    val denominator: BigInteger
) : Comparable<Rational> {

    companion object {
        fun of(num: BigInteger, den: BigInteger): Rational {
            // 校验分母不能为0
            require(den != BigInteger.ZERO) { "0 is not a valid denominator" }
            
            // 计算最大公约数,用于约分
            val gcd = num.gcd(den)
            var normalizedNum = num / gcd
            var normalizedDen = den / gcd

            // 符号归一化:确保分母为正,负号统一移到分子上
            if (normalizedDen < BigInteger.ZERO) {
                normalizedNum = -normalizedNum
                normalizedDen = -normalizedDen
            }

            return Rational(normalizedNum, normalizedDen)
        }
    }

    // 实现Comparable接口,支持有理数比较
    override fun compareTo(other: Rational): Int {
        return (this.numerator * other.denominator).compareTo(other.numerator * this.denominator)
    }
}

// 扩展函数,支持infix语法创建Rational
infix fun BigInteger.divBy(d: BigInteger): Rational {
    return Rational.of(this, d)
}

fun main() {
    val half = 2.toBigInteger() divBy 4.toBigInteger()
    println(half) // 输出:Rational(numerator=1, denominator=2)
    
    // 测试符号归一化
    val negativeHalf1 = (-2).toBigInteger() divBy 4.toBigInteger()
    println(negativeHalf1) // 输出:Rational(numerator=-1, denominator=2)
    
    val negativeHalf2 = 2.toBigInteger() divBy (-4).toBigInteger()
    println(negativeHalf2) // 输出:Rational(numerator=-1, denominator=2)
}

方案说明

  1. 私有主构造:将Rational的主构造设为私有,避免外部直接传入未归一化的参数,确保所有实例都是合法状态。
  2. 伴生对象工厂方法:of方法负责处理参数校验、约分、符号归一化,最后将处理后的合法值传入主构造,初始化只读属性。
  3. 符号归一化:补充符号处理,符合有理数标准规范——分母始终为正,负号仅保留在分子上。
  4. 保留data class特性:依然可以使用data class自动生成的equals、hashCode、toString等方法,满足需求。

替代方案:次构造函数

如果偏好使用构造函数而非工厂方法,也可以用次构造函数处理预处理,再调用主构造:

import java.math.BigInteger

data class Rational(val numerator: BigInteger, val denominator: BigInteger) : Comparable<Rational> {
    constructor(num: BigInteger, den: BigInteger) : this(
        run {
            require(den != BigInteger.ZERO) { "0 is not a valid denominator" }
            val gcd = num.gcd(den)
            var normalizedNum = num / gcd
            val normalizedDen = den / gcd
            if (normalizedDen < BigInteger.ZERO) -normalizedNum else normalizedNum
        },
        run {
            val gcd = num.gcd(den)
            var normalizedDen = den / gcd
            if (normalizedDen < BigInteger.ZERO) -normalizedDen else normalizedDen
        }
    )

    override fun compareTo(other: Rational): Int {
        return (this.numerator * other.denominator).compareTo(other.numerator * this.denominator)
    }
}

infix fun BigInteger.divBy(d: BigInteger): Rational {
    return Rational(this, d)
}

不过这种方式代码冗余度较高,不如工厂方法清晰,因此更推荐第一种方案。

内容的提问来源于stack exchange,提问作者pbuchheit

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 02:10:40