Scala不可变映射,何时可变?

时间:2012-04-13 13:23:23

标签: scala map immutability mutable use-case

我目前的用例非常简单,无论是可变的还是不可变的Map都可以解决问题。

有一个方法,它接受一个不可变的Map,然后调用第三方API方法,该方法也采用不可变的Map

def doFoo(foo: String = "default", params: Map[String, Any] = Map()) {

  val newMap = 
    if(someCondition) params + ("foo" -> foo) else params

  api.doSomething(newMap)
}

有问题的地图通常很小,最多可能有一个嵌入的案例类实例列表,最多几千个条目。因此,再次假设在这种情况下不可变的影响很小(即通过newMap val副本基本上有2个Map实例)。

尽管如此,它还是让我有点尴尬,复制地图只是为了获得一张新地图,上面贴了几个k-> v条目。

我可以为我想要处理的条目变为可变的params.put("bar", bar)等,然后params.toMap转换为api调用的不可变,这是一个选项。但后来我必须导入并传递可变映射,与使用Scala的默认不可变映射相比,这有点麻烦。

那么,对于在不可变地图上使用可变地图的合理/良好做法,一般指导原则是什么?

由于

修改 因此,看起来不可变地图上的添加操作接近恒定时间,确认@ dhg和@Nicolas断言没有制作完整副本,这解决了所呈现的具体案例的问题。

4 个答案:

答案 0 :(得分:35)

根据不可变的Map实现,添加一些条目可能实际上不会复制整个原始Map。这是不可变数据结构方法的优势之一:Scala将尽可能少地完成复制。

使用List最容易看到这种行为。如果我有val a = List(1,2,3),则该列表存储在内存中。但是,如果我添加了一个额外的元素,例如val b = 0 :: a,我会返回一个新的4元素List,但Scala却复制了原始列表a。相反,我们只创建了一个名为b的新链接,并为其指定了现有列表a

您也可以为其他类型的集合设想这样的策略。例如,如果我向Map添加一个元素,则集合可以简单地包装现有地图,在需要时回退到它,同时提供API就好像它是单个Map。 / p>

答案 1 :(得分:14)

使用可变对象本身并不坏,它在函数式编程环境中变得很糟糕,在这种环境中,你试图通过保持函数纯和对象不可变来避免副作用。

但是,如果在函数内部创建一个可变对象并修改此对象,如果您不在函数外部释放对该对象的引用,则该函数仍然是纯粹的。可以使用以下代码:

def buildVector( x: Double, y: Double, z: Double ): Vector[Double] = {
    val ary = Array.ofDim[Double]( 3 )
    ary( 0 ) = x
    ary( 1 ) = y
    ary( 2 ) = z
    ary.toVector
}

现在,我认为这种方法在两种情况下是有用/推荐的:(1)性能,如果创建和修改不可变对象是整个应用程序的瓶颈; (2)代码可读性,因为有时可以更容易地修改复杂的对象(而不是使用镜头,拉链等)。

答案 2 :(得分:5)

除了dhg的回答,您还可以查看performance of the scala collections。如果添加/删除操作不需要线性时间,则必须执行其他操作,而不仅仅是复制整个结构。 (注意反过来不是这样的:它不是因为复制整个结构需要线性时间)

答案 3 :(得分:0)

我喜欢使用collections.maps作为声明的参数类型(输入或返回值)而不是可变或不可变的映射。 Collections映射是不可变的接口,适用于两种类型的实现。使用地图的消费者方法实际上并不需要知道地图实现或其构造方式。 (无论如何,它真的没有任何业务)。

如果您采用从使用它的消费者隐藏地图的特定结构(可变或不可变)的方法,那么您仍然在下游获得基本上不可变的地图。通过使用collection.Map作为不可变接口,您可以完全删除所有" .toMap"消费者编写的使用immutable.Map类型对象的低效率。必须将完全构造的地图转换为另一个地图,只是为了遵守第一个不支持的接口,当你想到它时,这绝对是不必要的开销。

我怀疑在几年后我们将回顾三个独立的接口集(可变映射,不可变映射和集合映射),并意识到99%的时间只需要2个(可变和集合)并且使用(不幸的)默认的不可变映射接口确实为"可扩展语言"增加了许多不必要的开销。