1. 概述
在命令式编程语言中,我们通常使用 for 循环 和 while 循环 来遍历集合。而 Scala 引入了一种新的循环结构:for-comprehension(for 推导式)。
和其他许多 Scala 特性一样,for-comprehension 直接借鉴自 Haskell。它的用途远不止于遍历集合,它还能帮助我们以更函数式的方式简化语法复杂度。
在本篇文章中,我们将深入探讨 Scala 的 for-comprehension 结构。
2. Java 中的传统与声明式循环对比
为了更好地理解 Scala 的 for-comprehension,我们先快速回顾一下 Java 中的循环方式。
2.1. Java 8 之前
在 Scala 中,我们不太喜欢使用传统的循环结构。实际上,在 Scala 中几乎找不到带有副作用的 while 或 for 循环。
但在 Java 中,这样的代码却很常见:
final List<TestResult> results =
Arrays.asList(new TestResult("test 1",10, 10), new TestResult("test 2",2, 6));
int totalPassedAssertsInSucceededTests = 0;
for (int i = 0; i < results.size(); i++) {
final TestResult result = results.get(i);
if (result.isSucceeded()) {
totalPassedAssertsInSucceededTests += result.getSuccessfulAsserts();
}
}
✅ 在 Scala 中,这种风格是被不鼓励的,取而代之的是更声明式的写法,关注的是对集合的变换,而不是具体的循环过程。
2.2. Java 8 及之后
Java 8 引入了一些新的结构,让开发者可以写出更具声明式的代码。例如,lambda 表达式的引入允许我们将函数或过程应用到集合的每个元素上:
final long totalPassedAssertsInSucceededTests1 = results.stream()
.filter(TestResult::isSucceeded)
.mapToInt(TestResult::getSuccessfulAsserts)
.sum();
❌ 但问题在于,如果没有进一步的语法支持来组合多个迭代步骤,代码很快就会变得难以维护,哪怕可读性有所提升。很容易陷入如下嵌套地狱:
results.stream().flatMap(
res -> f(res).flatMap(
res1 -> res1.flatMap(
res2 -> res2.map( /* and so on */ ))));
那么,Scala 是如何优雅地解决这个问题的呢?
3. Scala 的声明式循环:For-Comprehension 结构
✅ for-comprehension 是 Scala 中以纯声明式风格处理集合的标准方式:
val listOfPassedAssertsInSucceededTests: List[Int] =
for {
result <- results
if result.succeeded
} yield (result.successfulAsserts)
val passedAssertsInSucceededTests: Int = listOfPassedAssertsInSucceededTests.sum
我们可以将 for-comprehension 分解为几个组成部分。虽然看起来有些复杂,但接下来我们会逐一解析。
Scala 中的 for-comprehension 格式为:for (enumerators) yield e
。其中,enumerators 是紧跟 for
关键字之后括号内的代码块,它们负责将值绑定到变量。而 e
是 for-comprehension 的主体,会对每个由 enumerators 生成的值进行计算,最终形成一个值的序列。
接下来我们将逐一分析这些组成部分。
4. 枚举器(Enumerators)
枚举器可以是 generator(生成器) 或 filter(过滤器)。在之前的例子中,我们同时使用了这两种枚举器。下面我们分别来看。
4.1. 生成器(Generators)
语句 result <- results
是一个生成器。它 **引入了一个新变量 result
**,该变量会依次绑定到 results
集合中的每一个元素。因此,result
的类型是 TestResult
。
✅ 我们可以有 任意数量的生成器,它们 彼此独立地循环,产生所有变量的组合。例如:
val executionTimeList = List(("test 1", 100), ("test 2", 230))
val numberOfAssertsWithExecutionTime: List[(String, Int, Int)] =
for {
result <- results
(id, time) <- executionTimeList
if result.id == id
} yield ((id, result.totalAsserts, time))
最终 numberOfAssertsWithExecutionTime
的值为:
List[("test 1", 10, 100), ("test 2", 6, 230)]
⚠️ for-comprehension 中的所有生成器必须作用于相同类型的容器。在上面的例子中,两个生成器都是 List
类型。类型参数可以不同,比如 List[TestResult]
和 List[(String, Int)]
是可以混用的。
4.2. 过滤器(Filters)
在 for-comprehension 内部,过滤器的形式是 if boolean-condition
。过滤器相当于一个守卫条件,它会过滤掉不符合条件的元素。
在过滤器中,我们可以使用 for-comprehension 作用域内的任意变量来构造布尔表达式。
例如:
val hugeNumberOfAssertsForATest: Int = 10
val resultsWithAHugeAmountOfAsserts: List[TestResult] =
for {
result <- results
if result.totalAsserts >= hugeNumberOfAssertsForATest
} yield (result)
5. For-Comprehension 主体(Body)
✅ 正如之前所说,for-comprehension 的主体会对每个由 enumerators 生成的值进行计算,并形成一个值的序列。在主体中,我们可以使用所有在作用域内的变量或值:
val magic: Int = 42
for {
res <- result
} yield res * magic
✅ yield 主体的返回类型可以是任意类型。到目前为止,我们的例子都返回了某种值。但也可以返回 Unit
,比如用于打印:
for {
res <- result
} println(s"The result is $res")
如果 yield 主体的返回类型是 Unit
,那么可以省略 yield
关键字。
6. For-Comprehension 深入解析
前面的例子展示了 for-comprehension 在语义上等价于对集合进行一系列操作。✅ 在 Scala 中,for-comprehension 实际上是以下方法调用的语法糖:
foreach
map
flatMap
withFilter
我们可以在任何定义了这些方法的类型上使用 for-comprehension。下面来看一个例子。
首先,我们定义一个简单的包装类:
case class Result[A](result: A)
尝试使用 for-comprehension 打印 Result
的值:
val result: Result[Int] = Result(42)
for {
res <- result
} println(res)
编译器会报错:
Value foreach is not a member of com.baeldung.scala.forcomprehension.ForComprehension.Result[Int].
✅ 这是因为编译器将 for-comprehension 翻译成了对 foreach
方法的调用。我们添加该方法:
def foreach(f: A => Unit): Unit = f(result)
接着,我们尝试使用 yield
:
for {
res <- result
} yield res * 2
编译器报错:
Value map is not a member of com.baeldung.scala.forcomprehension.ForComprehension.Result[Int].
✅ 编译器将其翻译为对 map
方法的调用,我们继续添加:
def map[B](f: A => B): Result[B] = Result(f(result))
如果我们想在 for-comprehension 中使用多个生成器:
val anotherResult: Result = 100
for {
res <- result
another <- anotherResult
} yield res + another
编译器提示缺少 flatMap
方法:
Value flatMap is not a member of com.baeldung.scala.forcomprehension.ForComprehension.Result[Int].
添加 flatMap
方法:
def flatMap[B](f: A => Result[B]): Result[B] = f(result)
其等价于:
result
.flatMap(res =>
anotherResult
.map(another => res + another)
)
最后,我们尝试在 for-comprehension 中添加过滤器:
for {
res <- result
if res == 10
} yield res
编译器提示缺少 withFilter
方法:
Value withFilter is not a member of com.baeldung.scala.forcomprehension.ForComprehension.Result[Int].
添加方法:
def withFilter(f: A => Boolean): Result[_] = if (f(result)) this else EmptyResult
⚠️ 注意:这里我们返回的是 EmptyResult
,它是 Result[Null]
的实例。
7. 总结
本文回顾了 Scala 中的 for-comprehension 结构。
我们比较了命令式与声明式循环风格的区别,然后分析了 for-comprehension 的结构,包括 枚举器 和 yield 主体 的使用。
✅ 最后我们发现,for-comprehension 实际上是 foreach
、map
、flatMap
和 withFilter
等方法调用的语法糖。
一如既往,所有代码示例都可以在 GitHub 上找到。