将StringTokenizer转换为Scala迭代器

3

我正在尝试将java.util.StringTokenizer转换为Scala的Iterator,但以下方法失败:

def toIterator(st: StringTokenizer): Iterator[String] =
   Iterator.continually(st.nextToken()).takeWhile(_ => st.hasMoreTokens()))

但是这个有效:
def toIterator(st: StringTokenizer): Iterator[String] =
    Iterator.fill(st.countTokens())(st.nextToken())

你可以在Scala控制台中看到这个:

scala> Iterator("a b", "c d").map(new java.util.StringTokenizer(_)).flatMap(st => Iterator.continually(st.nextToken()).takeWhile(_ => st.hasMoreTokens())).toList
res1: List[String] = List(a, c)

scala> Iterator("a b", "c d").map(new java.util.StringTokenizer(_)).flatMap(st => Iterator.fill(st.countTokens())(st.nextToken())).toList
res2: List[String] = List(a, b, c, d)

如你所见,res1 是错误的,而 res2 是正确的。我做错了什么?第一种方法应该可行并且更好,因为它比第二种方法快2倍,因为它不会扫描字符串两次。

1个回答

4

takeWhile并不适合被用作有状态的函数。它应该接受一个纯函数作为参数,仅基于输入来决定是否继续。

具体来说,迭代器必须在takeWhile谓词被调用之前产生值。即使你的函数忽略了takeWhile参数,它仍然会被评估。因此,我们先调用nextToken,然后再检查更多的标记。

要精确地说明,在你的"a b"情况中,

  1. 首先,我们调用nextToken,这就是Iterator.continually所做的。存在下一个标记,因此返回"a"
  2. 现在,为了确定我们是否应该包括下一个标记,我们使用"a"作为参数调用你的谓词。你的谓词忽略了"a"并调用了hasMoreTokens。我们的分词器还有更多的标记(即"b"),因此返回true。继续。
  3. 现在我们再次调用nextToken。这返回"b"
  4. 我们需要确定是否应该在结果中包含它,因此我们的takeWhile谓词使用"b"作为参数运行。我们的takeWhile谓词忽略了其参数并调用了hasMoreTokens。我们没有更多的标记了,因此返回false。我们不应该包含这个元素。
  5. takeWhile返回false,因此我们停在最后一个返回true的元素上。我们得到的结果列表是List("a")

由于滥用像takeWhile这样的纯函数技术来构建有状态的函数,导致我们得到了令人难以理解的结果。

虽然一行代码的解决方案看起来很聪明,但你拥有的是一个有状态的命令式对象,你想要将其适应Iterator接口。在一堆纯函数调用中隐藏这种状态性不是一个好主意,所以我们应该编写自己的Iterator子类并恰当地实现它。

import java.util.StringTokenizer

final class StringTokenizerIterator(
  private val tokenizer: StringTokenizer
) extends Iterator[String] {

  def hasNext: Boolean = tokenizer.hasMoreTokens

  def next(): String = tokenizer.nextToken()

}

object Example {

  def toIterator(st: StringTokenizer): Iterator[String] =
    new StringTokenizerIterator(st)

  def main(args: Array[String]) = {
    println(Iterator("a b", "c d")
            .map(new java.util.StringTokenizer(_))
            .flatMap(toIterator(_))
            .toList)
  }

}

我们正在执行您过去所做的工作,调用适当的StringTokenizer函数,但我们将其封装在一个完整的类中以封装状态,而不是假装状态部分不存在。它的代码长度确实更长,但这也是应该的。我们不希望其中混乱的部分被忽略。

谢谢你的回答。封装方法确实更好,现在我明白了 takeWhile(f) 方法的问题所在。它本质上像是一个 do yield next() while f 而不是 while(f) yield x - pathikrit
@pathikrit 有点像,更确切地说是(抱歉,无法在评论中换行)do tmp = next(); break unless f(tmp); yield tmp while truebreak 在从内部迭代器获取下一个元素之后但在从外部迭代器中产生它之前发生。 - Silvio Mayolo
在迭代器中添加一个 spantakeUntil 工具会很不错,它可以在这种情况下工作(再次强调,封装更好,但只是作为一种练习)。 - pathikrit

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接