83

我无法理解Option[T]Scala 中的课堂意义。我的意思是,我看不到Noneover的任何优点null

例如,考虑以下代码:

object Main{
  class Person(name: String, var age: int){
    def display = println(name+" "+age)
  }

  def getPerson1: Person = {
    // returns a Person instance or null
  }

  def getPerson2: Option[Person] = {
    // returns either Some[Person] or None
  }

  def main(argv: Array[String]): Unit = {
    val p = getPerson1
    if (p!=null) p.display

    getPerson2 match{
      case Some(person) => person.display
      case None => /* Do nothing */
    }
  }
}

现在假设,该方法getPerson1返回null,那么display对第一行的调用main必然会失败,并返回NPE。同样,如果getPerson2返回Nonedisplay调用将再次失败并出现一些类似的错误。

如果是这样,那么为什么 Scala 会通过引入新的值包装器(Option[T])而不是遵循 Java 中使用的简单方法来使事情复杂化?

更新:

我已经按照@Mitch的建议编辑了我的代码。我仍然看不到Option[T]. 我必须测试例外情况nullNone两种情况。:(

如果我从@Michael的回复中理解正确,唯一的好处Option[T]是它明确告诉程序员这个方法可以返回 None吗?这是这种设计选择背后的唯一原因吗?

4

18 回答 18

72

如果你Option强迫自己永远不要使用get. 那是因为get相当于“好吧,把我送回零地”。

所以,以你的例子为例。display如果不使用,您将如何调用get?以下是一些替代方案:

getPerson2 foreach (_.display)
for (person <- getPerson2) person.display
getPerson2 match {
  case Some(person) => person.display
  case _ =>
}
getPerson2.getOrElse(Person("Unknown", 0)).display

这些替代方案都不会让您调用display不存在的东西。

至于为什么get存在,Scala并没有告诉你你的代码应该怎么写。它可能会轻轻地刺激你,但如果你想回到没有安全网的状态,这是你的选择。


你把它钉在这里:

Option[T] 的唯一优点是它明确地告诉程序员这个方法可以返回 None?

除了“唯一”。但让我以另一种方式重申这一点:over的主要优点是类型安全。它确保您不会向可能不存在的对象发送方法,因为编译器不会让您这样做。Option[T]TT

您说在这两种情况下都必须测试可空性,但是如果您忘记了——或者不知道——你必须检查空值,编译器会告诉你吗?或者你的用户会?

当然,由于它与 Java 的互操作性,Scala 像 Java 一样允许空值。因此,如果您使用 Java 库,如果您使用写得不好的 Scala 库,或者如果您使用写得不好的个人Scala 库,您仍然必须处理空指针。

Option我能想到的另外两个重要优点是:

  • 文档:方法类型签名将告诉您是否始终返回对象。

  • 一元可组合性。

后者需要更长的时间才能完全理解,并且不太适合简单的示例,因为它仅在复杂代码上显示了它的优势。所以,我将在下面举一个例子,但我很清楚,除了已经得到它的人之外,它几乎没有任何意义。

for {
  person <- getUsers
  email <- person.getEmail // Assuming getEmail returns Option[String]
} yield (person, email)
于 2010-01-17T02:26:16.407 回答
31

比较:

val p = getPerson1 // a potentially null Person
val favouriteColour = if (p == null) p.favouriteColour else null

和:

val p = getPerson2 // an Option[Person]
val favouriteColour = p.map(_.favouriteColour)

在 Scala 中作为map函数出现的一元属性bind允许我们对对象进行链式操作,而不必担心它们是否为“空”。

把这个简单的例子更进一步。假设我们想找到一个人列表中所有最喜欢的颜色。

// list of (potentially null) Persons
for (person <- listOfPeople) yield if (person == null) null else person.favouriteColour

// list of Options[Person]
listOfPeople.map(_.map(_.favouriteColour))
listOfPeople.flatMap(_.map(_.favouriteColour)) // discards all None's

或者也许我们想找到一个人的父亲的母亲的妹妹的名字:

// with potential nulls
val father = if (person == null) null else person.father
val mother = if (father == null) null else father.mother
val sister = if (mother == null) null else mother.sister

// with options
val fathersMothersSister = getPerson2.flatMap(_.father).flatMap(_.mother).flatMap(_.sister)

我希望这能阐明选择如何让生活更轻松一些。

于 2010-01-17T01:10:39.790 回答
22

差异是微妙的。请记住,要成为真正的函数,它必须返回一个值 - 从这个意义上说,null 并不真正被视为“正常返回值”,更多的是一种底部类型/无。

但是,在实际意义上,当你调用一个可选地返回某些东西的函数时,你会这样做:

getPerson2 match {
   case Some(person) => //handle a person
   case None => //handle nothing 
}

当然,你可以用 null 做类似的事情——但这使得调用的语义getPerson2很明显,因为它返回的事实Option[Person](一个很好的实用的东西,除了依赖某人阅读文档并获得 NPE,因为他们不阅读文档)。

我会尝试挖掘一个函数式程序员,他可以给出比我更严格的答案。

于 2010-01-16T23:02:11.947 回答
15

对我来说,使用 for comprehension 语法处理选项非常有趣。以synesso前面的例子:

// with potential nulls
val father = if (person == null) null else person.father
val mother = if (father == null) null else father.mother
val sister = if (mother == null) null else mother.sister

// with options
val fathersMothersSister = for {
                                  father <- person.father
                                  mother <- father.mother
                                  sister <- mother.sister
                               } yield sister

如果有任何赋值是NonefathersMothersSister则将是None但不会NullPointerException提出。然后,您可以放心地fathersMothersSister将 Option 参数传递给一个函数。所以你不检查 null 也不关心异常。将此与synesso示例中提供的 java 版本进行比较。

于 2010-01-17T09:48:50.907 回答
9

你有非常强大的组合能力选项:

def getURL : Option[URL]
def getDefaultURL : Option[URL]


val (host,port) = (getURL orElse getDefaultURL).map( url => (url.getHost,url.getPort) ).getOrElse( throw new IllegalStateException("No URL defined") )
于 2010-01-18T09:35:46.353 回答
8

也许其他人指出了这一点,但我没有看到:

使用 Option[T] 与 null 检查进行模式匹配的一个优点是 Option 是一个密封类,因此如果您忽略对 Some 或 None 情况进行编码,Scala 编译器将发出警告。编译器有一个编译器标志,可以将警告转化为错误。因此,可以防止在编译时而不是在运行时处理“不存在”的情况失败。与使用空值相比,这是一个巨大的优势。

于 2010-08-01T23:37:18.313 回答
7

它不是为了帮助避免空检查,而是强制进行空检查。当您的类有 10 个字段(其中两个可能为空)时,这一点就很清楚了。您的系统还有 50 个其他类似的类。在 Java 世界中,您尝试使用某种思维能力、命名约定甚至注释的组合来防止这些领域的 NPE。每个 Java 开发人员都在很大程度上失败了。Option 类不仅使任何试图理解代码的开发人员在视觉上清楚地看到“可空”值,而且允许编译器强制执行这个以前不言而喻的约定。

于 2010-01-17T15:02:49.827 回答
6

[从丹尼尔斯皮瓦克的评论中复制]

如果唯一的使用Option方法是模式匹配以获取值,那么是的,我同意它根本没有改善 null。但是,您缺少其功能的 *huge* 类。使用它的唯一令人信服的理由Option是如果您正在使用它的高阶实用程序函数。实际上,您需要使用它的一元性质。例如(假设有一定量的 API 修整):

val row: Option[Row] = database fetchRowById 42
val key: Option[String] = row flatMap { _ get “port_key” }
val value: Option[MyType] = key flatMap (myMap get)
val result: MyType = value getOrElse defaultValue

那里,不是很漂亮吗?如果我们使用 for-comprehensions,我们实际上可以做得更好:

val value = for {
row <- database fetchRowById 42
key <- row get "port_key"
value <- myMap get key
} yield value
val result = value getOrElse defaultValue

你会注意到我们*从不*明确检查 null、None 或任何类似的东西。Option 的全部意义在于避免任何检查。您只需将计算串起来并向下移动,直到您*真的*需要得到一个值。此时,您可以决定是否要进行显式检查(您永远不必这样做)、提供默认值、抛出异常等。

我从来没有,从来没有对Option. David Pollak 前几天向我提到,他使用这种显式匹配 on Option(或者 Box,在 Lift 的情况下)作为编写代码的开发人员不完全理解该语言及其标准库的标志。

我并不是要成为一个巨魔锤子,但在你抨击它们无用之前,你真的需要看看语言特性在实践中是如何*实际*使用的。我绝对同意 Option 在*你*使用它时非常不引人注目,但你并没有按照它的设计方式使用它。

于 2010-07-29T14:07:15.293 回答
6

这里似乎没有其他人提出的一点是,虽然您可以有一个空引用,但 Option 引入了一个区别。

那是你可以拥有的Option[Option[A]],这将是居住的NoneSome(None)并且Some(Some(a))a通常的居民之一A。这意味着,如果您有某种容器,并且希望能够在其中存储空指针并将它们取出,您需要传回一些额外的布尔值,以了解您是否真的取出了一个值。Java 容器 API中比比皆是,一些无锁变体甚至无法提供它们。

null是一次性的构造,它不会自行组合,它仅可用于引用类型,并且它迫使您以非完全的方式进行推理。

例如,当您检查

if (x == null) ...
else x.foo()

您必须在整个else分支中随身携带,x != null并且已经检查过。但是,当使用类似选项时

x match {
case None => ...
case Some(y) => y.foo
}

知道y 不是None凭空建造的——null如果不是因为 Hoare 的十亿美元错误,你就会知道它也不是。

于 2011-03-26T01:10:47.850 回答
3

添加到 Randall 的答案预告片中Option,理解为什么潜在的值缺失是由 Option如果一个表示不存在一个值为 null 的值,则该缺席 - 存在区别不能参与其他单子类型共享的合同。

如果您不知道 monad 是什么,或者如果您没有注意到它们在 Scala 库中是如何表示的,那么您将看不到与之相关的内容Option,也看不到您错过了什么。使用而不是 null 有很多好处,Option即使在没有任何 monad 概念的情况下也是值得注意的(我在此处的“选项成本 / Some vs null”scala-user 邮件列表线程中讨论了其中一些但在谈论它的隔离有点像谈论一个特定的链表实现的迭代器类型,想知道为什么它是必要的,同时错过了更通用的容器/迭代器/算法接口。这里也有一个更广泛的界面在起作用,Option

于 2010-01-17T00:38:35.483 回答
3

Option[T] 是一个单子,当您使用高阶函数来操作值时,它非常有用。

我建议您阅读下面列出的文章,它们是向您展示 Option[T] 为何有用以及如何以函数方式使用它的非常好的文章。

于 2010-01-18T00:32:52.517 回答
2

空返回值仅用于与 Java 兼容。否则,您不应使用它们。

于 2010-01-16T23:11:36.973 回答
2

我认为在 Synesso 的回答中找到了关键:Option 主要不是用作 null 的繁琐别名,而是作为一个成熟的对象,可以帮助您解决逻辑问题。

null 的问题在于缺少对象。它没有可以帮助您处理它的方法(尽管作为语言设计者,如果您真的喜欢它,您可以向您的语言添加越来越长的功能列表来模拟对象)。

正如您所展示的,Option 可以做的一件事是模拟 null;然后,您必须测试异常值“None”而不是异常值“null”。如果你忘记了,无论哪种情况,坏事都会发生。选项确实降低了意外发生的可能性,因为您必须输入“get”(这应该提醒您它可能为空,呃,我的意思是无),但这是换取额外包装对象的一个​​小好处.

Option 真正开始显示其力量的地方是帮助您处理我想要的东西但我实际上没有的概念。

让我们考虑一些您可能想要对可能为空的事情做的事情。

如果你有一个空值,也许你想设置一个默认值。让我们比较一下 Java 和 Scala:

String s = (input==null) ? "(undefined)" : input;
val s = input getOrElse "(undefined)"

代替有点麻烦的 ?: 构造,我们有一个方法来处理“如果我为空,则使用默认值”的想法。这会稍微清理您的代码。

也许只有当你有一个真正的价值时,你才想创建一个新对象。比较:

File f = (filename==null) ? null : new File(filename);
val f = filename map (new File(_))

Scala 稍微短一些,并且再次避免了错误来源。然后考虑当您需要将事物链接在一起时的累积收益,如 Synesso、Daniel 和范式的示例所示。

这不是一个巨大的改进,但是如果您将所有内容相加,那么在任何地方都值得这样做,以节省非常高性能的代码(您希望避免创建 Some(x) 包装器对象的微小开销)。

匹配用法本身并没有那么有用,除非它是一种提醒您有关 null/None 情况的设备。当你开始链接它时,它真的很有帮助,例如,如果你有一个选项列表:

val a = List(Some("Hi"),None,Some("Bye"));
a match {
  case List(Some(x),_*) => println("We started with " + x)
  case _ => println("Nothing to start with.")
}

现在,您可以将 None 案例和 List-is-empty 案例一起折叠在一个方便的语句中,该语句准确地提取出您想要的值。

于 2010-01-17T16:56:44.810 回答
1

这确实是一个编程风格的问题。使用函数式 Java,或者通过编写自己的辅助方法,您可以拥有 Option 功能,但不会放弃 Java 语言:

http://functionaljava.org/examples/#Option.bind

仅仅因为 Scala 默认包含它并不能使它特别。该库中提供了函数式语言的大多数方面,并且它可以与其他 Java 代码很好地共存。就像您可以选择使用 null 来编写 Scala 一样,您也可以选择不使用 null 来编写 Java。

于 2010-07-30T05:11:18.743 回答
0

事先承认这是一个油嘴滑舌的答案, Option 是一个单子。

于 2010-01-16T23:44:46.997 回答
0

其实我和你分享这个疑问。关于选项,我真的很困扰 1) 存在性能开销,因为每个地方都创建了许多“某些”包装器。2)我必须在我的代码中使用很多 Some 和 Option 。

因此,要查看这种语言设计决策的优缺点,我们应该考虑替代方案。由于 Java 只是忽略了可空性问题,因此它不是替代方案。实际的替代方案提供了 Fantom 编程语言。那里有可空类型和不可空类型,还有?。?: 运算符而不是 Scala 的 map/flatMap/getOrElse。我在比较中看到以下项目符号:

期权的优势:

  1. 更简单的语言 - 不需要额外的语言结构
  2. 与其他单子类型一致

Nullable 的优点:

  1. 典型情况下更短的语法
  2. 更好的性能(因为您不需要为 map、flatMap 创建新的 Option 对象和 lambda)

所以这里没有明显的赢家。还有一个注意事项。使用 Option 没有主要的语法优势。您可以定义如下内容:

def nullableMap[T](value: T, f: T => T) = if (value == null) null else f(value)

或者使用一些隐式转换来获得带有点的原始语法。

于 2010-01-17T22:13:04.673 回答
0

具有显式选项类型的真正优势在于,您可以在 98% 的地方使用它们,从而静态地排除空异常。(在另外 2% 中,类型系统会提醒您在实际访问它们时正确检查。)

于 2012-12-18T19:10:43.043 回答
-3

Option 起作用的另一种情况是类型不能具有空值的情况。不能将 null 存储在 Int、Float、Double 等值中,但通过 Option,您可以使用 None。

在 Java 中,您需要使用这些类型的盒装版本(整数,...)。

于 2012-12-18T17:41:37.580 回答