Защитная копия изменяемой коллекции в классе данных 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])
}

Есть ли способ, как выполнить защитную копию переданного в списке в классе данных?

3 4

3 ответа:

Я думаю, что лучше использовать библиотеку 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])
}

Примечание: Вы также можете использовать методы add и remove с 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

Команда Котлина также работает над действительно неизменяемыми коллекциями, вы можете просмотреть их, если они достаточно стабильны для использования: https://github.com/Kotlin/kotlinx.collections.immutable


Вариант 3

Другим способом было бы создать интерфейс, который действительно позволяет использовать тип потомка MutableList. Это именно то, что делает библиотека Klutter, создавая иерархию легковесных делегирующих классов, которые могут обернуть списки, чтобы гарантировать отсутствие мутации. возможный. Поскольку они используют Делегирование, у них мало накладных расходов. Вы можете использовать эту библиотеку или просто посмотреть на исходный код как пример того, как создать этот тип защищенных коллекций. Затем вы измените свой метод, чтобы запросить эту защищенную версию вместо оригинала. Смотрите исходный код дляClutter 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() и сделайте конструкторы класса коллекции all internal.


Дополнительная информация

Смотрите также: Котлин и неизменные коллекции?

Невозможно переопределить назначение свойства, объявленного в основном конструкторе. Если вам нужно пользовательское назначение, вам придется переместить его из конструктора, но тогда вы больше не сможете сделать свой класс классом данных.

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

Если вы должны сохранить его классом данных, единственный способ, который я вижу для этого, - это использовать блок init для создания копии, но вам придется сделать ваше свойство var, чтобы иметь возможность сделать это, так как первое назначение свойства произойдет автоматически, и вот вы изменяете его впоследствии.

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