没有默认构造函数的数据类

Data class without default constructor

提问人:Andrew Kolubov 提问时间:11/9/2023 最后编辑:David SorokoAndrew Kolubov 更新时间:11/10/2023 访问量:77

问:

我的数据类中有这些字段:

    val year: Int,
    val month: Int? = null,
    val day: Int? = null,

我如何通过构造函数确保If in not-null也不是null? 理想情况下,我只想有 3 个构造函数,而根本没有默认的可为 null 字段。daymonth

(year: Int) (year: Int, month: Int) and (year: Int, month: Int, day: Int)
Kotlin 构造函数 Data-Class

评论


答:

1赞 David Soroko 11/9/2023 #1

您可以使用某种工厂来强制执行类型和其他验证(负值等)。一种方法是使用伴随对象。

正如@gidds所评论的,虽然构造函数可以是私有的,但仍然可以使用 copy() 创建“无效”实例

data class MyDate 
private constructor(val year: Int, val month: Int? = null, val day: Int? = null) {
    companion object {
        fun from(year: Int) = MyDate(year, null, null)
        fun from(year: Int, month: Int) = MyDate(year, month, null)
        fun from(year: Int, month: Int, day: Int) = MyDate(year, month, day)
    }
}

fun main() {
    MyDate.from(2023)
        .also { println(it) } // MyDate(year=2023, month=null, day=null)
    MyDate.from(2023, 11)
        .also { println(it) } // MyDate(year=2023, month=11, day=null)
    MyDate.from(2023, 11, 9)
        .also { println(it) } // MyDate(year=2023, month=11, day=9)
    // Invalid instances can still be created using copy()
    MyDate.from(2023, 11, 9).copy(month = null)
        .also { println(it) } // MyDate(year=2023, month=null, day=9)
}

有一些选项可以开箱即用地创建各种日期对象,我认为它们不适合您的想法。

评论

0赞 David Soroko 11/10/2023
好点子,修改了答案。
4赞 Klitos Kyriacou 11/10/2023 #2

您可以使用私有构造函数创建一个普通类,并添加重载构造函数:

class X private constructor(val year: Int, val month: Int?, val day: Int?) {
    constructor(year: Int) : this(year, null, null)
    constructor(year: Int, month: Int) : this(year, month, null)
    constructor(year: Int, month: Int, day: Int) : this(year, month, day as Int?)

    // Optionally create equals, hashCode, toString and componentN methods...
}

如果您需要任何自动生成的额外方法,只需手动添加它们,或者让 IDE 为您生成它们。data class

理由

我们为什么要创建一个普通的班级?好吧,您可以使用私有构造函数创建一个数据类,但这不会阻止类的用户调用该函数并向其传递无效参数,例如具有 null 月份的一天。copy

与 和 不同,您不能以任何方式覆盖该函数或禁用该函数。如果你将你的类声明为数据类,你会得到一个你无法摆脱的不需要的函数。使用普通类,您可以在函数中定义自己的逻辑,尽管您可能根本不想定义它,因为它有问题。toStringhashCodeequalscopycopycopy

1赞 k314159 11/10/2023 #3

您可以创建一个在用法上类似于数据类的接口:

sealed interface FlexibleDate {
    val year: Int
    val month: Int?
    val day: Int?

    operator fun component1() = year
    operator fun component2() = month
    operator fun component3() = day
    
    companion object {
        operator fun invoke(year: Int): FlexibleDate = Impl(year, null, null)
        operator fun invoke(year: Int, month: Int): FlexibleDate = Impl(year, month, null)
        operator fun invoke(year: Int, month: Int, day: Int): FlexibleDate = Impl(year, month, day)
    }

     private data class Impl(override val year: Int, override val month: Int?, override val day: Int?) : FlexibleDate
}

私有实现类提供正确生成的 实现,并且所有数据类都具有。ImpltoStringequalshashCode

该接口提供了一些函数,以便您可以创建实例,就好像它具有构造函数一样。超载不会接受没有一个月的一天。invokeinvoke

评论

0赞 David Soroko 11/10/2023
不错,为什么?sealed
1赞 k314159 11/10/2023
@DavidSoroko 它不必被密封,但我想让它尽可能接近数据类。无法扩展数据类。同样,密封接口也无法实现(也就是说,一旦你把你的库放在它自己的模块中)。