Defensive copy of a mutable collection in Kotlin d

2019-06-22 08:09发布

问题:

I want to have a data class accepting a read-only list:

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

But it can accept MutableList as well, because it is a subtype of the List.

For example the following code modifies the passed in 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])
}

Is there a way how perform defensive copy of the passed in list in the data class?

回答1:

I think better is to use JetBrains library for immutable collections - https://github.com/Kotlin/kotlinx.collections.immutable

Import in your project

Add the bintray repository:

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

Add the dependency:

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

Result:

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])
}

Note: You also can use add and remove methods with ImmutableList, but this methods doesn't modify current list just creates new one with your changes and return it for you.



回答2:

Option 1

You can use .toList() to make a copy, although you will need another property internally that holds the list copy or you need to move away from a data class to a normal class.

As a data class:

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

The problem here is that your data class is going to have .equals() and .hashCode() based on a potentially mutating list.

So the alternative is to use a normal class:

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

Option 2

Kotlin team is working on truly immutable collections as well, you might be able to preview them if they are stable enough for use: https://github.com/Kotlin/kotlinx.collections.immutable


Option 3

Another way would be to create an interface that does allow the descendant type of MutableList to be used. This is exactly what the Klutter library does by creating a hierarchy of light-weight delegating classes that can wrap lists to ensure no mutation is possible. Since they use delegation they have little overhead. You can use this library, or just look at the source code as an example of how to create this type of protected collections. Then you change your method to ask for this protected version instead of the original. See the source code for Klutter ReadOnly Collection Wrappers and associated tests for ideas.

As an example of using these classes from Klutter, the data class would be:

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

And the caller would be forced to comply by passing in a wrapped list which is pretty simple:

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

What is happening is that the caller (by invoking asReadOnly()) is making the defensive copy to satisfy the requirements of your method, and there is no way to then mutate the protected copy because of how these classes are designed.

One flaw in the Klutter implementation is that it does not have a separate hierarchy for ReadOnly vs. Immutable so if the caller instead calls asReadOnly() the holder of the list can still cause mutation. So in your version of this code (or an update to Klutter) it would be best to make sure all of your factory methods always make a copy and never allow these classes to be constructed in any other way (i.e. make constructors internal). Or have a second hierarchy that is used when the copy has clearly been made. The simplest way is to copy the code to your own library, remove the asReadOnly() methods leaving only toImmutable() and make the collection class constructors all internal.


Further information

see also: Kotlin and Immutable Collections?



回答3:

There is no way to override the assignment of a property declared in the primary constructor. If you need a custom assignment, you'll have to move it out of the constructor, but then you can no longer make your class a data class.

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

If you must keep it a data class, the only way I see for doing this is to use the init block to make the copy, but you'll have to make your property a var to be able to do this, since the first assignment of the property will happen automatically, and here you're modifying it afterwards.

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


回答4:

You should use JetBrains library for immutable collections, import this library in your app project.

Add bintray repository:

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

Then Add dependencies:

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

Result will be

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])
}