Post

Scala基础教程 第2节 控制结构

Scala基础教程 第2节 控制结构

https://docs.scala-lang.org/overviews/scala-book/control-structures.html

2.1 if/else

https://docs.scala-lang.org/overviews/scala-book/if-then-else-construct.html

Scala的基本if语句如下:

1
2
3
if (a == b) {
  doSomething()
}

对于单条语句可以省略花括号:

1
if (a == b) doSomething()

if/else结构如下:

1
2
3
4
5
if (a == b) {
  doSomething()
} else {
  doSomethingElse()
}

if/else-if/else结构如下(实际上是else后面跟着另一个if/else):

1
2
3
4
5
6
7
if (test1) {
  doX()
} else if (test2) {
  doY()
} else {
  doZ()
}

Scala的if语句总是返回一个结果。可以像前面的例子一样忽略结果,但一种更常见用法(尤其是在函数式编程中)是将结果赋给一个变量:

1
val minValue = if (a < b) a else b

这样Scala就不需要特殊的“三元运算符”。

2.2 for循环

https://docs.scala-lang.org/overviews/scala-book/for-loops.html

Scala的for循环可用于迭代集合中的元素。形式如下:

1
for (elem <- collection) doSomething()

例如:

1
2
val nums = Seq(1, 2, 3)
for (n <- nums) println(n)

可以像这样遍历整数范围:

1
2
3
4
5
6
scala> for (i <- 0 to 5) print(s"$i ")
0 1 2 3 4 5 
scala> for (i <- 0 until 5) print(s"$i ")
0 1 2 3 4 
scala> for (i <- 0 to 10 by 2) print(s"$i ")
0 2 4 6 8 10 

注:这实际上是用中缀语法调用了RichInt类的to()until()方法。0 until 5等价于0.until(5),返回一个可迭代对象Range(0, 5)

Scala也有whiledo/while循环:

1
2
3
4
5
6
7
8
9
// while loop
while (condition) {
  statement
}

// do-while
do {
  statement
} while (condition)

2.2.1 多个生成器

for循环可以有多个生成器(相当于嵌套循环)。例如:

1
2
3
4
5
6
for {
  i <- 1 to 2
  j <- 'a' to 'c'
} {
  println(s"i = $i, j = $j")
}

输出结果如下:

1
2
3
4
5
6
i = 1, j = a
i = 1, j = b
i = 1, j = c
i = 2, j = a
i = 2, j = b
i = 2, j = c

2.2.2 守卫

for循环还可以包含if语句,称为守卫(guard)。例如:

1
2
3
4
5
6
for {
  i <- 1 to 5
  if i % 2 == 0
} {
  println(i)
}

输出结果为

1
2
2
4

for循环可以有任意数量的守卫(相当于所有的条件取and)。下面的例子只打印 “4” :

1
2
3
4
5
6
7
8
for {
  i <- 1 to 10
  if i > 3
  if i < 6
  if i % 2 == 0
} {
  println(i)
}

2.2.3 迭代映射

可以使用for循环迭代Scala Map(类似于Java HashMap)的键值对。Map[K, V]是键值对元组(K, V)的集合。

例如,给定电影名称和评分的映射:

1
2
3
4
5
val ratings = Map(
  "Lady in the Water"  -> 3.0, 
  "Snakes on a Plane"  -> 4.0, 
  "You, Me and Dupree" -> 3.5
)

可以像这样使用for循环打印电影评分:

1
2
for ((name, rating) <- ratings)
  println(s"Movie: $name, Rating: $rating")

2.2.4 foreach方法

为了迭代集合元素,还可以使用Scala集合类的foreach()方法,其参数是接受一个元素参数的函数。

例如,可以像这样打印整数列表:

1
2
val nums = Seq(1, 2, 3)
nums.foreach(println)

也可以像这样使用foreach()打印前面的映射:

1
2
3
ratings.foreach {
  case (name, rating) => println(s"Movie: $name, Rating: $rating")
}

注:上面的代码利用了模式匹配的匿名函数语法,等价于以下写法:

1
ratings.foreach(t => println(s"Movie: ${t._1}, Rating: ${t._2}"))

2.3 for表达式

https://docs.scala-lang.org/overviews/scala-book/for-expressions.html

https://docs.scala-lang.org/tour/for-comprehensions.html

for循环用于副作用(如打印输出),而for表达式用于从现有集合创建新集合(类似于Python的列表推导式)。for表达式的语法类似于for循环,但语句体是yield语句。

例如,给定一个整数列表

1
val nums = Seq(1, 2, 3, 4, 5)

可以像这样创建一个新的整数列表,其中每个值都加倍:

1
2
scala> val doubledNums = for (n <- nums) yield n * 2
val doubledNums: Seq[Int] = List(2, 4, 6, 8, 10)

这等价于下面的map()方法调用:

1
val doubledNums = nums.map(_ * 2)

for表达式也可以有多个生成器和守卫。例如:

1
2
val fruits = List("apple", "banana", "lime", "orange")
val fruitLengths = for (f <- fruits if f.length > 4) yield f.length // List(5, 6, 6)

这等价于

1
val fruitLengths = fruits.filter(_.length > 4).map(_.length)

2.4 match表达式和模式匹配

https://docs.scala-lang.org/overviews/scala-book/match-expressions.html

https://docs.scala-lang.org/tour/pattern-matching.html

模式匹配(pattern matching)是一种根据模式检查值的机制。Scala通过match表达式实现模式匹配,它是Java switch语句的一个更强大的版本。

Scala模式匹配的完整语法参见Scala语言规范-Pattern Matching

2.4.1 基本用法

match表达式由一个值、match关键字和若干个case子句(也称为备选项(alternative))组成。例如:

1
2
3
4
5
6
7
8
9
import scala.util.Random

val i: Int = Random.nextInt(10)

i match {
  case 1 => println("one")
  case 2 => println("two")
  case _ => println("other")
}

其中i是0~9之间的随机整数,后面的match表达式有3个case,最后一个_是默认case,可以匹配其他任何值(catch-all)。如果不匹配任何case,也没有默认case,则会抛出MatchError异常。

match表达式也会返回值(类似于Java 14引入的switch表达式),因此可以将其赋给变量:

1
2
3
4
5
val s = i match {
  case 1 => "one"
  case 2 => "two"
  case _ => "other"
}

match表达式作为方法体也是一种常见的用法:

1
2
3
4
5
def matchTest(i: Int): String = i match {
  case 1 => "one"
  case 2 => "two"
  case _ => "other"
}

match表达式允许在单个case语句中匹配多个值。下面的例子计算“布尔相等”:0或空字符串的结果为false,其他任何值的结果为true

1
2
3
4
def isTrue(a: Any) = a match {
  case 0 | "" => false
  case _ => true
}

由于参数类型为Any(Scala的顶级类型),这个方法适用于任何数据类型:

1
2
3
4
5
6
7
8
9
10
11
scala> isTrue(0)
val res0: Boolean = false

scala> isTrue("")
val res1: Boolean = false

scala> isTrue(1.1F)
val res2: Boolean = true

scala> isTrue(List())
val res3: Boolean = true

下面是另一个在一个case语句中处理多个值的例子:

1
2
3
4
5
val evenOrOdd = i match {
  case 1 | 3 | 5 | 7 | 9 => "odd"
  case 2 | 4 | 6 | 8 | 10 => "even"
  case _ => "some other number"
}
1
2
3
4
5
cmd match {
  case "start" | "go" => println("starting")
  case "stop" | "quit" | "exit" => println("stopping")
  case _ => println("doing nothing")
}

2.4.2 模式守卫

可以在case语句中使用if表达式,例如:

1
2
3
4
5
6
7
8
9
10
count match {
  case 1 =>
    println("one, a lonely number")
  case x if x == 2 || x == 3 =>
    println("two's company, three's a crowd")
  case x if x > 3 =>
    println("4+, that's a party")
  case _ =>
    println("i'm guessing your number is zero or less")
}

case后的变量x会匹配任何满足条件的值,并将值绑定到该变量,可以在=>后的语句中访问该变量。

2.4.3 匹配元组

可以像这样匹配元组并提取元素:

1
2
3
4
5
def matchTuple(t: Product): String = t match {
  case (a, b) => s"Pair: ($a, $b)"
  case (a, b, c) => s"Triple: ($a, $b, $c)"
  case _ => "Other tuple"
}
1
2
3
4
5
6
7
8
scala> matchTuple(("hello", 42))
val res0: String = Pair: (hello, 42)

scala> matchTuple(("hello", 42, true))
val res1: String = Triple: (hello, 42, true)

scala> matchTuple(("hello", 42, true, 3.14))
val res2: String = Other tuple

其中Product特质是Product1~Product22的公共超类,分别表示具有1~22个固定元素的类型。元组Tuple1~Tuple22分别扩展了这些特质。

2.4.4 匹配数组和集合

匹配数组:

1
2
3
4
5
6
def matchArray(arr: Array[Int]): String = arr match {
  case Array() => "Empty array"
  case Array(x) => s"Single element: $x"
  case Array(x, y) => s"Two elements: $x, $y"
  case Array(x, _*) => s"At lease one element, head: $x"
}

匹配列表:

1
2
3
4
def matchList(list: List[Int]): String = list match {
  case Nil => "Empty list"
  case head :: tail => s"Head: $head, tail: $tail"
}

2.4.5 匹配类型

还可以匹配值的类型(类似于Java 16引入的instanceof模式匹配)。例如:

1
2
3
4
5
6
7
def getClassAsString(x: Any): String = x match {
  case s: String => s"'$s' is a String"
  case i: Int => "Int"
  case d: Double => "Double"
  case l: List[_] => "List"
  case _ => "Unknown"
}
1
2
3
4
5
6
7
8
scala> getClassAsString(1)
val res0: String = Int

scala> getClassAsString("hello")
val res1: String = 'hello' is a String

scala> getClassAsString(List(1, 2, 3))
val res2: String = List

注意:由于类型擦除,match表达式无法匹配泛型类的类型参数。即使将第4个case写为case l: List[Int],传递一个List[String]仍然会匹配这个case。

2.4.6 匹配case类

Case类对于模式匹配特别有用(将在第8节详细介绍)。

1
2
3
4
5
6
7
sealed trait Notification

case class Email(sender: String, title: String, body: String) extends Notification

case class SMS(caller: String, message: String) extends Notification

case class VoiceRecording(contactName: String, link: String) extends Notification

Notification是一个密封特质(只能被同一个文件中的类扩展),有3个实现case类:EmailSMSVoiceRecording。现在可以对这些case类进行模式匹配:

1
2
3
4
5
6
7
8
9
10
def showNotification(notification: Notification): String = {
  notification match {
    case Email(sender, title, _) =>
      s"You got an email from $sender with title: $title"
    case SMS(number, message) =>
      s"You got an SMS from $number! Message: $message"
    case VoiceRecording(name, link) =>
      s"You received a Voice Recording from $name! Click the link to hear it: $link"
  }
}
1
2
3
4
5
6
7
8
9
10
val someEmail = Email("jenny@gmail.com", "Drinks tonight?", "I'm free after 5!")
val someSms = SMS("12345", "Are you there?")
val someVoiceRecording = VoiceRecording("Tom", "voicerecording.org/id/123")

println(showNotification(someEmail))
  // You got an email from jenny@gmail.com with title: Drinks tonight?
println(showNotification(someSms))
  // You got an SMS from 12345! Message: Are you there?
println(showNotification(someVoiceRecording))
  // You received a Voice Recording from Tom! Click the link to hear it: voicerecording.org/id/123

当基类型是sealed时,编译器会检查match表达式的case是否穷尽。例如,在上面的showNotification()方法中,如果遗漏一个case(比如VoiceRecording),编译器会发出警告:

1
2
match may not be exhaustive.
It would fail on the following input: VoiceRecording(_, _)

此时如果输入VoiceRecording对象,会抛出MatchError异常。

2.4.7 字符串匹配

s插值符也可以用于模式匹配。例如:

1
2
3
4
5
6
7
val input = "Alice is 25 years old"

val result = input match {
  case s"$name is $age years old" => s"$name's age is $age"
  case _ => "No match"
}
// result: "Alice's age is 25"

在这个例子中,根据模式提取了字符串的nameage部分,这有助于解析结构化文本。

还可以将提取器对象用于字符串模式匹配。

1
2
3
4
5
6
7
8
9
10
11
object Age {
  def unapply(s: String): Option[Int] = s.toIntOption
}

val input: String = "Alice is 25 years old"

val (name, age) = input match {
  case s"$name is ${Age(age)} years old" => (name, age)
}
// name: String = Alice
// age: Int = 25

2.4.8 自定义提取器

https://docs.scala-lang.org/tour/extractor-objects.html

提取器对象(extractor object)是具有unapply()方法的对象。apply()方法类似于构造器,接受参数并创建一个对象;unapply()方法相反,接受一个对象并尝试还原出构造参数。这通常用于模式匹配和偏函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
object Address {
  def apply(host: String, port: Int): String = s"$host:$port"

  def unapply(address: String): Option[(String, Int)] = {
    address.split(":", 2) match {
      case Array(host, portStr) if host.nonEmpty => portStr.toIntOption match {
        case Some(port) if port >= 1 && port <= 65535 => Some((host, port))
        case _ => None
      }
      case _ => None
    }
  }
}

Address(host, port)等价于Address.apply(host, port),由主机名和端口号构造一个地址字符串。例如:

1
2
3
println(Address("localhost", 8080))  // "localhost:8080"
println(Address("192.168.1.1", 80))  // "192.168.1.1:80"
println(Address("example.com", 443)) // "example.com:443"

unapply()方法执行相反的操作,尝试将地址字符串解析为主机名和端口号。返回类型Option表示可选的值,只有两个子类:SomeNone。对一个字符串s进行模式匹配时,case Address(host, port) => ...会调用Address.unapply(s)。如果返回Some((host, port))则匹配该case,并将元组的两个元素分别赋给变量hostport;否则不匹配该case。例如:

1
2
3
4
def matchAddress(address: String): String = address match {
  case Address(host, port) => s"host is $host, port is $port"
  case _ => "Invalid address"
}
1
2
3
4
5
6
7
println(matchAddress("localhost:8080"))  // "host is localhost, port is 8080"
println(matchAddress("192.168.1.1:80"))  // "host is 192.168.1.1, port is 80"
println(matchAddress("example.com:443")) // "host is example.com, port is 443"
println(matchAddress("host:99999"))      // "Invalid address"
println(matchAddress("host:abc"))        // "Invalid address"
println(matchAddress(":8080"))           // "Invalid address"
println(matchAddress("invalid"))         // "Invalid address"

提取器可用于初始化变量:

1
2
3
4
val addr = "localhost:8080"
val Address(host, port) = addr
// host: String = localhost
// port: Int = 8080

这等价于val (host, port) = Address.unapply(addr).get。如果匹配失败,会抛出MatchError

Scala会自动为case类生成unapply()方法,因此case类可以直接用于模式匹配。

unapply()方法的返回类型应该按如下方式选择:

  • 如果仅测试是否匹配,则返回Boolean
  • 如果结果为单个T类型的值,则返回Option[T]
  • 如果结果为多个值T1,...,Tn,则返回可选的元组Option[(T1,...,Tn)]

有时,要提取的值的数量不是固定的。在这种情况下,可以定义具有unapplySeq()方法的提取器,返回Option[Seq[T]]。例如匹配列表和正则表达式。

2.4.9 正则表达式

https://docs.scala-lang.org/tour/regular-expression-patterns.html

正则表达式(regular expression)是可用于查找模式的字符串。可以使用.r方法将字符串转换为正则表达式。

1
2
3
4
5
6
val numberPattern = "[0-9]".r

numberPattern.findFirstMatchIn("awesomepassword") match {
  case Some(_) => println("Password OK")
  case None => println("Password must contain a number")
}

在上面的示例中,numberPattern是一个scala.util.matching.Regex,用于匹配单个0~9的数字。

可以使用圆括号搜索正则表达式的分组(group)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
val keyValPattern = "([0-9a-zA-Z- ]+): ([0-9a-zA-Z-#()/. ]+)".r

val input: String =
  """background-color: #A03300;
    |background-image: url(img/header100.png);
    |background-position: top center;
    |background-repeat: repeat-x;
    |background-size: 2160px 108px;
    |margin: 0;
    |height: 108px;
    |width: 100%;""".stripMargin

for (patternMatch <- keyValPattern.findAllMatchIn(input))
  println(s"key: ${patternMatch.group(1)} value: ${patternMatch.group(2)}")

输出结果如下:

1
2
3
4
5
6
7
8
key: background-color value: #A03300
key: background-image value: url(img/header100.png)
key: background-position value: top center
key: background-repeat value: repeat-x
key: background-size value: 2160px 108px
key: margin value: 0
key: height value: 108px
key: width value: 100

另外,Regex类定义了unapplySeq()方法,因此正则表达式可以用于模式匹配,从而方便地提取匹配的分组。模式中的变量个数必须与分组个数完全一致,但可以用_*捕获结尾的所有变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def saveContactInformation(contact: String): Unit = {
    val emailPattern = """^(\w+)@(\w+(.\w+)+)$""".r
    val phonePattern = """^(\d{3}-\d{3}-\d{4})$""".r

  contact match {
      case emailPattern(localPart, _*) =>
      println(s"Hi $localPart, we have saved your email address.")
    case phonePattern(phoneNumber) => 
      println(s"Hi, we have saved your phone number $phoneNumber.")
    case _ => 
      println("Invalid contact information, neither an email address nor phone number.")
  }
}

saveContactInformation("123-456-7890")
saveContactInformation("JohnSmith@sample.domain.com")
saveContactInformation("2 Franklin St, Mars, Milky Way")

输出结果如下:

1
2
3
Hi, we have saved your phone number 123-456-7890.
Hi JohnSmith, we have saved your email address.
Invalid contact information, neither an email address nor phone number.

2.5 try/catch

https://docs.scala-lang.org/overviews/scala-book/try-catch-finally.html

与Java一样,Scala的try/catch语句可以捕获异常。主要区别在于,Scala使用与match匹配相同的语法——case语句来匹配可能发生的异常。

在下面的例子中,openAndReadAFile()方法打开一个文件并读取其中的文本,将结果赋给变量text。该方法可能会抛出FileNotFoundExceptionIOException,在catch块中捕获这两个异常。

1
2
3
4
5
6
7
8
9
var text = ""
try {
  text = openAndReadAFile(filename)
} catch {
  case e: FileNotFoundException =>
    println("Couldn't find that file.")
  case e: IOException =>
    println("Had an IOException trying to read that file")
}

Scala的try/catch语法还允许使用finally子句,通常用于关闭资源。以下是一个示例:

1
2
3
4
5
6
7
8
9
10
11
val in = new FileInputStream(...);
try {
  // your scala code here
} catch {
  case e: IOException =>
    println("Got an IOException")
  case _: Throwable =>
    println("Got some other kind of Throwable exception")
} finally {
  in.close();
}
This post is licensed under CC BY 4.0 by the author.