前目的地

这个故事非常有想象力,看到一半的时候我想起来曾在某个论坛看到过这个关于时空穿越悖论的故事,后来去google了一下,原来改编自《All You Zombies》。的确很有意思,科幻迷们值得一看。

一次编码问题的排查

上周遇到的一个问题,代码里有通过scala.io.Source.fromFile 方式读取文件内容,并且没有指定编码格式,开始程序运行是ok的,后来添加了中文,发现在某些情况下会遇到乱码问题,并不是必然发生的,而是不同的人ssh登录到linux的测试环境启动应用可能会有不同的结果。这里记录一下当时的情况。

问题的排查,首先确认Source.fromFile这个api里面对编码的使用方式:

def fromFile(name: String)(implicit codec: Codec): BufferedSource =
    fromFile(new JFile(name))(codec)

不指定编码的话,系统使用了一个隐式参数作为默认编码,看一下它是什么:

scala>  def f(implicit c: io.Codec) = c
f: (implicit c: scala.io.Codec)scala.io.Codec

scala> f.name
res6: String = US-ASCII

这台linux上显示的是US-ASCII,而在我的mac上是UTF-8,看来是编码设置的问题,这个值应该是jvm的系统属性里编码相关的:

scala> System.getProperties.foreach(s => if (s.toString.indexOf("enc") != -1) println(s) )
(file.encoding.pkg,sun.io)
(sun.jnu.encoding,ANSI_X3.4-1968)
(file.encoding,ANSI_X3.4-1968)
(sun.io.unicode.encoding,UnicodeLittle)

测试了一下是file.encoding这个系统属性,修改这个属性为UTF-8后确实可以正常的。但是另一个同事登录这台linux后运行却跟我的情况不同,他登录后(使用同一账号)直接jvm的file.encoding默认就是UTF-8,这看起来有些奇怪。

jvm的系统属性,其实也是根据OS的环境变量得到的。虽然这台机器的LANGLC_*等指定了UTF-8,但并不意味jvm的file.encoding也一样:

$ locale
LANG=en_US.UTF-8
LC_CTYPE=UTF-8
LC_NUMERIC="en_US.UTF-8"
LC_TIME="en_US.UTF-8"
LC_COLLATE="en_US.UTF-8"
LC_MONETARY="en_US.UTF-8"
LC_MESSAGES="en_US.UTF-8"
LC_PAPER="en_US.UTF-8"
LC_NAME="en_US.UTF-8"
LC_ADDRESS="en_US.UTF-8"
LC_TELEPHONE="en_US.UTF-8"
LC_MEASUREMENT="en_US.UTF-8"
LC_IDENTIFICATION="en_US.UTF-8"
LC_ALL=

要确认file.encoding属性的话,可以使用locale charmap命令,我指定的结果显示:

$ locale charmap
ANSI_X3.4-1968

ANSI_X3.4-1968是系统默认的编码,而另一个同事显示的是”UTF-8″,又把怀疑落在ssh客户端上。我是在mac下直接通过ssh命令的方式登录,而他则是在windows下使用putty登录。各自打开ssh的 -verbose参数:

$ ssh -v xxx@ip

他的展示信息里出现了:

debug1: Sending env LC_ALL = en_US.UTF-8 
debug1: Sending env LANG = zh_CN.UTF-8 

而我的ssh客户端则没有这些信息,确实是ssh客户端配置所引起的差异。(关于LC_*LC_ALL等环境变量的关系后续有精力再整理)

scala雾中风景(20): MutableList迭代器的bug

上一篇,这个问题还没完,泽斌给出这个问题是因另一个问题引发的:Scala 的 for each,为什么会有且仅有一次 recursion? 引用原问题:

Scala 2.10/2.11 中,运行

val l = MutableList(16)

for (i<-l) {
    val p = i/2
    if (!(l contains p)) { 
        l += p 
    }
}

l

得到的是

MutableList[Int](16, 8, 4)

如果是返回 (16, 8)(如同 JavaScript 或 PHP)的话我能理解。或者是返回 (16, 8, 4, 2, 1, 0)(如同 Python 或 Ruby) 我也能够理解。但是返回 (16, 8, 4) 是为什么呢?

这得看看MutableList的迭代器实现了, 它在scala.collection.LinearSeqOptimized里面:

override /*IterableLike*/
def foreach[B](f: A => B) {
    var these = this
    while (!these.isEmpty) {
      f(these.head)
      these = these.tail
    }
}

不太容易直接看出来这块的问题,断点的时候发现这里会导致调试器在展示值时调用toString引起异常,我是复制了一份MutableList的代码在foreach里增加一些打印信息发现的。

我们在repl下模拟这个迭代过程,刚开始foreach的时候元素只有1个,对16进行处理,追加一个新元素8:

scala> val l = scala.collection.mutable.MutableList(16)
l: scala.collection.mutable.MutableList[Int] = MutableList(16)

scala> l += 8
res0: l.type = MutableList(16, 8)

这个追加的行为对应foreach里面的f(these.head)这一句,之后把指针指向后续链表节点:

scala> var these = l.tail
these: scala.collection.mutable.MutableList[Int] = MutableList(8)

因为刚开始var these=this,所以这次these=these.tail会得到链表的第二个元素8,然后传入8,原链表又被追加了一个元素4:

scala> l += 4
res1: l.type = MutableList(16, 8, 4)

接着继续移动指针,但这次these对象已经不是this而是单节点链表,取它的tail会得到空,导致foreach终止。所以最终原链表里有3个元素:

scala> these = these.tail
these: scala.collection.mutable.MutableList[Int] = MutableList()

虽然,这里的these与原链表是复用的,它size虽然为0,但实际可以访问后续元素:

scala> these(0)
res3: Int = 4

我觉得应该算是MutableList迭代器的bug了,它不应该使用LinearSeqOptimized里的实现,那个里面的逻辑没有考虑到集合长度可变的情况。

scala雾中风景(19): MutableList与mutable.LinkedList的问题

群里看到的问题:

scala> import scala.collection._

scala> val l = mutable.MutableList(1,2)
l: scala.collection.mutable.MutableList[Int] = MutableList(1, 2)

scala> val t = l.tail
t: scala.collection.mutable.MutableList[Int] = MutableList(2)

scala>  l += 3
res0: l.type = MutableList(1, 2, 3)

scala> t.length
res1: Int = 1

这里显示 t 的长度没有问题,但显示t的值的时候,却不是预期的:

scala> t
res2: scala.collection.mutable.MutableList[Int] = MutableList(2, 3)

怎么把l列表后来增加的3也给包含进去了。这要看这个MutableListtail方法的实现是怎么回事了。

scala.collection.mutable.MutableList里为了实现scala.collection.GenTraversableLike特质里的head, tail等方法,是通过链表scala.collection.mutable.LinkedList来存储数据。它的tail方法实现如下:

override def tail: MutableList[A] = {
    val tl = new MutableList[A]
    tailImpl(tl)
    tl
}

// this method must be private for binary compatibility
private final def tailImpl(tl: MutableList[A]) {
    require(nonEmpty, "tail of empty list")
    tl.first0 = first0.tail
    tl.len = len - 1
    tl.last0 = if (tl.len == 0) tl.first0 else last0
}

这里面要看看 tl.first0 = first0.tail 的实现是什么,在LinkedListLike里:

override def tail: This = {
    require(nonEmpty, "tail of empty list")
    next
}

它直接返回当前链表的下一个节点引用。所以当原链表末尾附加新的值时,这个 tl 链表也是可以引用到的,因为它们是复用的。

不过因为对这个tl的len设置过了,所以迭代的时候,并不会越界。并且toString方法也不会多展示不属于它的数据的:

scala> t.toString
res14: String = MutableList(2)

scala> t.foreach(print)
2

但在repl的展示上,就没有做的很严谨了,它没有调用这个对象的toString来展示它的值,而是自己把这个节点及链表后续节点的值都展示了:

scala> t
res15: scala.collection.mutable.MutableList[Int] = MutableList(2, 3)

这应该算是repl展示层面的不严谨吧。不过MutableList的实现也不太严谨,这个t虽然length是1,但后边节点如果有值的话也是可以索引到的:

scala> t(0)
res17: Int = 2

scala> t(1)
res18: Int = 3

scala> l += 4
res19: l.type = MutableList(1, 2, 3, 4)

scala> t(2)
res20: Int = 4

这也算是一个陷阱吧,mutable的集合在使用的时候要清楚它的场景和限制。