Diberikan kelas dengan sekelompok anggota, saya ingin menggabungkan dua contoh itu. Instance yang dihasilkan harus mempertahankan nilai non-null dari masing-masing dari dua input. Jika dua nilai non-null bertentangan, pengecualian harus dimunculkan.

Implementasi saya saat ini agak berfungsi, tetapi tidak berskala dengan baik:

import kotlin.reflect.KProperty1
import kotlin.reflect.full.memberProperties
import kotlin.test.fail

class Thing(
    val a: Int,
    var b: String,
    val c: Int? = null,
    val d: Boolean? = null,
    val e: Long? = null,
    val f: String? = null,
    val g: String? = null
)

private fun <T> mergeThingProperty(property: KProperty1<Thing, *>, a: Thing, b: Thing): T {
    val propA = property.get(a)
    val propB = property.get(b)
    val mergedValue = if (propA != null && propB == null) {
        propA
    } else if (propA == null && propB != null) {
        propB
    } else if (propA != null && propB != null) {
        if (propA != propB) {
            throw RuntimeException("Can not merge Thing data on property ${property.name}: $propA vs. $propB.")
        } else {
            propA
        }
    } else {
        null
    }
    @Suppress("UNCHECKED_CAST")
    return mergedValue as T
}

fun mergeTwoThings(thing1: Thing, thing2: Thing): Thing {
    val properties = Thing::class.memberProperties.associateBy { it.name }
    val propertyMissingMsg = "Missing value in Thing properties"
    return Thing(
        mergeThingProperty(properties["a"] ?: error(propertyMissingMsg), thing1, thing2),
        mergeThingProperty(properties["b"] ?: error(propertyMissingMsg), thing1, thing2),
        mergeThingProperty(properties["c"] ?: error(propertyMissingMsg), thing1, thing2),
        mergeThingProperty(properties["d"] ?: error(propertyMissingMsg), thing1, thing2),
        mergeThingProperty(properties["e"] ?: error(propertyMissingMsg), thing1, thing2),
        mergeThingProperty(properties["f"] ?: error(propertyMissingMsg), thing1, thing2),
        mergeThingProperty(properties["g"] ?: error(propertyMissingMsg), thing1, thing2)
    )
}

fun main() {
    val result1 = mergeTwoThings(Thing(a = 42, b = "foo"), Thing(a = 42, b = "foo", c = 23))
    assert(result1.c == 23)
    assert(result1.d == null)

    try {
        mergeTwoThings(Thing(a = 42, b = "foo"), Thing(a = 42, b = "bar"))
        fail("An exception should have been thrown.")
    } catch (ex: RuntimeException) {
    }
}

Bagaimana saya bisa menghindari pengulangan manual setiap anggota (saat ini di mergeTwoThings)?

Juga, akan lebih baik jika saya tidak membutuhkan pemeran yang tidak dicentang (saat ini di mergeThingProperty).

0
Tobias Hermann 5 Mei 2020, 08:23

1 menjawab

Jawaban Terbaik

callBy dapat digunakan untuk memanggil fungsi (seperti konstruktor kelas) dengan argumen yang disediakan di peta. Solusinya kemudian terlihat sebagai berikut:

import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.primaryConstructor
import kotlin.reflect.full.valueParameters
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.fail

class Thing(
    val a: Int,
    var b: String,
    val c: Int? = null,
    val d: Boolean? = null,
    val e: Long? = null,
    val f: String? = null,
    val g: String? = null
)

inline fun <reified T : Any> getPrimaryConstructor() =
    T::class.primaryConstructor
        ?: throw RuntimeException("${T::class.qualifiedName} does not have a primary constructor.")

inline fun <reified T : Any> doConstructorParametersMatchMembers() =
    T::class
        .declaredMemberProperties
        .map { Pair(it.name, it.returnType) }
        .toSet() ==
            getPrimaryConstructor<T>()
                .valueParameters.map { Pair(it.name, it.type) }
                .toSet()

/**
 * Returns the single element, or `null` if the collection is empty, or throws an exception if the collection has more than one element.
 */
fun <T> Iterable<T>.nullOrExactlySingle() =
    when (toList().size) {
        0 -> null
        1 -> single()
        else -> throw IllegalArgumentException("Collection has more than one element.")
    }

inline fun <reified T : Any> mergeTwoObjects(a: T, b: T): T {
    assert(doConstructorParametersMatchMembers<T>()) {
        "Constructor parameters of ${T::class.qualifiedName} does not match its members."
    }
    val arguments = T::class
        .declaredMemberProperties
        .associateBy { it.name }
        .mapValues {
            listOfNotNull(
                it.value.get(a),
                it.value.get(b)
            )
                .toSet()
                .nullOrExactlySingle()
        }
        .filterValues { it != null }

    return getPrimaryConstructor<T>()
        .callBy(getPrimaryConstructor<T>()
            .valueParameters
            .associateWith {
                arguments[it.name]
            })
}

fun mergeTwoThings(thing1: Thing, thing2: Thing) = mergeTwoObjects(thing1, thing2)

fun main() {
    val result1 = mergeTwoThings(
        Thing(a = 42, b = "foo"),
        Thing(a = 42, b = "foo", c = 23)
    )
    assertEquals(23, result1.c)
    assertNull(result1.d)

    try {
        mergeTwoThings(
            Thing(a = 42, b = "foo"),
            Thing(a = 42, b = "bar")
        )
        fail("An exception should have been thrown.")
    } catch (ex: IllegalArgumentException) {
    }
}

0
Tobias Hermann 6 Mei 2020, 05:08