如何在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) }
方案说明
- 私有主构造:将
Rational的主构造设为私有,避免外部直接传入未归一化的参数,确保所有实例都是合法状态。 - 伴生对象工厂方法:
of方法负责处理参数校验、约分、符号归一化,最后将处理后的合法值传入主构造,初始化只读属性。 - 符号归一化:补充符号处理,符合有理数标准规范——分母始终为正,负号仅保留在分子上。
- 保留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
相关产品推荐
相关产品推荐

