Kotlin - 如果不是空,则用修改后的 obj 道具覆盖 obj 道具



TL;博士:

如何减少冗余(任何有效的方法都有帮助)?

if (personModification.firstName != null) {person.firstName = personModification.firstName}
if (personModification.lastName != null) {person.lastName = personModification.lastName}
if (personModification.job != null) {person.job = personModification.job}

长版本:我有一个简单的问题。我有一个班级Person

class Person (val firstName: String?, 
val lastName: String?, 
val job: String?)

我有一个名为PersonModification的类:

class PersonModification(val firstName: String?, 
val lastName: String?, 
val job: String?)

任务是用PersonModification值覆盖任何Person属性值,如果PersonModification属性未null。如果您关心,这背后的业务逻辑是一个 API 端点,它修改Person并将PersonModification作为参数(但可以更改所有或任何属性,因此我们不想用 null 覆盖有效的旧值)。解决方案如下所示。

if (personModification.firstName != null) {person.firstName = personModification.firstName}
if (personModification.lastName != null) {person.lastName = personModification.lastName}
if (personModification.job != null) {person.job = personModification.job}

有人告诉我这是多余的(我同意)。解决方案伪代码如下所示:

foreach(propName in personProps){
if (personModification["propName"] != null) {person["propName"] = personModification["propName"]}
}

当然,这不是JavaScript,所以它并不容易。我的反射解决方案在下面,但恕我直言,在这里有冗余比做反思更好。我还有哪些其他选项可以消除冗余?


重选:

package kotlin.reflect;
class Person (val firstName: String?, 
val lastName: String?, 
val job: String?)
class PersonModification(val firstName: String?, 
val lastName: String?, 
val job: String?)
// Reflection - a bad solution. Impossible without it.
//https://stackoverflow.com/questions/35525122/kotlin-data-class-how-to-read-the-value-of-property-if-i-dont-know-its-name-at
inline fun <reified T : Any> Any.getThroughReflection(propertyName: String): T? {
val getterName = "get" + propertyName.capitalize()
return try {
javaClass.getMethod(getterName).invoke(this) as? T
} catch (e: NoSuchMethodException) {
null
}
}
fun main(args: Array<String>) {
var person: Person = Person("Bob","Dylan","Artist")
val personModification: PersonModification = PersonModification("Jane","Smith","Placeholder")
val personClassPropertyNames = listOf("firstName", "lastName", "job")
for(properyName in personClassPropertyNames) {
println(properyName)
val currentValue = person.getThroughReflection<String>(properyName)
val modifiedValue = personModification.getThroughReflection<String>(properyName)
println(currentValue)
if(modifiedValue != null){
//Some packages or imports are missing for "output" and "it"
val property = outputs::class.memberProperties.find { it.name == "firstName" }
if (property is KMutableProperty<*>) {
property.setter.call(person, "123")
}
}
})
}

您可以在此处复制并粘贴以运行它:https://try.kotlinlang.org/

编写一个 5 行帮助程序来执行此操作应该非常简单,它甚至支持复制每个匹配的属性或仅选择属性。

尽管如果您正在编写 Kotlin 代码并大量使用数据类和val(不可变属性),它可能没有用。看看吧:

fun <T : Any, R : Any> T.copyPropsFrom(fromObject: R, skipNulls: Boolean = true, vararg props: KProperty<*>) {
// only consider mutable properties
val mutableProps = this::class.memberProperties.filterIsInstance<KMutableProperty<*>>()
// if source list is provided use that otherwise use all available properties
val sourceProps = if (props.isEmpty()) fromObject::class.memberProperties else props.toList()
// copy all matching
mutableProps.forEach { targetProp ->
sourceProps.find {
// make sure properties have same name and compatible types 
it.name == targetProp.name && targetProp.returnType.isSupertypeOf(it.returnType) 
}?.let { matchingProp ->
val copyValue = matchingProp.getter.call(fromObject);
if (!skipNulls || (skipNulls && copyValue != null)) {
targetProp.setter.call(this, copyValue)
}
}
}
}

这种方法使用反射,但它使用非常轻量级的 Kotlin 反射。我没有计时任何东西,但它的运行速度应该与手动复制属性几乎相同。

此外,它使用KProperty而不是字符串来定义属性的子集(如果您不希望复制所有属性),因此它具有完整的重构支持,因此如果您重命名类上的属性,则不必寻找字符串引用来重命名。

默认情况下,它将跳过空值,或者您可以将skipNulls参数切换为 false(默认值为 true)。

现在给出 2 个类:

data class DataOne(val propA: String, val propB: String)
data class DataTwo(var propA: String = "", var propB: String = "")

您可以执行以下操作:

var data2 = DataTwo()
var data1 = DataOne("a", "b")
println("Before")
println(data1)
println(data2)
// this copies all matching properties
data2.copyPropsFrom(data1)
println("After")
println(data1)
println(data2)
data2 = DataTwo()
data1 = DataOne("a", "b")
println("Before")
println(data1)
println(data2)
// this copies only matching properties from the provided list 
// with complete refactoring and completion support
data2.copyPropsFrom(data1, DataOne::propA)
println("After")
println(data1)
println(data2)

输出将是:

Before
DataOne(propA=a, propB=b)
DataTwo(propA=, propB=)
After
DataOne(propA=a, propB=b)
DataTwo(propA=a, propB=b)
Before
DataOne(propA=a, propB=b)
DataTwo(propA=, propB=)
After
DataOne(propA=a, propB=b)
DataTwo(propA=a, propB=)

这可以使用委托属性在不反射的情况下解决。请参阅:https://kotlinlang.org/docs/reference/delegated-properties.html

class Person(firstName: String?,
lastName: String?,
job: String?) {
val map = mutableMapOf<String, Any?>()
var firstName: String? by map
var lastName: String? by map
var job: String? by map
init {
this.firstName = firstName
this.lastName = lastName
this.job = job
}
}
class PersonModification(firstName: String?,
lastName: String?,
job: String?) {
val map = mutableMapOf<String, Any?>()
var firstName: String? by map
var lastName: String? by map
var job: String? by map
init {
this.firstName = firstName
this.lastName = lastName
this.job = job
}
}

fun main(args: Array<String>) {
val person = Person("Bob", "Dylan", "Artist")
val personModification1 = PersonModification("Jane", "Smith", "Placeholder")
val personModification2 = PersonModification(null, "Mueller", null)
println("Person: firstName: ${person.firstName}, lastName: ${person.lastName}, job: ${person.job}")
personModification1.map.entries.forEach { entry -> if (entry.value != null) person.map[entry.key] = entry.value }
println("Person: firstName: ${person.firstName}, lastName: ${person.lastName}, job: ${person.job}")
personModification2.map.entries.forEach { entry -> if (entry.value != null) person.map[entry.key] = entry.value }
println("Person: firstName: ${person.firstName}, lastName: ${person.lastName}, job: ${person.job}")

}

您可以为此创建一个很好的特征,您将能够将其应用于您可能拥有的任何修改类:

interface Updatable<T : Any> {
fun updateFrom(model: T) {
model::class.java.declaredFields.forEach { modelField ->
this::class.java.declaredFields
.filter { it.name == modelField.name && it.type == modelField.type }
.forEach { field ->
field.isAccessible = true
modelField.isAccessible = true
modelField.get(model)?.let { value ->
field.set(this, value)
}
}
}
}
}

用法:

data class Person(val firstName: String?,
val lastName: String?,
val job: String?) : Updatable<PersonModification>
data class PersonModification(val firstName: String?,
val lastName: String?,
val job: String?)

然后你可以尝试一下:

fun main(args: Array<String>) {
val person = Person(null, null, null)
val mod0 = PersonModification("John", null, null)
val mod1 = PersonModification(null, "Doe", null)
val mod2 = PersonModification(null, null, "Unemployed")
person.updateFrom(mod0)
println(person)
person.updateFrom(mod1)
println(person)
person.updateFrom(mod2)
println(person)
}

这将打印:

Person(firstName=John, lastName=null, job=null)
Person(firstName=John, lastName=Doe, job=null)
Person(firstName=John, lastName=Doe, job=Unemployed)

模型映射实用程序

您还可以使用众多模型映射实用程序之一,如 http://www.baeldung.com/java-performance-mapping-frameworks 中列出的实用程序(至少您已经看到了有关不同类型模型映射器的一些性能基准)。

请注意,如果您没有彻底测试它,我真的不建议您编写自己的映射实用程序。已经看到自定义映射实用程序增长和增长的示例,后来由于未考虑某些极端情况而导致奇怪的行为。

简化!= null

否则,如果你不是太懒惰,我宁愿推荐这样的东西:

personModification.firstName?.also { person.firstName = it }

它不需要任何反思,简单且仍然可读......不知何故至少;-)

委托属性

我想到的另一件事以某种方式与您的Javascript方法相匹配的是委托属性(我仅在支持的Map适合您的模型时才推荐委托属性;实际上我在下面显示的是使用HashMap的委派人员地图,我不能真正推荐,但是这是获得Javascript外观和感觉的非常简单和有用的方法;我不推荐它的原因:PersonMap吗?;-)).

class Person() : MutableMap<String, String?> by HashMap() { // alternatively use class Person(val personProps : MutableMap<String, String?> = HashMap()) instead and replace `this` below with personProps
var firstName by this
var lastName by this
var job by this
constructor(firstName : String?, lastName : String?, job : String?) : this() {
this.firstName = firstName
this.lastName = lastName
this.job = job
}
}

然后PersonModification类看起来基本相同。然后,应用映射将如下所示:

val person = Person("first", "last", null)
val personMod = PersonModification("new first", null, "new job")
personMod.filterValues { it != null }
.forEach { key, value -> person[key] = value } // here the benefit of extending the Map becomes visible: person[key] instead of person.personProps[key], but then again: person.personProps[key] is cleaner

如果你不需要那个辅助构造函数,那就更好了,那么类看起来几乎和以前一样,属性可以像以前一样设置和获取。

考虑到这一点,您实际上并不需要辅助构造函数,因为您仍然可以使用apply,然后只需添加您感兴趣的变量(几乎作为命名参数)。然后,该类将类似于:

class PersonModification : MutableMap<String, String?> by HashMap() { // or again simply: class PersonModification(props : MutableMap<String, String?> = HashMap()) and replacing `this` with props below
var firstName by this
var lastName by this
var job by this
}

然后实例化它如下所示:

val personMod = PersonModification().apply {
firstName = "new first"
job = "new job"
}

映射仍然相同。

已经有很多人提供了他们的解决方案。但我想再提供一点:

jackson中有一些有趣的功能,你可以尝试合并json。因此,您可以将 src 对象与PersonModification的反序列化版本合并

有了它,可以做这样的事情:

class ModificationTest {
@Test
fun test() {
val objectMapper = jacksonObjectMapper().apply {
setSerializationInclusion(JsonInclude.Include.NON_NULL)
}
fun Person.merge(personModification: PersonModification): Person = run {
val temp = objectMapper.writeValueAsString(personModification)
objectMapper.readerForUpdating(this).readValue(temp)
}
val simplePerson = Person("firstName", "lastName", "job")
val modification = PersonModification(firstName = "one_modified")
val modification2 = PersonModification(lastName = "lastName_modified")
val personAfterModification1: Person = simplePerson.merge(modification)
//Person(firstName=one_modified, lastName=lastName, job=job)
println(personAfterModification1)
val personAfterModification2: Person = personAfterModification1.merge(modification2)
//Person(firstName=one_modified, lastName=lastName_modified, job=job)
println(personAfterModification2)
}
}

希望这对您有所帮助!

为 Person 创建一个扩展函数:

fun Person.modify(pm: PersonModification) {
pm.firstName?.let { firstName = it }
pm.lastName?.let { lastName = it }
pm.job?.let { job = it }
}
fun Person.println() {
println("firstName=$firstName, lastName=$lastName, job=$job")
}

并像这样使用它:

fun main(args: Array <String> ) {
val p = Person("Nick", "Doe", "Cartoonist")
print("Person before: ")
p.println()
val pm = PersonModification("Maria", null, "Actress")
p.modify(pm)
print("Person after: ")
p.println()
}

或选择以下选项之一:

fun Person.println() {
println("firstName=$firstName, lastName=$lastName, job=$job")
}
fun main(args: Array <String> ) {
val p = Person("Nick", "Doe", "Cartoonist")
print("Person before: ")
p.println()
val pm = PersonModification("John", null, null)
pm.firstName?.run { p.firstName = this }.also { pm.lastName?.run { p.lastName = this } }.also { pm.job?.run { p.job = this } }
// or
pm.firstName?.also { p.firstName = it }.also { pm.lastName?.also { p.lastName = it } }.also { pm.job?.also { p.job = it } }
// or 
with (pm) {
firstName?.run { p.firstName = this }
lastName?.run { p.lastName= this }
job?.run { p.job= this }
}
print("Person after: ")
p.println()
}

它没什么花哨的,但它隐藏了外界变异Person的复杂性。

class Person(
var firstName: String?,
var lastName: String?,
var job: String?
) {
fun modify(p: PersonModification){
p.firstName?.let { firstName = it }
p.lastName?.let { lastName = it }
p.job?.let { job = it }
}
}
class PersonModification(/* ... */)

最新更新