Есть ли способ потребовать, чтобы универсальный тип был классом данных в Котлине?

Следующее не работает, но, надеюсь, поможет вам понять, что я имею в виду:

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

}

Или есть лучший способ выполнить то, что я пытаюсь сделать?


person Community    schedule 05.06.2016    source источник


Ответы (4)


У меня такое ощущение, что на самом деле вы хотите, чтобы T мог копировать себя с новым идентификатором и иметь идентификатор. Не обязательно, что это класс данных. Таким образом, вы можете просто использовать интерфейс, чтобы определить это.

Например:

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
    }
}
person JB Nizet    schedule 05.06.2016
comment
Есть ли беспокойство по поводу того, что это вызывает бесконечный цикл? Там, где мы не можем вызвать super.copy, не вызовет ли он копирование рекурсивно? Я назвал свой copyID просто для уверенности. - person Chad Bingham; 14.03.2018
comment
Это работает. копия исходит из класса данных. имя копии может быть клоном или другим. - person Cafer Mert Ceyhan; 12.03.2020

Нет, классы data не имеют какого-либо конкретного представления в системе типов и их нельзя отличить от обычных классов (похожий вопрос).

Однако вы можете потребовать, чтобы методы класса data с определенным количеством компонентов имели интерфейс (фактически это будет интерфейс маркера для классов data).

Вот пример для классов data с двумя компонентами:

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

toString, hashCode и equals в любом случае можно вызывать для любого типа.

Затем просто отметьте свой класс 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 не t имеют собственный тип (и нет способа требовать, чтобы реализации интерфейса были подтипом определенного типа), но если вы пометите им только свои data классы, это должно работать (см. другой вариант ниже).

Другой недостаток заключается в том, что вы теряете параметры по умолчанию метода copy и должны вызывать его со всеми указанными параметрами:

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

Еще один вариант — определить эти интерфейсы как Data2<T1, T2, out Self>, class Impl(...): Data2<..., Impl> и заставить copy возвращать Self, но это не улучшит ситуацию, если вы будете использовать интерфейс как Data2<SomeType, SomeType, *>.

person hotkey    schedule 05.06.2016

Вы также можете реализовать копию или компонент 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
    }
}
person Arsenius    schedule 20.03.2019

Может быть не связано, потому что у меня была похожая, но немного другая проблема.

Мне нужно было перенести общую логику в суперкласс, и проблема заключалась в том, что я не могу использовать метод copy универсального T. Я нашел этот обходной путь:

Организация:

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 Generic Repository:

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)
person Anton    schedule 26.08.2020