scala中for推导式详细讲解
一、scala-for推导式概念解读
想象一下:你要做一道新菜
普通的 for
循环,就像一个只会严格执行命令的机器人:
“第一步:拿一个苹果。”
“第二步:切开。”
“第三步:装盘。”
…
它只是按部就班地做事。
而 for
推导式,就像一本**“智能菜谱”,它不是告诉你一步步做什么,而是描述你最终想要一个什么样的“新东西”**。
这本智能菜谱有三个核心部分:
- 食材来源 (Generator):从哪里拿原材料。
- 筛选条件 (Filter/Guard):只挑选符合条件的原材料。
- 加工方法 (
yield
):把选好的原材料加工成一道新菜。
1. 最简单的推导式:加工所有食材
假设你有一篮子数字,你想要一个“新的篮子”,里面装的是原来每个数字的两倍。
食材来源:nums = List(1, 2, 3, 4)
加工方法:把每个数字 n
变成 n * 2
val nums = List(1, 2, 3, 4)// 智能菜谱写法
val doubledNums = for (n <- nums) yield n * 2println(doubledNums) // 输出: List(2, 4, 6, 8)
通俗解释:
for (n <- nums)
:“对于nums
篮子里的每一个东西,我们暂时叫它n
”。这里的<-
符号可以读作 “in” 或者 “来自”。yield n * 2
:“请把n * 2
这个新东西,放进我最终想要的新篮子里”。yield
是“产出”的意思,是for
推导式的灵魂。只要看到yield
,就说明它不是一个简单的循环,而是在构建一个全新的集合。
2. 加入筛选条件:只挑选想要的食材
现在,我们只想加工篮子里的偶数。
食材来源:nums = List(1, 2, 3, 4)
筛选条件:只选偶数,即 n % 2 == 0
加工方法:把选出来的偶数 n
变成 n * 2
val nums = List(1, 2, 3, 4)// 加入 if 条件
val doubledEvens = for (n <- nums if n % 2 == 0) yield n * 2println(doubledEvens) // 输出: List(4, 8)
通俗解释:
if n % 2 == 0
:这就是一个“过滤器”或“守卫”。它告诉菜谱:“只有当n
是偶数时,才对它进行下一步的yield
加工,否则就直接跳过这个n
”。
3. 使用多种食材:制作组合菜
这是 for
推导式最强大的地方。假设你有两种食材,你想把它们两两组合,创造出所有可能的搭配。
食材来源1 (字母):letters = List("A", "B")
食材来源2 (数字):numbers = List(1, 2)
加工方法:把字母 l
和数字 n
组合成一个新字符串,比如 "A1"
val letters = List("A", "B")
val numbers = List(1, 2)// 组合所有可能
val combinations = for {l <- lettersn <- numbers
} yield l + nprintln(combinations) // 输出: List("A1", "A2", "B1", "B2")
通俗解释:
- 这个推导式读起来就像:“对于每一个来自
letters
的字母l
,请再遍历一遍numbers
里的所有数字n
,然后产出l + n
这个组合”。 - 它非常清晰地表达了“组合所有可能性”的意图,比写两层嵌套的
for
循环要优雅得多。
总结:for
推导式是什么?
它根本就不是一个循环,而是一个集合(或其它容器)的“构造器”或“转换器”。
它让你用一种非常接近自然语言的方式,来描述你想要生成一个什么样的新集合,而不需要你自己去写循环、判断、创建新列表、添加元素这些繁琐的步骤。编译器会把这个漂亮的“智能菜谱”自动翻译成背后更复杂的代码(map
, flatMap
, filter
等操作)。
所以,当你下次想对一个列表进行转换、筛选、或者组合来得到一个新列表时,就想想这本“智能菜谱”吧!
二、返回的结果类型
for
推导式产出的集合类型,通常取决于你第一个 “食材来源” (generator) 的类型。
可以把它理解成一个“从哪里来,回哪里去”的原则。你从一个 List
开始,它就给你返回一个新的 List
;你从一个 Set
开始,它就给你返回一个新的 Set
。
下面我们来看几个例子,你就彻底明白了。
1. 从列表(List)出发,得到列表(List)
这是我们之前看到的例子,也是最常见的情况。
val numbers = List(1, 2, 3)
val result = for (n <- numbers) yield n * 2
// numbers 是 List, 所以 result 也是 List
println(result) // 输出: List(2, 4, 6)
println(result.getClass) // 输出: class scala.collection.immutable.List
2. 从集合(Set)出发,得到集合(Set)
Set
是一个不允许有重复元素的集合。for
推导式会很智能地保持这个特性。
val numbers = Set(1, 2, 3, 2, 1) // Set 会自动去重,实际内容是 {1, 2, 3}
val result = for (n <- numbers) yield n * 2
// numbers 是 Set, 所以 result 也是 Set
println(result) // 输出: Set(2, 4, 6)
println(result.getClass) // 输出: class scala.collection.immutable.HashSet
3. 从字符串(String)出发,得到字符串(String)
这是一个很有趣的例子!在 Scala 中,字符串可以被看作是字符(Char
)的集合。
val text = "abc"
val result = for (char <- text) yield char.toUpper
// text 是 String, 所以 result 也是 String
println(result) // 输出: ABC
println(result.getClass) // 输出: class java.lang.String
4. 从范围(Range)出发,得到向量(Vector)
Range
是一种特殊的表示数字序列的集合。对它进行推导,通常会得到一个更通用的序列类型 Vector
。Vector
你可以暂时就把它当成一个性能很好的 List
。
val numbers = 1 to 3 // 这是一个 Range,包含 1, 2, 3
val result = for (n <- numbers) yield n * 2
// numbers 是 Range, 但结果是一个 Vector
println(result) // 输出: Vector(2, 4, 6)
println(result.getClass) // 输出: class scala.collection.immutable.Vector
5. 从映射(Map)出发,得到映射(Map)
如果你从一个 Map
(键值对集合)出发,并且 yield
回一个新的键值对,那么你就会得到一个新的 Map
。
val scores = Map("Alice" -> 10, "Bob" -> 8)// yield 一个 (key -> value) 格式的元组
val result = for ((name, score) <- scores) yield (name.toUpperCase -> (score * 10))// scores 是 Map, yield 的是键值对, 所以 result 也是 Map
println(result) // 输出: Map("ALICE" -> 100, "BOB" -> 80)
println(result.getClass) // 输出: class scala.collection.immutable.Map$Map2
总结与技巧
-
同类型返回原则:
for
推导式非常智能,它会尝试返回与源集合最匹配的类型。List
->List
,Set
->Set
,String
->String
。 -
我想要指定类型怎么办?:如果你想把推导的结果强制变成某个特定的集合类型,非常简单,在表达式的最后调用
.toXxx
方法即可。val text = "hello" // 我希望结果是一个 List, 而不是 String val resultAsList = (for (c <- text) yield c.toUpper).toList println(resultAsList) // 输出: List(H, E, L, L, O)
所以,for
推导式远比一个简单的循环要强大,它深度整合了 Scala 强大的集合库,让代码既简洁又符合逻辑。