Kotlin 和不可变集合?

Kotlin and Immutable Collections?

我正在学习 Kotlin,看起来我可能想在明年将其用作我的主要语言。但是,我不断收到相互矛盾的研究,认为 Kotlin 有或没有不可变集合,我想弄清楚我是否需​​要使用 Google 番石榴。

有人可以给我一些指导吗?它默认使用不可变集合吗?什么运算符 return 可变或不可变集合?如果没有,是否有实施的计划?

标准库中的 Kotlin List 是只读的:

interface List<out E> : Collection<E> (source)

A generic ordered collection of elements. Methods in this interface support only read-only access to the list; read/write access is supported through the MutableList interface.

Parameters
E - the type of elements contained in the list.

如前所述,还有 MutableList

interface MutableList<E> : List<E>, MutableCollection<E> (source)

A generic ordered collection of elements that supports adding and removing elements.

Parameters
E - the type of elements contained in the list.

因此,Kotlin 通过其接口强制执行只读行为,而不是像默认 Java 实现那样在运行时抛出异常。

同样,还有 MutableCollectionMutableIterableMutableIteratorMutableListIteratorMutableMapMutableSet,请参阅 stdlib 文档。

这很令人困惑,但是有三种而不是两种类型的不变性:

  1. 可变 - 你应该改变集合(Kotlin 的 MutableList
  2. 只读 - 你不应该改变它(Kotlin 的 List)但有些东西可能(投射到可变,或从 Java 改变)
  3. 不可变 - 没有人可以更改它(Guavas 的不可变集合)

所以情况 (2) List 只是一个没有变异方法的接口,但是如果将其转换为 MutableList.

则可以更改实例

使用 Guava(案例 (3)),任何人都可以安全地更改集合,即使是强制转换或来自另一个线程也是如此。

Kotlin 选择只读以便直接使用 Java 集合,因此在使用 Java 集合时没有开销或转换..

Kotlin 1.0 将不会在标准库中包含不可变集合。但是,它确实具有只读和可变 接口 。没有什么能阻止您使用第三方不可变集合库。

Kotlin List interface "support only read-only access to the list" while methods in its MutableList 接口中的方法支持 "adding and removing elements"。然而,这两者都只是 接口.

Kotlin 的 List 接口在编译时强制执行只读访问,而不是像 java.util.Collections.unmodifiableList(java.util.List) (which "returns an unmodifiable view of the specified list... [where] attempts to modify the returned list... result in an UnsupportedOperationException 那样将此类检查推迟到 运行 时间。”它不会强制不变性。

考虑以下 Kotlin 代码:

import com.google.common.collect.ImmutableList
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith

fun main(args: Array<String>) {
    val readOnlyList: List<Int> = arrayListOf(1, 2, 3)
    val mutableList: MutableList<Int> = readOnlyList as MutableList<Int>
    val immutableList: ImmutableList<Int> = ImmutableList.copyOf(readOnlyList)

    assertEquals(readOnlyList, mutableList)
    assertEquals(mutableList, immutableList)

    // readOnlyList.add(4) // Kotlin: Unresolved reference: add
    mutableList.add(4)
    assertFailsWith(UnsupportedOperationException::class) { immutableList.add(4) }

    assertEquals(readOnlyList, mutableList)
    assertEquals(mutableList, immutableList)
}

注意 readOnlyList 是一个 List 并且 add 等方法无法解析(也不会编译),mutableList 自然可以变异,并且immutableList 上的 add(来自 Google Guava)也可以在编译时解决,但会在 运行 时抛出异常。

除了最后一个导致Exception in thread "main" java.lang.AssertionError: Expected <[1, 2, 3, 4]>, actual <[1, 2, 3]>.的断言外,上述所有断言都通过了,即我们成功地改变了只读List

请注意,使用 listOf(...) 而不是 arrayListOf(...) returns 是一个有效的不可变列表,因为您不能将其转换为任何可变列表类型。但是,为变量使用 List 接口不会阻止将 MutableList 分配给它(MutableList<E> 扩展 List<E>)。

最后,请注意 Kotlin 中的接口(以及 Java 中的接口)不能强制不变性,因为它 "cannot store state"(参见 Interfaces)。因此,如果你想要一个不可变的集合,你需要使用类似 Google Guava.

提供的东西。

另见 ImmutableCollectionsExplained · google/guava Wiki · GitHub

正如您在其他答案中看到的那样,Kotlin 具有可变集合的只读接口,可让您通过只读镜头查看集合。但是可以通过强制转换或从 Java 操纵来绕过该集合。但是在很好的协作 Kotlin 代码中,大多数用途不需要真正不可变的集合,如果您的团队避免强制转换为集合的可变形式,那么您可能不需要完全不可变的集合。

Kotlin 集合允许更改时复制突变和惰性突变。因此,为了回答您的部分问题,诸如 filtermapflatmap、运算符 + - 之类的东西在用于非惰性集合时都会创建副本。当在 Sequence 上使用时,它们会在访问时将值修改为集合,并继续保持惰性(导致另一个 Sequence)。尽管对于 Sequence,调用 toListtoSettoMap 等任何内容都会生成最终副本。根据命名约定,几乎所有以 to 开头的内容都在制作副本。

换句话说,大多数运算符 return 与您开始使用的类型相同,如果该类型是“只读”,那么您将收到一份副本。如果该类型是惰性的,那么您将惰性地应用更改,直到您需要整个集合。

有些人出于其他原因需要它们,例如并行处理。在这些情况下,最好查看专为这些目的而设计的真正高性能的集合。并且只在这些情况下使用它们,而不是在所有一般情况下使用它们。

在 JVM 世界中,很难避免与需要标准 Java 集合的库进行互操作,并且转换 to/from 这些集合会给不支持的库增加很多痛苦和开销通用接口。 Kotlin 很好地结合了互操作和缺乏转换,并通过合同提供只读保护。

因此,如果您无法避免想要不可变的集合,Kotlin 可以轻松地使用 JVM 中的任何东西 space:

此外,Kotlin 团队正在为 Kotlin 本地开发不可变集合,可以在此处看到这项工作: https://github.com/Kotlin/kotlinx.collections.immutable

还有许多其他的集合框架可以满足所有不同的需求和限制,Google 是您寻找它们的朋友。 Kotlin 团队没有理由需要为其标准库重新发明它们。你有很多选择,他们专注于不同的事情,如性能、内存使用、非装箱、不变性等。“选择是好的”……因此还有一些:HPCC, HPCC-RT, FastUtil, Koloboke, Trove 等等……

甚至还有像 Pure4J 这样的努力,因为 Kotlin 现在支持注释处理,也许可以有一个端口到 Kotlin 以实现类似的理想。

注意: 这个答案在这里是因为代码简单且开源,您可以使用这个想法使您创建的集合不可变。它不仅仅作为图书馆的广告。

Klutter library 中,是新的 Kotlin 不可变包装器,它使用 Kotlin 委托来包装现有的 Kotlin 集合接口和保护层,而不会影响任何性能。然后就无法将集合、它的迭代器或它可能 return 的其他集合转换为可以修改的内容。它们实际上变得不可变。

Klutter 1.20.0 released which adds immutable protectors for existing collections, based on a provides a light-weight delegate around collections that prevents any avenue of modification including casting to a mutable type then modifying. And Klutter goes a step further by protecting sub collections such as iterator, listIterator, entrySet, etc. All of those doors are closed and using Kotlin delegation for most methods you take no hit in performance. Simply call myCollection.asReadonly() (protect) or myCollection.toImmutable() (copy then protect) and the result is the same interface but protected.

这是代码中的一个示例,展示了该技术的简单性,基本上是将接口委托给实际的 class,同时覆盖突变方法和任何子集合 returned 被包裹在苍蝇。

/**
 * Wraps a List with a lightweight delegating class that prevents casting back to mutable type
 */
open class ReadOnlyList <T>(protected val delegate: List<T>) : List<T> by delegate, ReadOnly, Serializable {
    companion object {
        @JvmField val serialVersionUID = 1L
    }

    override fun iterator(): Iterator<T> {
        return delegate.iterator().asReadOnly()
    }

    override fun listIterator(): ListIterator<T> {
        return delegate.listIterator().asReadOnly()
    }

    override fun listIterator(index: Int): ListIterator<T> {
        return delegate.listIterator(index).asReadOnly()
    }

    override fun subList(fromIndex: Int, toIndex: Int): List<T> {
        return delegate.subList(fromIndex, toIndex).asReadOnly()
    }

    override fun toString(): String {
        return "ReadOnly: ${super.toString()}"
    }

    override fun equals(other: Any?): Boolean {
        return delegate.equals(other)
    }

    override fun hashCode(): Int {
        return delegate.hashCode()
    }
}

连同辅助扩展功能,使其易于访问:

/**
 * Wraps the List with a lightweight delegating class that prevents casting back to mutable type,
 * specializing for the case of the RandomAccess marker interface being retained if it was there originally
 */
fun <T> List<T>.asReadOnly(): List<T> {
    return this.whenNotAlreadyReadOnly {
        when (it) {
            is RandomAccess -> ReadOnlyRandomAccessList(it)
            else -> ReadOnlyList(it)
        }
    }
}

/**
 * Copies the List and then wraps with a lightweight delegating class that prevents casting back to mutable type,
 * specializing for the case of the RandomAccess marker interface being retained if it was there originally
 */
@Suppress("UNCHECKED_CAST")
fun <T> List<T>.toImmutable(): List<T> {
    val copy = when (this) {
        is RandomAccess -> ArrayList<T>(this)
        else -> this.toList()
    }
    return when (copy) {
        is RandomAccess ->  ReadOnlyRandomAccessList(copy)
        else -> ReadOnlyList(copy)
    }
}

您可以从该代码中看到想法并推断创建缺失的 classes,该代码重复其他引用类型的模式。或在此处查看完整代码:

https://github.com/kohesive/klutter/blob/master/core-jdk6/src/main/kotlin/uy/klutter/core/common/Immutable.kt

并且测试显示了一些以前允许修改但现在不允许修改的技巧,以及使用这些包装器的被阻止的转换和调用。

https://github.com/kohesive/klutter/blob/master/core-jdk6/src/test/kotlin/uy/klutter/core/collections/TestImmutable.kt