提问人:Daniel Florez Cortes 提问时间:1/7/2023 更新时间:1/7/2023 访问量:519
Kotlin 修改 map 中的 dataclass 对象键会在修改变量后更改引用
Kotlin modifying dataclass object key from map changes the reference after modifying variable
问:
我有一个MutableMap,它的键是来自DataClass(User数据类型)的对象,值是来自其他Dataclass(Dog数据类型)的数组。如果我有一个带有 User 对象的变量,并且我把它放在 MutableMap 中并测试映射是否包含 User,它会说这是真的。但是,在将用户放入 MutableMap 之后,如果我使用保存 User 对象的变量更改 User 对象的属性之一,Map 会说它不包含用户对象。
这是一个示例
data class User(
var name: String,
var project: String,
)
data class Dog(
var kind: String
)
fun main(args: Array<String>) {
var mapUserDogs: MutableMap<User, MutableList<Dog>> = mutableMapOf()
var userSelected = User("name2", "P2")
mapUserDogs.put(
User("name1", "P1"),
mutableListOf(Dog("R1"), Dog("R2"))
)
mapUserDogs.put(
userSelected,
mutableListOf(Dog("R21"), Dog("R31"))
)
println(userSelected)
println(mapUserDogs.keys.toString())
println(mapUserDogs.contains(userSelected))
println(mapUserDogs.values.toString())
println("\n")
userSelected.name = "Name3"
println(userSelected)
println(mapUserDogs.keys.toString())
println(mapUserDogs.contains(userSelected))
println(mapUserDogs.values.toString())
}
prints 语句显示如下:
User(name=name2, project=P2)
[User(name=name1, project=P1), User(name=name2, project=P2)]
true
[[Dog(kind=R1), Dog(kind=R2)], [Dog(kind=R21), Dog(kind=R31)]]
User(name=Name3, project=P2)
[User(name=name1, project=P1), User(name=Name3, project=P2)]
false
[[Dog(kind=R1), Dog(kind=R2)], [Dog(kind=R21), Dog(kind=R31)]]
Process finished with exit code 0
但这没有意义。如果很明显,地图在修改后仍然保留对它的引用,为什么地图说它不包含用户对象?
User(name=Name3, project=P2)
[User(name=name1, project=P1), User(name=Name3, project=P2)]
当我修改 userSelected 变量时,keys 集合中的用户也发生了变化,所以现在该对象在变量和 Map 键中的属性名称都为“Name3”,但它仍然说它不包含它。
我能做些什么才能更改userSelected对象中的属性,并且在使用“contains”方法时Map仍然返回true?反向执行相同的过程也显示了相同的结果。如果我从地图中获取用户并修改它,则userVariable也会被修改,但是如果我稍后测试地图是否包含userVariable,则会显示false。
答:
发生这种情况是因为 Kotlin 中的数据类是按值比较的,而常规类是通过引用进行比较的。当您使用数据类作为键时,将搜索具有相同字符串值的 和 字段的映射,而不是内存中的对象本身。User
name
project
例如:
data class User(
var name: String,
var project: String,
)
val user1 = User("Daniel", "Something Cool")
val user2 = User("Daniel", "Something Cool")
println(user1 == user2) // true
之所以有效,是因为即使它们是不同的对象(因此也是不同的引用),它们也具有相同的 AND 值。
但是,如果我要这样做:name
project
user1.name = "Christian"
println(user1 == user2) // false
答案是错误的,因为它们的所有字段都不具有相同的值。
如果我做了一个标准类:User
class User(
var name: String,
var project: String,
)
val user1 = User("Daniel", "Something Cool")
val user2 = User("Daniel", "Something Cool")
println(user1 == user2) // false
答案是错误的,因为它们是不同的引用,即使它们具有相同的值。若要使代码按所需方式工作,请使 User
成为常规类而不是数据类
。
这就是常规类和数据类之间的主要区别:类是通过引用传递的,数据类是通过值传递的。数据类只不过是值的集合,其中(可选)附加了一些方法,类是单独的对象。
评论
equals()
hashcode()
copy()
equals
hashcode
data class
我该怎么做才能更改userSelected对象中的属性,并且在使用“contains”方法时Map仍然返回true?
您无法执行任何操作来保留您在映射中查找条目的能力和修改密钥的能力。
使数据类不可变(而不是 等),当您需要更改映射时,请删除旧键并放入新键。这真的是你唯一能做的有用的事情。val
var
补充 Louis Wasserman 的正确答案:
这就是 Kotlin 中地图的工作方式:他们的合同要求密钥一旦存储就不会发生重大变化。* 的文档说明了这一点:java.util.Map
注意:如果将可变对象用作映射键,则必须格外小心。如果对象的值在对象是映射中的键时以影响等于比较的方式更改,则不会指定映射的行为。
最安全的方法是仅使用不可变对象作为键。(请注意,不仅是对象本身,而且它引用的任何对象等都必须是不可变的,才能完全安全。
只要密钥存储在映射中,您就可以避免使用可变密钥,只要您小心不要更改任何会影响调用它的结果的内容。(如果对象需要一些初始设置,而这些设置不能全部在其构造函数中完成,或者为了避免类的可变和不可变变体,这可能是合适的。但是它不容易保证,并且会给将来的维护留下潜在的问题,因此完全不可变性是可取的。equals()
更改密钥的影响可能是显而易见的,也可能是微妙的。正如 OP 所注意到的,映射可能会消失,以后可能会重新出现。但根据确切的映射实现,它可能会导致进一步的问题,例如获取/添加/删除不相关映射时出错、内存泄漏,甚至无限循环。(“行为......未指定“意味着任何事情都可能发生!
我该怎么做才能更改userSelected对象中的属性,并且在使用“contains”方法时Map仍然返回true?
你要做的是改变映射。如果您存储从键 K1 到值 V 的映射,并且您更改键以保存 K2,那么您实际上是在说“K1 不再映射到 V;相反,K2 现在映射到 V。
因此,正确的方法是删除旧映射,然后添加新映射。如果密钥是不可变的,这就是您必须执行的操作,但即使密钥是可变的,您也必须在更改之前删除旧映射,然后在更改后添加新映射,以便它在存储在映射中时永远不会更改。
(* 不幸的是,Kotlin 库文档没有解决这个问题——恕我直言,与示例性 Java 文档相比,这是它们缺乏的众多领域之一......
评论
equals()
equals()
评论