Kotlin 修改 map 中的 dataclass 对象键会在修改变量后更改引用

Kotlin modifying dataclass object key from map changes the reference after modifying variable

提问人:Daniel Florez Cortes 提问时间:1/7/2023 更新时间:1/7/2023 访问量:519

问:

我有一个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 引用 相等 数据类 mutablemap

评论


答:

-2赞 Christian Hollinger 1/7/2023 #1

发生这种情况是因为 Kotlin 中的数据类是按比较的,而常规类是通过引用进行比较的。当您使用数据类作为键时,将搜索具有相同字符串值的 和 字段的映射,而不是内存中的对象本身。Usernameproject

例如:

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 值。 但是,如果我要这样做:nameproject

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 成为常规类而不是数据类。 这就是常规类和数据类之间的主要区别:类是通过引用传递的,数据类是通过值传递的。数据类只不过是值的集合,其中(可选)附加了一些方法,类是单独的对象。

评论

1赞 Tenfour04 1/7/2023
这是不正确的。所有类都通过其 和 函数进行比较,无论类是否为数据类,都可以以您喜欢的任何方式定义这些函数。它可能会也可能不会通过引用或值或类中某些属性子集进行比较。所有非内联类始终通过引用传递,无论它们是否为数据类。数据类是常规类的子集,添加了隐式函数和 的隐式覆盖。它们的处理方式与任何其他类完全相同。equals()hashcode()copy()equalshashcode
3赞 Tenfour04 1/7/2023
简而言之,它只不过是定义特定类型的“常规”类的语法快捷方式。该类本身没有任何特殊地位或任何不同的待遇。data class
2赞 Louis Wasserman 1/7/2023 #2

我该怎么做才能更改userSelected对象中的属性,并且在使用“contains”方法时Map仍然返回true?

您无法执行任何操作来保留您在映射中查找条目的能力和修改密钥的能力。

使数据类不可变(而不是 等),当您需要更改映射时,请删除旧键并放入新键。这真的是你唯一能做的有用的事情。valvar

1赞 gidds 1/7/2023 #3

补充 Louis Wasserman 的正确答案:

这就是 Kotlin 中地图的工作方式:他们的合同要求密钥一旦存储就不会发生重大变化。* 的文档说明了这一点java.util.Map

注意:如果将可变对象用作映射键,则必须格外小心。如果对象的值在对象是映射中的键时以影响等于比较的方式更改,则不会指定映射的行为。

最安全的方法是仅使用不可变对象作为键。(请注意,不仅是对象本身,而且它引用的任何对象等都必须是不可变的,才能完全安全。 只要密钥存储在映射中,您就可以避免使用可变密钥,只要您小心不要更改任何会影响调用它的结果的内容。(如果对象需要一些初始设置,而这些设置不能全部在其构造函数中完成,或者为了避免类的可变和不可变变体,这可能是合适的。但是它不容易保证,并且会给将来的维护留下潜在的问题,因此完全不可变性是可取的。equals()

更改密钥的影响可能是显而易见的,也可能是微妙的。正如 OP 所注意到的,映射可能会消失,以后可能会重新出现。但根据确切的映射实现,它可能会导致进一步的问题,例如获取/添加/删除不相关映射时出错、内存泄漏,甚至无限循环。(“行为......未指定“意味着任何事情都可能发生!

我该怎么做才能更改userSelected对象中的属性,并且在使用“contains”方法时Map仍然返回true?

你要做的是改变映射。如果您存储从键 K1 到值 V 的映射,并且您更改键以保存 K2,那么您实际上是在说“K1 不再映射到 V;相反,K2 现在映射到 V。

因此,正确的方法是删除旧映射,然后添加新映射。如果密钥是不可变的,这就是您必须执行的操作,但即使密钥是可变的,您也必须在更改之前删除旧映射,然后在更改后添加新映射,以便它在存储在映射中时永远不会更改。


(* 不幸的是,Kotlin 库文档没有解决这个问题——恕我直言,与示例性 Java 文档相比,这是它们缺乏的众多领域之一......

评论

0赞 Daniel Florez Cortes 1/8/2023
哇,这么棒的答案!我没有考虑使用可变对象作为键的后果。非常感谢!当我阅读 Louis 的答案时,我只是想制作一个自定义包含函数来测试数据类对象是否在映射键中,但现在我害怕键消失或其他可能的错误。我只是要改变实现。谢谢伙计!
0赞 Daniel Florez Cortes 1/8/2023
嘿,@gidds,我想问你关于“价值观”的问题。在我的示例中,Map 的“值”是数据类对象的数组。如果我更改值数组中对象的属性,也会出现同样的问题?或者这样做是安全的?
0赞 gidds 1/8/2023
将可变对象作为映射中的值没有问题,因为映射不需要对它们进行任何处理——特别是,它不需要调用它们,因此可以更改而不会产生任何后果。equals()
0赞 gidds 1/8/2023
思考如何自己实现一个简单的映射可能会有所帮助——例如,使用两个并行数组,一个键,另一个包含相应的值。要获取键的值,您必须扫描键数组,调用每个键,直到找到正确的键,然后您可以从另一个数组中的相应项中获取其值。(当然,在实践中,大多数地图的实现方式都比这复杂和高效得多,但这仍然是一个有用的练习。equals()