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实现中的一个缺陷是它没有单独的层次结构ReadOnly
与Immutable
所以如果调用者调用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() } }