函数式编程就像写一系列代数方程,因为代数没有空值或抛出异常,所以你不用在 FP 中使用这些特性。 这带来了一个有趣的问题:在 OOP 代码中通常可能使用空值或异常的情况下,您会怎么做?
Scala 的解决方案是使用类似 Option
/Some
/None
类的结构。
本课介绍如何使用这些技术。
在我们开始之前有两个注意事项:
Some
和None
类是Option
的子类。- 下面的文字一般只指“
Option
”或“Option
类”,而不是重复说“Option
/Some
/None
”。
第一个例子
虽然第一个示例不处理空值,但它是引入 Option
类的好方法,所以我们将从它开始。
想象一下,您想编写一个方法,可以轻松地将字符串转换为整数值,并且您想要一种优雅的方法来处理异常,这个是异常是该方法获取类似“Hello”而不是“1”的字符串时引发的。 对这种方法的初步猜测可能如下所示:
def makeInt(s: String): Int =
try {
Integer.parseInt(s.trim)
} catch {
case e: Exception => 0
}
def makeInt(s: String): Int =
try
Integer.parseInt(s.trim)
catch
case e: Exception => 0
如果转换成功,则此方法返回正确的 Int
值,但如果失败,则该方法返回 0
。
出于某些目的,这可能是可以的,但它并不准确。
例如,该方法可能收到了"0"
,但它也可能收到了 "foo"
、"bar"
或无数其他将引发异常的字符串。
这是一个真正的问题:您如何知道该方法何时真正收到 "0"
,或者何时收到其他内容?
答案是,用这种方法,没有办法知道。
使用 Option/Some/None
Scala 中这个问题的一个常见解决方案是使用三个类,称为 Option
、Some
和 None
。
Some
和 None
类是 Option
的子类,因此解决方案的工作原理如下:
- 你声明
makeInt
返回一个Option
类型 - 如果
makeInt
接收到一个字符串,它可以 转换为Int
,答案将包含在Some
中 - 如果
makeInt
接收到一个它无法转换的字符串,它返回一个None
这是 makeInt
的修订版:
def makeInt(s: String): Option[Int] =
try {
Some(Integer.parseInt(s.trim))
} catch {
case e: Exception => None
}
def makeInt(s: String): Option[Int] =
try
Some(Integer.parseInt(s.trim))
catch
case e: Exception => None
这段代码可以理解为,“当给定的字符串转换为整数时,返回包裹在 Some
中的 Int
,例如 Some(1)
。
当字符串无法转换为整数时,会抛出并捕获异常,并且该方法返回一个 None
值。”
这些示例展示了 makeInt
的工作原理:
val a = makeInt("1") // Some(1)
val b = makeInt("one") // None
如图所示,字符串"1"
产生一个 Some(1)
,而字符串 "one"
产生一个 None
。
这是错误处理的 Option
方法的本质。
如图所示,使用了这种技术,因此方法可以返回 值 而不是 异常。
在其他情况下,Option
值也用于替换 null
值。
两个注意事项:
- 你会发现这种方法在整个 Scala 库类和第三方 Scala 库中使用。
- 这个例子的一个关键点是函数式方法不会抛出异常;相反,它们返回类似
Option
的值。
成为 makeInt 的消费者
现在假设您是 makeInt
方法的使用者。
你知道它返回一个 Option[Int]
的子类,所以问题就变成了,你如何处理这些返回类型?
根据您的需要,有两个常见的答案:
- 使用
match
表达式 - 使用
for
表达式
使用 match
表达式
一种可能的解决方案是使用 match
表达式:
makeInt(x) match {
case Some(i) => println(i)
case None => println("That didn’t work.")
}
makeInt(x) match
case Some(i) => println(i)
case None => println("That didn’t work.")
在本例中,如果 x
可以转换为 Int
,则计算第一个 case
子句右侧的表达式;如果 x
不能转换为 Int
,则计算第二个 case
子句右侧的表达式。
使用 for
表达式
另一种常见的解决方案是使用 for
表达式,即本书前面显示的 for
/yield
组合。
例如,假设您要将三个字符串转换为整数值,然后将它们相加。
这就是你使用 for
表达式和 makeInt
的方法:
val y = for {
a <- makeInt(stringA)
b <- makeInt(stringB)
c <- makeInt(stringC)
} yield {
a + b + c
}
val y = for
a <- makeInt(stringA)
b <- makeInt(stringB)
c <- makeInt(stringC)
yield
a + b + c
在该表达式运行后,y
将是以下两种情况之一:
- 如果所有三个字符串都转换为
Int
值,y
将是Some[Int]
,即包裹在Some
中的整数 - 如果三个字符串中任意一个字符串不能转换为
Int
,则y
将是None
你可以自己测试一下:
val stringA = "1"
val stringB = "2"
val stringC = "3"
val y = for {
a <- makeInt(stringA)
b <- makeInt(stringB)
c <- makeInt(stringC)
} yield {
a + b + c
}
val stringA = "1"
val stringB = "2"
val stringC = "3"
val y = for
a <- makeInt(stringA)
b <- makeInt(stringB)
c <- makeInt(stringC)
yield
a + b + c
使用该样本数据,变量 y
的值将是 Some(6)
。
要查看失败案例,请将这些字符串中的任何一个更改为不会转换为整数的字符串。
当你这样做时,你会看到 y
是 None
:
y: Option[Int] = None
将 Option 视为容器
心智模型通常可以帮助我们理解新情况,因此,如果您不熟悉 Option
类,可以将它们视为容器:
Some
是一个容器,里面有一个项目None
是一个容器,但里面什么都没有
如果您更愿意将 Option
类想象成一个盒子,None
就像一个空盒子。
它可能有一些东西,但它没有。
使用 Option
替换 null
回到 null
值,null
值可以悄悄地潜入你的代码的地方是这样的类:
class Address(
var street1: String,
var street2: String,
var city: String,
var state: String,
var zip: String
)
虽然地球上的每个地址都有一个 street1
值,但 street2
值是可选的。
因此,street2
字段可以被分配一个 null
值:
val santa = new Address(
"1 Main Street",
null, // <-- D’oh! A null value!
"North Pole",
"Alaska",
"99705"
)
val santa = Address(
"1 Main Street",
null, // <-- D’oh! A null value!
"North Pole",
"Alaska",
"99705"
)
从历史上看,开发人员在这种情况下使用了空白字符串和空值,这两种方法都是使用技巧来解决基础性的问题,这个问题是:street2
是一个可选字段。
在 Scala 和其他现代语言中,正确的解决方案是预先声明 street2
是可选的:
class Address(
var street1: String,
var street2: Option[String], // an optional value
var city: String,
var state: String,
var zip: String
)
现在开发人员可以编写更准确的代码,如下所示:
val santa = new Address(
"1 Main Street",
None, // 'street2' has no value
"North Pole",
"Alaska",
"99705"
)
val santa = Address(
"1 Main Street",
None, // 'street2' has no value
"North Pole",
"Alaska",
"99705"
)
或这个:
val santa = new Address(
"123 Main Street",
Some("Apt. 2B"),
"Talkeetna",
"Alaska",
"99676"
)
val santa = Address(
"123 Main Street",
Some("Apt. 2B"),
"Talkeetna",
"Alaska",
"99676"
)
Option
不是唯一的解决方案
虽然本节关注的是 Option
类,但 Scala 还有一些其他选择。
例如,称为 Try
/Success
/Failure
的三个类以相同的方式工作,但是 (a) 当您的代码可以抛出异常时,您主要使用这些类,并且 (b) 您想要使用Failure
类,因为它使您可以访问异常消息。
例如,在编写与文件、数据库和 Internet 服务交互的方法时,通常会使用这些 Try
类,因为这些函数很容易引发异常。
快速回顾
这部分很长,让我们快速回顾一下:
- 函数式程序员不使用
null
值 null
值的主要替代品是使用Option
类- 函数式方法不会抛出异常; 相反,它们返回诸如
Option
、Try
或Either
之类的值 - 使用
Option
值的常用方法是match
和for
表达式 - Option 可以被认为是一个项目(
Some
)和没有项目(None
)的容器 - option 也可用于可选的构造函数或方法参数
Contributors to this page:
Contents
- 导言
- Scala 3 特性
- 为什么是 Scala 3 ?
- Scala 的味道
- Hello, World!
- The REPL
- 变量和数据类型
- 控制结构
- 领域建模
- 方法
- 头等函数
- 单例对象
- 集合
- 上下文抽象
- 顶层定义
- 总结
- 类型初探
- 字符串插值
- 控制结构
- 领域建模
- 工具
- OOP 领域建模
- 函数式领域建模
- 方法
- 方法特性
- main 方法
- 总结
- 函数
- 匿名函数
- 函数变量
- Eta 扩展
- 高阶函数
- 自定义 map 函数
- 创建可以返回函数的方法
- 总结
- 打包和导入
- Scala 集合
- 集合类型
- 集合方法
- 总结
- 函数式编程
- 什么是函数式编程?
- 不可变值
- 纯函数
- 函数是值
- 函数式错误处理
- 总结
- 类型和类型系统
- 类型推断
- 泛型
- 相交类型
- 联合类型
- 代数数据类型
- 型变
- 不透明类型
- 结构化类型
- 依赖函数类型
- 其他类型
- 上下文抽象
- 扩展方法
- Given 实例和 Using 语句
- 上下文绑定
- Given 导入
- 实现类型类
- 多元相等性
- 隐式转换
- 总结
- 并发
- Scala 工具
- 使用 sbt 构建和测试 Scala 项目
- worksheet
- 与 Java 交互
- 向 Java 开发者介绍Scala
- Scala for JavaScript Developers
- Scala for Python Developers
- 下一步去哪