More generic solution (with some limitations)
- Allows to merge not only data classes but any classes (if there is an appropriate constructor, it may be not a no-arg constructor);
- Allows to merge both mutable and immutable properties(*see Limitations section), including ones defined in superclasses;
- No Java Reflection API (only Kotlin Reflection)
inline fun <reified T : Any> mergeOrNull(a: T, b: T): T? {
val availableConstructors = kClass.constructors.filter { it.visibility != PRIVATE && it.visibility != PROTECTED }
for (constructor in availableConstructors) {
return mergeOrNull(a, b, constructor) ?: continue
}
return null
}
inline fun <reified T : Any> mergeOrNull(a: T, b: T, constructor: KFunction<T>): T? {
//Match parameters with properties by name
val properties = T::class.memberProperties.associateBy { it.name }
val parameterToPropertyMap = constructor.parameters.associateWith { properties[it.name] }
//check there is no unmatched mandatory parameters
// (unmatched parameters with default values can be ignored)
val unmatchedMandatoryParameters = parameterToPropertyMap
.filterValues { it == null }.keys
.filter { !it.isOptional }
if (unmatchedMandatoryParameters.isNotEmpty()) {
log.debug(
"Failed to use $constructor constructor for merge. " +
"No matching property for mandatory constructor parameters: ${unmatchedMandatoryParameters.joinToString { it.name!! }}."
)
return null
}
val mergePropertyValues = { prop: KProperty1<T, *> -> prop.get(a) ?: prop.get(b) }
val args = parameterToPropertyMap.filterValues { it != null }.mapValues { (_, prop) -> mergePropertyValues(prop!!) }
val result = try {
constructor.callBy(args)
} catch (e: Throwable) {
log.debug("Failed to use $constructor constructor for merge. ${e.stackTraceToString()}")
return null
}
val (mutableProperties, immutableProperties) = T::class.memberProperties.partition { it is KMutableProperty1<T, *> }
mutableProperties.forEach { (it as KMutableProperty1<T, *>).setter.call(result, mergePropertyValues(it)) }
//check that all immutable properties were merged correctly
val incorrectlyMergedImmutableProperties = immutableProperties.filter { it.get(result) != mergePropertyValues(it) }
return when {
incorrectlyMergedImmutableProperties.isEmpty() -> result
else -> {
log.debug("Failed to use $constructor constructor for merge. " +
"Failed to correctly merge following immutable properties: ${
incorrectlyMergedImmutableProperties.joinToString {
"${it.name} (expected: ${mergePropertyValues(it)}, actual: ${it.get(result)})"
}
}")
null
}
}
}
Limitations
- Can't correctly merge immutable properties if there are several properties, evaluated from one constructor parameter when some of them evaluated to
null
, while some others - not.
data class DataStore(val x: String?) {
val y = "$x!!!"
}
val result = mergeOrNull(DataStore(null), DataStore("2")) // null
//Failed to use fun <init>(kotlin.String?): DataStore constructor for merge.
//Failed to correctly merge following immutable properties: y (expected: null!!!, actual: 2!!!)
- Can't correctly merge immutable properties if there is a same-named constructor parameter, but its value is not assigned as-is.
class DataStore(x: String?) {
val x = "$x!!!"
}
val result = mergeOrNull(DataStore("x"), DataStore(null)) // null
//Failed to use fun <init>(kotlin.String?): DataStore constructor for merge.
//Failed to correctly merge following immutable properties: x (expected: x!!!, actual: x!!!!!!)
Update (recursive merge for nested objects)
inline fun <reified T : Any> mergeOrNull(a: T, b: T) = mergeOrNull(a, b, T::class)
fun <T : Any> mergeOrNull(a: T, b: T, kClass: KClass<T>): T? {
//If a and b are primitives, merge strategy is simple
if (a is Int || a is Long || a is Double || a is Float || a is Short || a is Char || a is Byte || a is Boolean || a is String || a::class.isValue || a::class.objectInstance != null) return a
//Otherwise we need to construct new object
val availableConstructors = kClass.constructors.filter { it.visibility != PRIVATE && it.visibility != PROTECTED }
for (constructor in availableConstructors) {
return mergeOrNull(a, b, kClass, constructor) ?: continue
}
return null
}
private fun <T : Any> mergeOrNull(a: T, b: T, kClass: KClass<T>, constructor: KFunction<T>): T? {
//Match parameters with properties by name
val properties = kClass.memberProperties.associateBy { it.name }
val parameterToPropertyMap = constructor.parameters.associateWith { properties[it.name] }
//check there is no unmatched mandatory parameters
// (unmatched parameters with default values can be ignored)
val unmatchedMandatoryParameters = parameterToPropertyMap
.filterValues { it == null }.keys
.filter { !it.isOptional }
if (unmatchedMandatoryParameters.isNotEmpty()) {
log.debug(
"Failed to use $constructor constructor for merge. " +
"No matching property for mandatory constructor parameters: ${unmatchedMandatoryParameters.joinToString { it.name!! }}."
)
return null
}
val mergePropertyValues = { prop: KProperty1<T, *> ->
val propA = prop.get(a)
val propB = prop.get(b)
@Suppress("UNCHECKED_CAST")
if (propA != null && propB != null) mergeOrNull(propA, propB, propA::class as KClass<Any>) else propA ?: propB
}
val args = parameterToPropertyMap.filterValues { it != null }.mapValues { (_, prop) -> mergePropertyValues(prop!!) }
val result = try {
constructor.callBy(args)
} catch (e: Throwable) {
log.debug("Failed to use $constructor constructor for merge. ${e.stackTraceToString()}")
return null
}
val (mutableProperties, immutableProperties) = kClass.memberProperties.partition { it is KMutableProperty1<T, *> }
mutableProperties.forEach { (it as KMutableProperty1<T, *>).setter.call(result, mergePropertyValues(it)) }
//check that all immutable properties were merged correctly
val incorrectlyMergedImmutableProperties = immutableProperties.filter { it.get(result) != mergePropertyValues(it) }
return when {
incorrectlyMergedImmutableProperties.isEmpty() -> result
else -> {
log.debug("Failed to use $constructor constructor for merge. " +
"Failed to correctly merge following immutable properties: ${
incorrectlyMergedImmutableProperties.joinToString {
"${it.name} (expected: ${mergePropertyValues(it)}, actual: ${it.get(result)})"
}
}")
null
}
}
}