Scala 中避免使用 null 值及 Option 模式的应用
1. 匹配表达式简介
匹配表达式是 Scala 语言不可或缺的一部分,具有多种使用方式。随着使用的深入,你会发现更多的应用场景。
2. 消除代码中的 null 值
2.1 问题提出
早在 1965 年发明 null 引用的 Tony Hoare 称 null 值的创建是他的“十亿美元错误”。为遵循现代最佳实践,我们应消除代码中的 null 值。
2.2 解决方案
David Pollak 提出简单规则:“禁止在代码中使用 null”。以下是几种常见情况及避免使用 null 的方法:
-
使用 Option 初始化 var 字段
:当类或方法中的 var 字段没有初始默认值时,用 Option 而非 null 进行初始化。
例如,原本可能这样写:
case class Address (city: String, state: String, zip: String)
class User(email: String, password: String) {
var firstName: String = _
var lastName: String = _
var address: Address = _
}
这种方式中
firstName
、
lastName
和
address
都被声明为 null,若在赋值前访问可能导致应用程序出现问题。
更好的做法是将每个字段定义为 Option:
case class Address (city: String, state: String, zip: String)
class User(email: String, password: String) {
var firstName = None: Option[String]
var lastName = None: Option[String]
var address = None: Option[Address]
}
创建用户实例并赋值和访问字段的示例如下:
val u = new User("al@example.com", "secret")
u.firstName = Some("Al")
u.lastName = Some("Alexander")
u.address = Some(Address("Talkeetna", "AK", "99676"))
println(u.firstName.getOrElse("<not assigned>"))
u.address.foreach { a =>
println(a.city)
println(a.state)
println(a.zip)
}
在构造函数中,当字段为可选时也应使用 Option,如:
case class Stock (id: Long,
var symbol: String,
var company: Option[String])
-
方法不返回 null
:方法不应返回 null。若不能返回 null,可返回 Option;若需了解方法中可能出现的错误,使用 Try 替代 Option。
方法签名示例:
def doSomething: Option[String] = { ... }
def toInt(s: String): Option[Int] = { ... }
def lookupPerson(name: String): Option[Person] = { ... }
以读取文件为例,避免返回 null 的代码如下:
def readTextFile(filename: String): Option[List[String]] = {
try {
Some(io.Source.fromFile(filename).getLines.toList)
} catch {
case e: Exception => None
}
}
若需要错误信息,可使用 Try/Success/Failure 方法:
import scala.util.{Try, Success, Failure}
object Test extends App {
def readTextFile(filename: String): Try[List[String]] = {
Try(io.Source.fromFile(filename).getLines.toList)
}
val filename = "/etc/passwd"
readTextFile(filename) match {
case Success(lines) => lines.foreach(println)
case Failure(f) => println(f)
}
}
-
将 null 转换为 Option 或其他类型
:在处理遗留 Java 代码时会遇到 null 值,可捕获 null 值并返回其他合适的类型,如 Option、空对象、空列表等。
例如,将 Java 方法可能返回的 null 结果转换为 Option[String]:
def getName: Option[String] = {
var name = javaPerson.getName
if (name == null) None else Some(name)
}
2.3 消除 null 值的好处
-
消除
NullPointerException。 - 代码更安全。
- 无需编写检查 null 值的 if 语句。
-
为方法添加
Option[T]返回类型声明能明确告知调用者可能收到 None 而非Some[T],比返回 null 更优。 - 更熟悉 Option 的使用,便于利用其在集合库和其他框架中的应用。
3. 使用 Option/Some/None 模式
3.1 问题提出
出于多种原因,如消除代码中的 null 值,我们希望使用 Option/Some/None 模式;若关注代码处理过程中出现的问题(异常),可从方法返回 Try/Success/Failure 而非 Option/Some/None。
3.2 解决方案
-
从方法返回 Option
:以
toInt方法为例,它接收一个 String 作为输入,若成功转换为 Int 则返回Some[Int],否则返回 None:
def toInt(s: String): Option[Int] = {
try {
Some(Integer.parseInt(s.trim))
} catch {
case e: Exception => None
}
}
在 REPL 中调用示例:
scala> val x = toInt("1")
x: Option[Int] = Some(1)
scala> val x = toInt("foo")
x: Option[Int] = None
- 从 Option 获取值 :作为返回 Option 的方法的调用者,可通过以下方式访问结果:
-
使用
getOrElse:获取方法成功时的实际值,或方法失败时使用默认值。
scala> val x = toInt("1").getOrElse(0)
x: Int = 1
-
使用
foreach:由于 Option 可视为包含零个或一个元素的集合,foreach方法在很多情况下适用。
toInt("1").foreach{ i =>
println(s"Got an int: $i")
}
- 使用匹配表达式 :
toInt("1") match {
case Some(i) => println(i)
case None => println("That didn't work.")
}
- 在 Scala 集合中使用 Option :Option 与 Scala 集合配合良好。例如,有一个字符串列表:
val bag = List("1", "2", "foo", "3", "bar")
若想得到能从该列表转换而来的所有整数列表,可通过以下操作实现:
scala> bag.map(toInt)
res0: List[Option[Int]] = List(Some(1), Some(2), None, Some(3), None)
scala> bag.map(toInt).flatten
res1: List[Int] = List(1, 2, 3)
scala> bag.flatMap(toInt)
res2: List[Int] = List(1, 2, 3)
scala> bag.map(toInt).collect{case Some(i) => i}
res3: List[Int] = List(1, 2, 3)
这些操作能成功的原因如下:
| 原因 | 说明 |
| ---- | ---- |
|
toInt
定义 |
toInt
定义为返回
Option[Int]
|
| 集合方法特性 |
flatten
、
flatMap
等方法能很好地处理 Option 值 |
| 匿名函数使用 | 可将匿名函数传递给集合方法 |
以下是操作流程的 mermaid 流程图:
graph LR
A[字符串列表 bag] --> B[map(toInt)]
B --> C[List[Option[Int]]]
C --> D[flatten 或 flatMap]
D --> E[List[Int]]
C --> F[collect{case Some(i) => i}]
F --> E
Scala 中避免使用 null 值及 Option 模式的应用
3. 使用 Option/Some/None 模式(续)
3.2 解决方案(续)
-
在其他框架中使用 Option
:在使用第三方 Scala 库时,会发现 Option 用于处理变量可能没有值的情况。
- Play Framework 的 Anorm 数据库库 :在数据库表字段可能为 null 的情况下,使用 Option/Some/None 处理。示例代码如下:
def getAll() : List[Stock] = {
DB.withConnection { implicit connection =>
sqlQuery().collect {
// the 'company' field has a value
case Row(id: Int, symbol: String, Some(company: String)) =>
Stock(id, symbol, Some(company))
// the 'company' field does not have a value
case Row(id: Int, symbol: String, None) =>
Stock(id, symbol, None)
}.toList
}
}
- **Play 验证方法**:Option 方法也广泛应用于 Play 验证方法中。示例如下:
verifying("If age is given, it must be greater than zero",
model =>
model.age match {
case Some(age) => age < 0
case None => true
}
)
- **scala.util.control.Exception 对象**:可使用该对象的 `allCatch` 方法替代 `try/catch` 块。示例代码如下:
import scala.util.control.Exception._
def readTextFile(f: String): Option[List[String]] =
allCatch.opt(Source.fromFile(f).getLines.toList)
-
使用 Try, Success, and Failure
:Scala 2.10 引入了
scala.util.Try,它与 Option 类似,但能返回失败信息。- 示例方法 :
import scala.util.{Try, Success, Failure}
def divideXByY(x: Int, y: Int): Try[Int] = {
Try(x / y)
}
在 REPL 中的调用示例:
scala> divideXByY(1,1)
res0: scala.util.Try[Int] = Success(1)
scala> divideXByY(1,0)
res1: scala.util.Try[Int] = Failure(java.lang.ArithmeticException: / by zero)
- **访问 Try 结果的方式**:
- **使用 `getOrElse`**:
scala> val x = divideXByY(1, 1).getOrElse(0)
x: Int = 1
scala> val y = divideXByY(1, 0).getOrElse(0)
y: Int = 0
- **使用 `foreach`**:
scala> divideXByY(1, 1).foreach(println)
1
scala> divideXByY(1, 0).foreach(println)
(no output printed)
- **使用匹配表达式**:
divideXByY(1, 1) match {
case Success(i) => println(s"Success, value is: $i")
case Failure(s) => println(s"Failed, message is: $s")
}
- **Try 类的其他特性**:
- 可链式操作,捕获异常。示例代码如下:
val z = for {
a <- Try(x.toInt)
b <- Try(y.toInt)
} yield a * b
val answer = z.getOrElse(0) * 2
- 包含多种方法,如 `filter`、`flatMap`、`flatten`、`foreach`、`map`、`get`、`getOrElse`、`orElse`、`toOption`、`recover`、`recoverWith` 和 `transform` 等。
以下是 Try 操作的 mermaid 流程图:
graph LR
A[输入 x, y] --> B[divideXByY(x, y)]
B --> C{是否成功}
C -->|是| D[Success]
C -->|否| E[Failure]
D --> F[使用 getOrElse 等方法获取值]
E --> G[使用匹配表达式获取错误信息]
-
使用 Either, Left, and Right
:在 Scala 2.10 之前,可使用
Either、Left和Right类实现类似 Try 的功能。- 示例方法 :
def divideXByY(x: Int, y: Int): Either[String, Int] = {
if (y == 0) Left("Dude, can't divide by 0")
else Right(x / y)
}
- **说明**:方法应声明返回 `Either`,成功时返回 `Right`,失败时返回 `Left`。`Right` 类型是方法成功运行时返回的类型,`Left` 类型通常为 String 用于返回错误信息。
综上所述,在 Scala 编程中,避免使用 null 值并合理运用 Option/Some/None 模式、Try/Success/Failure 以及 Either/Left/Right 等方法,能使代码更加健壮、安全,减少错误的发生,同时提高代码的可读性和可维护性。通过掌握这些方法,开发者可以更好地处理各种异常情况,提升编程效率。
以下是不同方法的对比表格:
| 方法 | 适用场景 | 优点 | 缺点 |
| ---- | ---- | ---- | ---- |
| Option/Some/None | 处理变量可能没有值的情况 | 简单易用,能避免 null 问题 | 无法返回详细错误信息 |
| Try/Success/Failure | 需要获取错误信息的场景 | 能返回失败信息,可链式操作 | 相对复杂 |
| Either/Left/Right | Scala 2.10 之前类似 Try 的场景 | 提供错误信息 | 不如 Try 功能丰富 |
超级会员免费看
110

被折叠的 条评论
为什么被折叠?



