scala类库中的api陷阱(1): LinkedList.append

scala.collection.mutable.LinkedList 可变集合的 append 方法有个细节需要注意:

// 当list为空时
scala> val list = LinkedList[String]()

scala> val list2 = LinkedList[String]("ele")

scala> list.append(list2)
res6: scala.collection.mutable.LinkedList[String] = LinkedList(ele)

scala> list
res7: scala.collection.mutable.LinkedList[String] = LinkedList()

scala> list.size
res8: Int = 0

看到 list 本身并没有改变,仍为空,它的实现是判断自身为空时append方法直接返回要附加的对象,所以得这样用。

scala> val r = list.append(list2)
r: scala.collection.mutable.LinkedList[String] = LinkedList(ele)

scala> r(0)
res9: String = ele

当它不为空时,才会改变自身:

scala> val list = LinkedList[String]("1")

scala> list.append(list2)

scala> list.size
res11: Int = 2

它的文档里有讲到这点,但仍觉得很不合理,一是不符合大众的直觉,二是也并非所有的可变集合在为空时 append 操作也这样,比如ListBuffer里的append方法,在自身为空时仍会改变自身:

scala> val buf = ListBuffer[String]()
buf: scala.collection.mutable.ListBuffer[String] = ListBuffer()

scala> buf.append("A")

scala> buf
res4: scala.collection.mutable.ListBuffer[String] = ListBuffer(A)

scala> buf(0)
res5: String = A

scala雾中风景(9): List(1,2,3) == Seq(1,2,3) ?

惜朝在来往的扎堆里问:

scala> List(1, 1, 2) == Seq(1, 1, 2)
res219: Boolean = true

scala里Seq和List是一会儿事?

这个问题归根到底在于 == 在集合里是怎么实现的?在scala里==的语义等同于java里的equals,我们跟踪一下

val a = List(1,2,3)
val b = Seq(1,2,3)
a.equals(b)     // 设置断点

注意,在a.equals(b)出设置断点,scala-ide不一定能进入内部逻辑,你还是需要在它父类equals方法内设置断点才行。

上面的equals实际会到 GenSeqLike.equals,见下图:

它的逻辑是判断两个集合是否“可比较”(canEqual),如果可比较,则判断内部的元素是否相同:

override def equals(that: Any): Boolean = that match {
    case that: GenSeq[_] => (that canEqual this) && (this sameElements that)
    case _               => false
}   

于是我们推测 List(1,2,3)Seq(1,2,3)的容器类型应该是相同的类型或有继承关系,但是进入到canEqual逻辑内部,无法验证这个判断,它直接返回true,按理说应该对两个容器的类型进行比较一下才合适(看来只是给用户实现的集合类型实现equals时留了一个扩展点,scala自己的集合类型并不做类型判断)。

接下来进入 sameElements 逻辑,因为List混入了LinearSeqOptimized特质,这块的逻辑是在LinearSeqOptimized中的,见下图:

我们看到它通过模式匹配,要求目标集合也必须是LinearSeq类型。然后迭代并比较了两个容器内的各个元素是否相同,都相同的话就认为两个容器也相同。不过从这里的逻辑我们也可以判断出来,两个容器equals为true的话,并不一定需要是完全同样的类型或者有父子关系。我们验证一下:

1)两个集合分别是Seq特质与Set特质下的子类,是两种不一样的集合

scala> List(1,2,3) == Set(1,2,3)
res28: Boolean = false

2) 两个集合都是Set特质下的子类

scala> HashSet(1,2,3) == TreeSet(1,2,3)
res29: Boolean = true

3) 两个集合都是Seq特质下的子类

scala> ListBuffer(1,2) == LinkedList(1,2)
res20: Boolean = true

4) 两个集合都是Seq特质下的子类,不过QueueLinearSeq下的,而RangeIndexedSeq下的

scala> Queue(1,2) == Range(1,2)
res18: Boolean = false

5) 两个集合都是Seq特质下的子类,Seq(1,2,3)的实现是?

scala> Range(1,2,3) == Seq(1,2,3)
res12: Boolean = false

第1种情况好理解,ListSet 毕竟是另种含义不同集合,Set的实现也不会是LinearSeq特质的,所以返回false.第2种也容易理解,两个集合都是Set特质下的。

问题是3,4,5,为何Seq下会有多种情况,这还要我们再全面的看一下scala的集合框架,借用
这里的图片:

正是因为 Seq 特质下,又分为了IndexedSeqLinearSeq 两个分支,并且这两个特质中各自对 sameElements的逻辑有不同的实现,使得IndexedSeq的集合与LinearSeq下的集合比较时不可能相等。

另,对于 List(1,2,3)Seq(1,2,3)在构造集合的背后逻辑,可以参考这篇:通过List.apply方法构造List的背后逻辑

通过List.apply方法构造List的背后逻辑

通过List伴生对象的apply方法来创建实例: List("A","B") 过程发生了什么

首先,List伴生对象的apply方法接收的是一个可变参数列表,即数组:

override def apply[A](xs: A*): List[A] = xs.toList

而我们传入的Array("A","B")数组会被隐式转换为 WrappedArray 的子类型,这是在LowPriorityImplicits里定义的:

// Since the JVM thinks arrays are covariant, one 0-length Array[AnyRef]
// is as good as another for all T <: AnyRef.  Instead of creating 100,000,000
// unique ones by way of this implicit, let's share one.

implicit def wrapRefArray[T <: AnyRef](xs: Array[T]): WrappedArray[T] = {
    if (xs eq null) null
    else if (xs.length == 0) WrappedArray.empty[T]
    else new WrappedArray.ofRef[T](xs)
}

随后对这个WrappedArray 的子类型ofRef[String]类型,调用 toList 方法

不过在进行toList时用到了隐式参数CanBuildFrom,我们先看一下List伴生对象中定义的,用于生成CanBuildFrom信息的隐式方法:

/** $genericCanBuildFromInfo */
implicit def canBuildFrom[A]: CanBuildFrom[Coll, A, List[A]] =
    ReusableCBF.asInstanceOf[GenericCanBuildFrom[A]]

现在来追踪toList的执行过程,在父类TraversableOncetoList方法里调用了to方法,而这个to方法里有声明一个隐式参数。

用隐式参数CanBuildFrom构造了一个List类型的容器,把数据填充进去,再返回result

里面的隐式参数:

implicit cbf: CanBuildFrom[Nothing, A, Col[A @uV]] 

先不用管里面难懂的类型参数,编译在寻找对应的隐式参数值时,通过上面的 to[List]声明的目标类型是List,所以从List的伴生对象中去寻找,通过 canBuildFrom隐式函数得到了需要的参数,它是把一个可复用的对象造型成我们需要的CBF类型:

ReusableCBF.asInstanceOf[GenericCanBuildFrom[A]]

ReusableCBF的意思是可复用的CanBuildFrom,它在 GenTraversableFactory里定义:

通过这个CBF隐式参数帮我们构造了一个新的容器,然后把当前集合里的数据放进去,最后再调用新容器的result来得到List

通过断点,发现 b.result 时进入了 ListBuffer.toList 的代码里,也就是说这个隐式参数构造出来的新容器类型是 ListBuffer 的子类型。

最终,它返回ListBuffer类里的start成员,这个start是一个 :: 类型(List的子类)

scala雾中风景(8): 高阶函数与Unit的谜题

这个问题是在spray-routing的使用过程中发现的,又是一个“障眼法”问题。

简化一下,假定有下面的类型:

scala> class Request

scala> type Route = Request => Unit

Route是一个函数类型,然后我们定义一个接受Route类型的方法

scala> def hf(r:Route) { println("ok") }
hf: (r: Request => Unit)Unit

现在传递一个Route类型的实例,调用hf方法:

scala>  hf( req => {} )
ok

上面传递了一个 req => {} 的函数对象,运行没有问题。

我们再定义一个Route的生成器:

scala> def routeGenerator: Route = req => println("do nothing")
routeGenerator: Request => Unit

把这个生成器作为参数传递给 hf 方法:

scala> hf(routeGenerator)
ok

跟刚才没什么区别。

现在我们如果传入一个看上去像是高阶函数的函数: req => routeGenerator,hf方法还会接受吗?

scala> hf ( req => routeGenerator )
ok

是不是有点奇怪?我传入的不是 Request => Route 类型吗?展开的话应该是 Request => (Request => Unit) 为什么hf方法也能接受呢?

这里核心的问题就在于 req => routeGenerator 这个函数实例,究竟是什么类型?这与编译器的类型推导逻辑有关。

当我们定义一个变量,不声明类型,把上面的函数对象赋给这个变量:

scala> val param =  (req:Request) => routeGenerator
param: Request => (Request => Unit) = <function1>

变量param的类型是 Request => (Request => Unit) 类型,与我们预测的一致。

如果定义param时,指定它的类型是RouteRequest => Unit,上面的函数对象还可以赋值吗?

scala> val param: Route  =  (req:Request) => routeGenerator
param: Request => Unit = <function1>

这里困惑我们的是,为什么函数对象右边的routeGenerator的类型在这次的上下文中变成了Unit,而不是我们定义的Route类型了呢?

如果你对Unit类型真的了解(参考之前的这篇:scala雾中风景(4): Unit类型),这个时候就不会被迷惑了。

因为Unit类型自身的特点,在赋值时,可以把任意类型的表达式赋值给它:

scala> val tmp:Unit = "hello"
tmp: Unit = ()

因为表达式背后会被翻译为: { "hello", () },同理,在之前的上下文里,定义了 param 是一个 Reuqest => Unit
类型的变量,在赋值时,编译器就会把 req => routeGenerator 翻译成:

req => { routeGenerator; () } 

这个问题看上去像是个高阶函数的问题,实际与高阶函数没关系。至于Unit类型为何在给变量赋值时设计成这样,可能与函数式语言的历史上已经是这样设计了,scala很可能是从ML那块继承的这个设计。

或许我们可以把Unit类型在赋值时,理解成一个带有副作用的”过程”,这个过程接受无论什么类型的表达式,执行这些表达式,但最终返回的是()这个值。