【Scala原理系列】scala中的for循环特性用法示例权威详解

Scala中的for循环特性用法详解


在引入Lambda表达式之前,for循环可能是Java中最常用的迭代结构。通过这个循环,我们可以完成各种操作 - 从简单的映射到更复杂的“查找集合中的第一个元素”。在Scala中,我们可以使用monads来完成这些操作,尽管如此,for循环是一种提供了许多可能性的特性。

本文讨论了Scala中的for循环。
第一部分介绍了语法和一些通用信息。
第二部分列出了我们可以使用该循环做什么,并给出了每个点的示例。
最后一部分提到了可以替代for循环的递归。

一、原理和构成

Scala中的for循环是一种用于遍历集合、迭代元素和执行循环操作的控制结构。它基于函数式编程的思想,提供了一种简洁、灵活和表达力强的方式来处理集合数据。

Scala的for循环由以下几个部分组成:

  1. 关键字:for关键字用于引导for循环的开始。

  2. 生成器(Generators):生成器是用于生成迭代序列的表达式。它们以"<-"符号连接一个变量和一个可迭代的集合,如数组、列表、范围等。每次迭代时,生成器会将集合中的一个元素绑定到变量上。例如:

val numbers = List(1, 2, 3, 4, 5)
for (n <- numbers) {
  println(n)
}

在这个例子中,生成器"n <- numbers"将列表中的每个元素绑定到变量n上。

  1. 过滤器(Filters):过滤器用于根据特定的条件过滤生成器生成的元素。它们以"if"关键字开头,后面跟着一个布尔表达式。只有满足该表达式的元素才会被包含在循环中。例如:
val numbers = List(1, 2, 3, 4, 5)
for (n <- numbers if n % 2 == 0) {
  println(n)
}

在这个例子中,过滤器"if n % 2 == 0"只包含偶数。

  1. 嵌套循环(Nested Loops):Scala的for循环支持嵌套,可以在循环内部添加更多的生成器和过滤器。这样可以遍历多个集合,并根据需要进行条件筛选。例如:
val numbers = List(1, 2, 3)
val letters = List('a', 'b', 'c')
for (n <- numbers; l <- letters) {
  println(n, l)
}

在这个例子中,我们使用两个生成器遍历了两个列表,并打印每个数字和字母的组合。

总之,Scala的for循环是一种基于函数式编程的迭代控制结构,它通过生成器、过滤器和嵌套循环等组件提供了一种简洁、灵活和可读性高的方式来处理集合数据。它使得代码更具表达力和可维护性,并且在处理复杂的集合操作时非常有用。

现在需要注意的一个重要点是,Scala中没有原生支持break关键字,而其他语言(如Java)中可以用于停止迭代。另一个不受支持的操作是使用continue关键字来跳过循环执行的特定条件。

二、特性

在Scala中,for循环的特性包括:

1.综合

与Python类似,Scala提供了一种将序列转换为另一种类型的轻量级语法。这个符号是由一个for循环后跟一个yield关键字和要应用的转换组成:

it should "use comprehension as map function alternative" in {
  case class Number(value: Int)

  var generationStartTime: Option[Long] = None
  val wrappedNumbers = for (nr <- 1 to 10) yield {
    if (generationStartTime.isEmpty) generationStartTime = Some(System.currentTimeMillis())
    Number(nr)
  }

  // 请注意,`yield`不会引入任何关于执行的惰性 - 将其视为map的替代方案有助于避免混淆。但为了证明这一点,我们比较了第一次生成的时间,如果生成是惰性的,它将为空
  generationStartTime shouldBe defined
  wrappedNumbers should have size 10
  wrappedNumbers should contain allOf(Number(1), Number(2), Number(3), Number(4), Number(5), Number(6),
    Number(7), Number(8), Number(9), Number(10))
}

2. 一次迭代中的两个循环

嵌套的for循环,在Java中称为for (…) { for (…) { // … }。在Scala中,可以更简洁地编写它们,因为我们可以将它们写成for (firstIteration; secondIteration,...)

it should "iterate over 2 lists" in {
  val upperCasedLetters = Seq("A", "B")
  val lowerCasedLetters = Seq("a", "b", "c")

  val constructedPairs = new mutable.ListBuffer[String]()
  // 如您所见,这样写比使用两个不同的for循环更简洁
  for (upperCased <- upperCasedLetters; lowerCased <- lowerCasedLetters) {
    constructedPairs.append(s"${upperCased}-${lowerCased}")
  }

  constructedPairs should have size 6
  constructedPairs should contain allOf("A-a", "A-b", "A-c", "B-a", "B-b", "B-c")
}

3. 守卫

就像Scala模式匹配一样,守卫用于从迭代中排除某些值,就像过滤器一样:

it should "use guard to eliminate not desired objects" in {
  // 这里我们模拟了filter-map操作,并借助yield和guard来完成
  val upperCasedLetters = Seq("A", "B")
  val lowerCasedLetters = Seq("a", "b", "c")

  // 守卫既模拟了filter(...)又模拟了其他语言中使用的`continue`行为,用于跳过特定条件的执行
  val onlyMatchingPairs =
    for (upperCased <- upperCasedLetters; lowerCased <- lowerCasedLetters if lowerCased.toUpperCase == upperCased) yield {
      s"${upperCased}-${lowerCased}"
    }

  onlyMatchingPairs should have size 2
  onlyMatchingPairs should contain allOf("A-a", "B-b")
}

4. 生成器定义过滤器模式

使用for循环,我们还可以模拟filter-map操作的行为。通过使用守卫并定义我们要处理的值,我们可以实现这一点:

it should "use for variables inside the expression" in {
  case class Student(name: String, note: Int)
  val students = Seq(Student("student#1", 3), Student("student#2", 4), Student("student#3", 2),
    Student("student#4", 5))
  // 在这里,for循环与“生成器定义过滤器”模式一起使用

  val notes = for (
    student <- students; // 生成器
    note = student.note // 定义
    if note > 2 // 过滤器
  ) yield {
    s"Note=${note}"
  }

  notes should have size 3
  notes should contain allOf("Note=3", "Note=4", "Note=5")
}

5. 反向迭代

可以使用简单的for (item <- mySequence.reverse)实现反向迭代,但也有一种类似于基于索引的for循环的方式。它使用to(end: Int, step: Int)方法,在每次迭代中将Range的数字增加step值。显然,如果该值是负数,则数字将减少:

it should "iterate from the end" in {
  val numbers = 1 to 10

  // 如果我们想保留foreach的态度,我们可以使用`.reverse for (i <- numbers.reverse)`
  // 但这是使用索引的集合的另一种方法
  val numbersFromTheEnd = new mutable.ListBuffer[Int]()
  for (index <- numbers.size-1 to (0, -1)) {
    val number = numbers(index)
    numbersFromTheEnd.append(number)
  }

  numbersFromTheEnd should have size 10
  numbersFromTheEnd should contain allElementsOf(numbers.reverse)
}

6. “跳过”迭代

借助scala.collection.immutable.Range#by(step: Int)方法,我们可以很容易地迭代每个step元素,如下例所示:

it should "iterate every 2 element" in {
  val numbers = 1 to 10

  val oddNumbers = new mutable.ListBuffer[Int]()
  for (index <- 0 to numbers.size-1 by 2) {
    oddNumbers.append(numbers(index))
  }

  oddNumbers should have size 5
  oddNumbers should contain allOf(1, 3, 5, 7, 9)
}

7. 基于布尔标志的break

如前所述,Scala的for循环不提供break和continue关键字。但是,当满足停止条件后,仍然可以停止循环执行。一个有效的解决方案是使用布尔标志:

it should "stop the execution after finding the first matching item thanks to a boolean flag" in {
  val numbers = 1 to 10

  var wasFound = false
  var numberOfExecutions = 0
  for (number <- numbers if !wasFound) {
    if (number == 5) {
      wasFound = true
    }
    numberOfExecutions += 1
  }

  numberOfExecutions shouldBe 5
  wasFound shouldBe true
}

但是请注意,仍然可以使用Java的break模式。不同之处在于,在Scala中,break不是关键字,而是scala.util.control.Breaks中的一个方法:

it should "stop the execution with Breakable" in {
  val numbers = 1 to 10
  var wasFound = false
  // 这只是为了说明 - 使用布尔标志的方法似乎比这种方法更优雅
  import scala.util.control.Breaks._
  breakable {
    for (number <- numbers) {
      if (number == 5) {
        wasFound = true
        break()
      } // 加上()以避免与break关键字混淆
    }
  }

  wasFound shouldBe true
}

三、for循环和递归

许多程序员熟悉for循环。然而,函数式程序员(或至少其中一部分)认为使用for循环在功能上不友好。它引入了可变性。因此,被投票替代它们的概念是递归函数。毕竟,它们纯粹地使用函数,而在这种范式中,函数是一等公民。

但是递归也带来了一些危险点。第一个可能是最关键的是堆栈大小。每个方法都分配了一个新的堆栈帧,如果调用次数增加,就可能遇到堆栈溢出异常。幸运的是,为了减轻这个问题,Scala提出了@tailrec注释。使用该注释进行递归的方法在编译时转换为循环。通过这样做,我们可以保持函数式风格,避免堆栈帧问题。

@tailrec是一个约束。这意味着如果一个函数无法转换,则会产生编译错误。

递归也用于Scala库类。例如,Stream的foreach方法就是这样实现的:

@tailrec
override final def foreach[U](f: A => U) {
  if (!this.isEmpty) {
    f(head)
    tail.foreach(f)
  }
}

将for循环转化为递归函数调用非常简单:

"recursion" should "be able to replace for loop" in {
  // 我们可以使用递归调用来模拟for循环的行为
  @tailrec
  def findFirst(toFind: Int, items: Seq[Int]): Option[Int] = {
    val currentNumber = items.head
    if (currentNumber == toFind) {
      Some(currentNumber)
    } else if (items.size == 1) {
      None
    } else {
      findFirst(toFind, items.slice(1, items.size))
    }
  }

  val number5 = findFirst(5, 1 to 10)

  number5 shouldBe defined
  number5.get shouldEqual 5
  val number101 = findFirst(101, 1 to 10)
  number101 shouldBe empty
}

Scala的for循环,就像整个语言一样,很好地融合了过程式和函数式编写风格。如第一部分所示,该循环与foreach循环非常相似,其中从集合中急切地读取一个项。这种急切性在第二部分中涵盖的综合中没有改变,它更像是map的一种替代方案。但对于纯粹的函数式编程的坚定主义者来说,for循环并不完全好,因为它们引入了可变性。这就是为什么它们可以被递归替代 - 即使在幕后,它被转换成带有@tailrec注释的循环。这并不意味着for循环不好或丑陋。它们的使用在很大程度上取决于项目的编码风格。毕竟,在判断中应该有一个实用主义的方法。

四、参考链接

  • 24
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BigDataMLApplication

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值