有没有办法在 Kotlin 中要求泛型类型成为数据 class?

Is there a way to require a generic type to be a data class in Kotlin?

以下内容无效,但希望能帮助您理解我的意思:

class Example<T : DataClass>

如果您想知道我想要完成什么,这是我想到的示例:

class Repository<T> where T : Entity, // Entity defines mutable property 'id'
                          T : DataClass {

  // assume there is a map here

  fun add(obj: T) {
    val copy = obj.copy(id = generateID())
    map.put(copy.id, copy)
  }

}

或者是否有更好的方法来完成我想做的事情?

不,data classes 在类型系统中没有任何特定的表示,无法与常规 classes () 区分开来。

但是,您可以要求具有一定数量组件的方法 data class 使用接口(实际上它将是 data [=52 上的标记接口=]es)。

这里有一个 data class 有两个组件的例子:

interface Data2<T1, T2> {
    operator fun component1(): T1
    operator fun component2(): T2
    fun copy(t1: T1, t2: T2): Data2<T1, T2>
}

toStringhashCodeequals 可以在任何类型上调用。

然后用界面标记你的data class:

data class Impl(val i: Int, val s: String): Data2<Int, String>

val d: Data2<Int, String> = Impl(1, "2")
val (c1, c2) = d
val copy = d.copy(-1, d.component2())

copy 函数不是完全类型安全的,因为 Kotlin (并且没有办法要求接口实现是特定类型的子类型),但是如果你只标记你的 data class 是的,它应该可以工作(见下面的另一个选项)。

另一个缺点是您丢失了 copy 方法的默认参数并且必须使用指定的所有参数调用它:

val d = myD2.copy(newValue, myD2.component2())

另一种选择是将这些接口定义为Data2<T1, T2, out Self>class Impl(...): Data2<..., Impl>,并使copyreturnSelf,但它如果您将界面用作 Data2<SomeType, SomeType, *>.

也不会让它变得更好

我感觉你真正想要的是T应该可以用一个新的ID复制自己,并且有一个ID。不一定是数据class。所以你可以只使用一个接口来定义它。

例如:

interface CopyableWithId<out T> where T: CopyableWithId<T> {
    fun copy(newId: Long): T
    val id: Long
}

data class BarBaz(override var id: Long, var name: String): CopyableWithId<BarBaz> {
    override fun copy(newId: Long): BarBaz = copy(id = newId)
}

class Repository<T> where T : CopyableWithId<T>{

    val map: MutableMap<Long, CopyableWithId<T>> = HashMap()

    fun add(obj: T) {
        val copy = obj.copy(generateID())
        map.put(copy.id, copy)
    }

    private fun generateID(): Long {
        return 1L
    }
}

你也可以实现copy或者component1,component2更通用的方式。

例如:

    interface Copyable <T> {
        fun copy(fields: T.() -> T): T
    }

    data class BarBaz(var id: Long, var name: String): Copyable<BarBaz> {
        override fun copy(fields: BarBaz.() -> BarBaz): BarBaz {
           val instance = fields(this)
           return copy(id = instance.id, name = instance.name)
        }
    }

class Repository<T> where T : Copyable<T>{

    val map: MutableMap<Long, Copyable<T>> = HashMap()

    fun add(obj: T) {
        val copy = obj.copy{id = generateID()}
        map.put(copy.id, copy)
    }

    private fun generateID(): Long {
        return 1L
    }
}

可能是无关的,因为我有类似但略有不同的问题。

我需要将共享逻辑移至超级 class,问题是我无法使用通用 T 的 copy 方法。我找到了这个解决方法:

实体:

data class MyEntity(
    val id: String,
    val createdAt: Instant,
    val updatedAt: Instant
)

抽象通用存储库:

abstract class GenericRepository<T> {

    abstract val copyFn: KCallable<T>

    fun add(obj: T) {
        val instanceParameter = copyFn.instanceParameter!!
        val idParameter = copyFn.findParameterByName("id")!!
        val copy = copyFn.callBy(
            mapOf(
                instanceParameter to obj,
                idParameter to "new id"
            )
        )
        // Do whatever you want with the copy
    }
}

或者更简洁、更通用的抽象通用存储库版本:

abstract class BetterGenericRepository<T> {

    abstract val copyFn: KCallable<T>

    fun add(obj: T): T {
        val instanceParameter = getInstanceParameter()
        val idParameter = getParameterByName(instanceParameter, "id")
        val updatedAtParameter = getParameterByName(instanceParameter, "updatedAt")
        val copy = copyFn.callBy(
            mapOf(
                instanceParameter to obj,
                idParameter to "new id",
                updatedAtParameter to Instant.now()
            )
        )
        // Do whatever you want with the copy
        return copy
    }

    private fun getInstanceParameter() =
        copyFn.instanceParameter
            ?: throw RuntimeException("${copyFn.returnType} must be Data Class or its method '${copyFn.name}' must have 'instanceParameter' as KParameter")

    private fun getParameterByName(instanceParameter: KParameter, name: String) =
        copyFn.findParameterByName(name)
            ?: throw RuntimeException("${instanceParameter.type} must have '$name' property")
}

Abstract Repository的具体实现

class MyRepository: BetterGenericRepository<MyEntity>() {
    override val copyFn = MyEntity::copy
}

简单检查:

fun main() {
    val repository = MyRepository()
    val entity = MyEntity(
        id = "1",
        createdAt = Instant.EPOCH,
        updatedAt = Instant.EPOCH
    )
    println(entity)
    println(repository.add(entity))
}

结果

MyEntity(id=1, createdAt=1970-01-01T00:00:00Z, updatedAt=1970-01-01T00:00:00Z)
MyEntity(id=new id, createdAt=1970-01-01T00:00:00Z, updatedAt=2020-08-26T13:29:42.982Z)