有没有一种方法可以在Kotlin中要求泛型类型为数据类?

31
以下内容无法正常工作,但希望能帮助您理解我的意思:
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)
  }

}

还有其他更好的方法来完成我想做的事吗?
4个回答

17

我有一种感觉,你实际上想要的是 T 能够复制自己并具有新的ID,而不是一定要作为数据类。因此,你可以使用接口来定义这个功能。

例如:

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
    }
}

2
这会引起无限循环的担忧吗?如果我们不能真正调用super.copy,它不会递归调用copy吗?我将我的命名为copyID只是为了确保。 - Chad Bingham
1
它正在工作。复制来自数据类。复制名称可以是克隆或其他。 - Cafer Mert Ceyhan

5
不,数据类在类型系统中没有特定的表示形式,无法与常规类区分(similar question)。
但是,您可以使用接口要求具有某些组件数量的数据类的方法(实际上它将成为数据类上的标记接口)。
以下是具有两个组件的数据类的示例:
interface Data2<T1, T2> {
    operator fun component1(): T1
    operator fun component2(): T2
    fun copy(t1: T1, t2: T2): Data2<T1, T2>
}

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

然后只需将您的data类标记为该接口:

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类,它应该可以工作(请参见下面的另一种选项)。

另一个缺点是你失去了copy方法的默认参数,必须使用指定所有参数来调用它:

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

另一个选择是将这些接口定义为 Data2<T1, T2, out Self>class Impl(...): Data2<..., Impl>,并使 copy 返回 Self,但如果您将接口用作 Data2<SomeType, SomeType, *>,它并不会变得更好。

4

您也可以以更通用的方式实现复制或组件1、组件2。

例如:

    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
    }
}

看起来很好,但是不工作。在方法“add”中,您对“BarBaz”没有知识,因此您也不了解字段“id”。您可以将该知识添加到“Copyable”中,但不会有太大变化,因为您仍然无法在“copy” lambda内构造副本。 - Jacek Pietras

0

可能与我的问题类似但略有不同,可能并不相关。

我需要将共享逻辑移动到超类中,问题在于我无法使用泛型Tcopy方法。我找到了这个解决方法:

实体:

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")
}

抽象存储库的具体实现

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)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接