在Scala中使用索引进行有效的迭代


83

由于Scala没有for带有索引的旧Java样式循环,

// does not work
val xs = Array("first", "second", "third")
for (i=0; i<xs.length; i++) {
  println("String #" + i + " is " + xs(i))
}

如何在不使用var的情况下有效地进行迭代?

你可以这样做

val xs = Array("first", "second", "third")
val indexed = xs zipWithIndex
for (x <- indexed) println("String #" + x._2 + " is " + x._1)

但是列表遍历了两次-效率不高。


这些都是很好的回应。我从Java“ for”循环中缺少的是具有多个初始化程序的能力,以及使用不仅仅是增量/减量的“迭代”功能。这是Java比Scala更为简洁的一个例子。
迅速

...“ iteite”使用的不仅仅是增量/减量...在scala中,可以用step进行迭代,也可以使用循环头中的“ if”条件进行迭代。还是您在寻找其他东西?
om-nom-nom

1
/ * Java * / for(int i = 0,j = 0; i + j <100; i + = j * 2,j + = i + 2){...}如何在Scala的1行中做到这一点?
活泼

3
@snappy:在我看来,对Scala的最自然的翻译是一个while循环。我记得,几年前就存在一个争论,Scala是否应该继承Java的for(;;)循环,因此决定,好处不足以证明增加的复杂性。
基普顿·巴罗斯

Answers:


130

它比遍历两次要差得多,它创建了一个中间的数组对。您可以使用view。当您这样做时collection.view,您可以将后续调用视为在迭代过程中的延迟行为。如果您想找回一个适当的,完全实现的集合,请force在最后致电。在这里这将是无用且昂贵的。因此,将您的代码更改为

for((x,i) <- xs.view.zipWithIndex) println("String #" + i + " is " + x)

6
好主意,只有一个遍历,但即使没有创建新的集合,它也会创建n对。
迅速

2
完全正确。好吧,也许有人含糊地希望JVM可以优化那些创建,但是我不会指望这一点。我没有看到一种解决方案,该解决方案不会基于对索引的迭代。
Didier Dupont

1
@snappy这个应该已经被选为答案!在大多数其他答案中都建议按索引访问元素,这违反了Scala的功能特性,并且对链接列表(例如List,Scala中最常用的集合)-不仅对它们,还表现不佳。在这里查看apply操作。在类似链表的集合中,每次通过索引访问元素都会导致遍历列表。
Nikita Volkov

此处显示了完全不同的方法:stackoverflow.com/questions/6821194/…–
Neil

为什么这样有效?它正在创建一个新的数组对象,并使用了一个附加函数(“ view”),所以我很难理解为什么这对开发人员和机器都有效,除了感觉很惯用。
matanster '16

69

已经提到,Scala确实具有for循环语法:

for (i <- 0 until xs.length) ...

或简单地

for (i <- xs.indices) ...

但是,您还要求提高效率。事实证明,Scala的for语法实际上是更高阶的方法,如语法糖mapforeach等。因此,在某些情况下,这些循环可以是低效的,例如如何优化,内涵和循环在Scala呢?

(好消息是Scala团队正在努力改善这一点。这是Bug跟踪程序中的问题:https : //issues.scala-lang.org/browse/SI-4633

为了获得最大的效率,可以使用while循环,或者,如果您坚持要删除var尾部递归,请使用循环:

import scala.annotation.tailrec

@tailrec def printArray(i: Int, xs: Array[String]) {
  if (i < xs.length) {
    println("String #" + i + " is " + xs(i))
    printArray(i+1, xs)
  }
}
printArray(0, Array("first", "second", "third"))

注意,可选 @tailrec注释对于确保该方法实际上是尾递归很有用。Scala编译器将尾递归调用转换为与while循环等效的字节码。


+1代表我提到的索引方法/函数,因为它实际上消除了整套的一对一的编程错误,因此我认为它更可取。
chaotic3quilibrium 2011年

1
在这里必须注意,如果xs是任何类型的链表(例如,广泛使用的List),则按索引一样访问其元素xs(i)将是线性的,因此,其for (i <- xs.indices) println(i + " : " + xs(i))执行方式甚至for((x, i) <- xs.zipWithIndex) println(i + " : " + x)会比甚至更差,因为它将导致的结果远不止于此引擎盖下的两个遍历。因此,@ didierd建议使用视图的答案应该被认为是最通用和最惯用的一种观点,即IMO。
Nikita Volkov

1
如果需要最大效率(例如,在数值计算中),索引数组比遍历链表更快。链表的节点是分别分配堆的,并且在不同的内存位置之间跳转并不适合CPU缓存。如果使用a view,那么即使是如此高的抽象水平也将对堆和GC施加更大的压力。根据我的经验,通过避免在数字代码中分配堆,通常可以获得10倍的性能。
基普顿·巴罗斯

20

另一种方式:

scala> val xs = Array("first", "second", "third")
xs: Array[java.lang.String] = Array(first, second, third)

scala> for (i <- xs.indices)
     |   println(i + ": " + xs(i))
0: first
1: second
2: third

5
我真的很喜欢您指出索引的方法/功能。它降低了复杂性,并且实际上消除了一整套“一举一动”的错误,这是所有软件工程中最常见的编程错误/错误。
chaotic3quilibrium 2011年

14

实际上,scala具有索引的旧Java样式循环:

scala> val xs = Array("first","second","third")
xs: Array[java.lang.String] = Array(first, second, third)

scala> for (i <- 0 until xs.length)
     | println("String # " + i + " is "+ xs(i))

String # 0 is first
String # 1 is second
String # 2 is third

其中0 until xs.length或者0.until(xs.length)是一个RichInt返回方法Range适于循环。

另外,您可以尝试使用循环to

scala> for (i <- 0 to xs.length-1)
     | println("String # " + i + " is "+ xs(i))
String # 0 is first
String # 1 is second
String # 2 is third

5
xs(i)在列表上将复杂度提高到O(n ^ 2)
Vadzim

@Vadzim是的,但是在Java中,您也可以通过LinkedList在索引上使用for循环
francoisr 2015年

1
对于数组上的xs(i),上面的代码是O(n),对吗?由于scala中的数组提供了几乎恒定的时间随机访问?
dhfromkorea

2
@dhfromkorea是的,对于数组应该更快(实际上是O(n))
om-nom-nom


4

在scala中循环非常简单。创建您选择的任意阵列。

val myArray = new Array[String](3)
myArray(0)="0";
myArray(1)="1";
myArray(2)="2";

循环类型

for(data <- myArray)println(data)

for (i <- 0 until myArray.size)
println(i + ": " + myArray(i))

4

实际上,调用zipWithIndex一个集合将遍历它,并且还会为这对创建一个新的集合。为避免这种情况,您可以仅调用zipWithIndex集合的迭代器。这将仅返回一个新的迭代器,该迭代器在迭代时跟踪索引,因此无需创建额外的集合或额外的遍历。

这是scala.collection.Iterator.zipWithIndex当前在2.10.3中实现的方式:

  def zipWithIndex: Iterator[(A, Int)] = new AbstractIterator[(A, Int)] {
    var idx = 0
    def hasNext = self.hasNext
    def next = {
      val ret = (self.next, idx)
      idx += 1
      ret
    }
  }

这甚至比在集合上创建视图更有效率。


3

在不创建元组垃圾的情况下,stdlib中没有什么可以为您完成的,但是编写自己的并不难。不幸的是,我从来没有费心去弄清楚如何做适当的CanBuildFrom隐式雨舞,以使其在应用的集合类型中通用,但是,如果可能的话,我敢肯定有人会启发我们的。:)

def foreachWithIndex[A](as: Traversable[A])(f: (Int,A) => Unit) {
  var i = 0
  for (a <- as) {
    f(i, a)
    i += 1
  }
}

def mapWithIndex[A,B](in: List[A])(f: (Int,A) => B): List[B] = {
  def mapWithIndex0(in: List[A], gotSoFar: List[B], i: Int): List[B] = {
    in match {
      case Nil         => gotSoFar.reverse
      case one :: more => mapWithIndex0(more, f(i, one) :: gotSoFar, i+1)
    }
  }
  mapWithIndex0(in, Nil, 0)
}

// Tests....

@Test
def testForeachWithIndex() {
  var out = List[Int]()
  ScalaUtils.foreachWithIndex(List(1,2,3,4)) { (i, num) =>
    out :+= i * num
  }
  assertEquals(List(0,2,6,12),out)
}

@Test
def testMapWithIndex() {
  val out = ScalaUtils.mapWithIndex(List(4,3,2,1)) { (i, num) =>
    i * num
  }

  assertEquals(List(0,3,4,3),out)
}

将其添加到标准库中肯定是有意义的。
活泼的

1
我不太确定,因为如果您想遵守通常的foreach / map API,无论如何您都会遇到元组。
亚历克斯克鲁斯

3

其他一些迭代方法:

scala>  xs.foreach (println) 
first
second
third

foreach以及类似的map,它将返回一些内容(该函数的结果,对于println来说,是Unit,因此是Units的列表)

scala> val lens = for (x <- xs) yield (x.length) 
lens: Array[Int] = Array(5, 6, 5)

使用元素,而不是索引

scala> ("" /: xs) (_ + _) 
res21: java.lang.String = firstsecondthird

可折

for(int i=0, j=0; i+j<100; i+=j*2, j+=i+2) {...}

可以通过递归来完成:

def ijIter (i: Int = 0, j: Int = 0, carry: Int = 0) : Int =
  if (i + j >= 100) carry else 
    ijIter (i+2*j, j+i+2, carry / 3 + 2 * i - 4 * j + 10) 

进位部分只是一个示例,可以对i和j进行操作。不必是Int。

对于更简单的东西,更接近通常的for循环:

scala> (1 until 4)
res43: scala.collection.immutable.Range with scala.collection.immutable.Range.ByOne = Range(1, 2, 3)

scala> (0 to 8 by 2)   
res44: scala.collection.immutable.Range = Range(0, 2, 4, 6, 8)

scala> (26 to 13 by -3)
res45: scala.collection.immutable.Range = Range(26, 23, 20, 17, 14)

或不订购:

List (1, 3, 2, 5, 9, 7).foreach (print) 

3

我有以下方法

object HelloV2 {

   def main(args: Array[String]) {

     //Efficient iteration with index in Scala

     //Approach #1
     var msg = "";

     for (i <- args.indices)
     {
       msg+=(args(i));
     }
     var msg1="";

     //Approach #2
     for (i <- 0 until args.length) 
     {
       msg1 += (args(i));
     }

     //Approach #3
     var msg3=""
     args.foreach{
       arg =>
        msg3 += (arg)
     }


      println("msg= " + msg);

      println("msg1= " + msg1);

      println("msg3= " + msg3);

   }
}

2

一个简单而有效的方式,从实施的启发transformSeqLike.scala

    var i = 0
    xs foreach { el =>
      println("String #" + i + " is " + xs(i))
      i += 1
    }

0

所提出的解决方案遭受以下事实的困扰:它们要么明确地迭代集合,要么将集合填充到函数中。坚持使用Scala的惯用法,然后将索引放入惯常的map或foreach方法中,这是更自然的选择。这可以通过记忆来完成。结果代码可能看起来像

myIterable map (doIndexed(someFunction))

这是一种实现此目的的方法。考虑以下实用程序:

object TraversableUtil {
    class IndexMemoizingFunction[A, B](f: (Int, A) => B) extends Function1[A, B] {
        private var index = 0
        override def apply(a: A): B = {
            val ret = f(index, a)
            index += 1
            ret
        }
    }

    def doIndexed[A, B](f: (Int, A) => B): A => B = {
        new IndexMemoizingFunction(f)
    }
}

这已经是您所需要的。您可以将其应用如下:

import TraversableUtil._
List('a','b','c').map(doIndexed((i, char) => char + i))

结果在列表中

List(97, 99, 101)

这样,您可以使用通常的Traversable函数,而以包装有效函数为代价。请享用!

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.