Kotlin数据类中的可变集合的防御副本

我想有一个接受只读列表的数据类:

data class Notebook(val notes: List<String>) { } 

但是它也可以接受MutableList ,因为它是List一个子类型。

例如下面的代码修改传入的列表:

 fun main(args: Array<String>) { val notes = arrayListOf("One", "Two") val notebook = Notebook(notes) notes.add("Three") println(notebook) // prints: Notebook(notes=[One, Two, Three]) } 

有没有办法在数据类中执行传入列表的防御副本?

我认为更好的是使用JetBrains库不可变的集合 – https://github.com/Kotlin/kotlinx.collections.immutable

导入您的项目

添加bintray存储库:

 repositories { maven { url "http://dl.bintray.com/kotlin/kotlinx" } } 

添加依赖项:

 compile 'org.jetbrains.kotlinx:kotlinx-collections-immutable:0.1' 

结果:

 data class Notebook(val notes: ImmutableList<String>) {} fun main(args: Array<String>) { val notes = immutableListOf("One", "Two") val notebook = Notebook(notes) notes.add("Three") // creates a new collection println(notebook) // prints: Notebook(notes=[One, Two]) } 

注意:您也可以使用ImmutableList添加和删除方法,但是这种方法不会修改当前列表,只是用您的更改创建一个新列表并将其返回给您。

选项1

您可以使用.toList()来创建一个副本,但是您需要在内部保存列表副本的另一个属性,或者您需要从数据类移到正常的类。

作为一个数据类:

 data class Notebook(private val _notes: List<String>) { val notes: List<String> = _notes.toList() } 

这里的问题是你的数据类将有一个潜在的变异列表.equals().hashCode()

所以另一种方法是使用一个普通的类:

 class Notebook(notes: List<String>) { val notes: List<String> = notes.toList() } 

选项2

Kotlin团队也在研究真正不可变的集合,如果它们足够稳定以便使用,可以预览它们: https : //github.com/Kotlin/kotlinx.collections.immutable


选项3

另一种方法是创建一个允许使用MutableList的后代类型的MutableList 。 这正是Klutter库通过创建轻量级委托类的层次结构来实现的,该委托类可以包装列表以确保不会发生突变。 由于他们使用委托,他们没有什么开销。 您可以使用这个库,或者查看源代码作为如何创建这种类型的受保护集合的示例。 然后你改变你的方法来要求这个保护版本,而不是原来的。 查看Klutter ReadOnly Collection Wrappers的源代码和相关的想法测试 。

作为使用Klutter的这些类的一个例子,数据类将是:

 data class Notebook(val notes: ReadOnlyList<String>) { 

而呼叫者将被迫通过传递一个非常简单的包装清单来遵守:

 val myList = mutableListOf("day", "night") Notebook(myList.toImmutable()) // copy and protect 

发生了什么事是调用者(通过调用asReadOnly() )使防御副本满足您的方法的要求,并且由于这些类的设计方式而无法再对受保护的副本进行变异。

在Klutter实现中的一个缺陷是它没有单独的层次结构ReadOnlyImmutable所以如果调用者调用asReadOnly()列表的持有者仍然可以导致突变。 所以在你的代码版本(或者Klutter的更新版本)中,最好确保你所有的工厂方法都是复制的,并且绝不允许以任何其他的方式构造这些类(也就是使构造函数在internal )。 或者在副本明确时使用第二层次。 最简单的方法是将代码复制到自己的库中,删除asReadOnly()方法,只留下toImmutable() ,并使集合类构造函数全部在internal


更多信息

另请参阅: Kotlin和不可变的集合?

没有办法覆盖在主构造函数中声明的属性的赋值。 如果您需要自定义分配,则必须将其从构造函数中移出,但是不能再让您的类成为数据类。

 class Notebook(notes: List<String>) { val notes: List<String> = notes.toList() } 

如果你必须保留一个数据类,我唯一​​的方法就是使用init块来创建副本,但是你必须让你的属性变成一个var ,因为第一个任务的财产将自动发生,并在这里你修改它之后。

 data class Notebook(var notes: List<String>) { init { notes = notes.toList() } }