Option [T]类有什么意义?


83

我无法理解Option[T]Scala的课程重点。我的意思是,我不能看到任何advanagesNonenull

例如,考虑以下代码:

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。同样,如果getPerson2return None,则display调用将再次失败,并显示类似错误。

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

更新:

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

如果我从@Michael的答复中正确理解了Option[T],那么它的唯一优点是它明确告诉程序员该方法可以返回None?这是此设计选择背后的唯一原因吗?


23
实际上,Option [T]中的“ get”方法的发音是:“为什么不对它进行模式匹配?”
米奇·布莱文斯

2
米奇是对的。尝试重新使用您的示例而不使用get,您会明白的。:-)
Daniel C. Sobral

您有Person p ..这是java .. .try val p = ...此外,还有其他选项,如下面的Daniel和Synesso所示-这里有一些不错的答案。
Michael Neale 2010年

@Michael:糟糕!感谢您的指点;更正它。
missingfaktor

Answers:


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我能想到的其他两个重要优点是:

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

  • Monadic可组合性。

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

for {
  person <- getUsers
  email <- person.getEmail // Assuming getEmail returns Option[String]
} yield (person, email)

5
“强迫自己永远不要使用get”->所以,换句话说:“你不要get!” :)
fredoverflow

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)

monadic属性bind,在Scala中显示为地图函数使我们可以链接对象上的操作,而不必担心它们是否为“ null”。

再举一个简单的例子。假设我们要查找某人列表中所有喜欢的颜色。

// 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)

我希望这可以为选择如何使生活变得更轻松提供一些启示。


在您的最后一个示例中,如果人的父亲为空,该怎么办?map将返回None,并且调用将因某些错误而失败。比这种null方法更好吗?
missingfaktor

5
否。如果人为“无”(或父亲,母亲或姐妹),则“ fathersMothersSister”将为“无”,但不会引发任何错误。
范式

6
我认为您的意思是flatMap,而不是地图。
retronym,2010年

感谢Daniel的编辑。在发布代码之前,我没有尝试过该代码。下次会更好。
Synesso,2010年

2
val favouriteColour = if(p == null)p.favouriteColour else null //正是Option可以帮助您避免的错误!这个答案已经存在多年了,没有人发现这个错误!
Mark Lister

22

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

但是,从实际意义上讲,当调用有选择地返回某些内容的函数时,您会这样做:

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

当然,您可以使用null做类似的事情-但这使调用的语义因其getPerson2返回的事实而变得显而易见Option[Person](这是一件很实际的事情,除了依靠某人阅读文档并获得NPE的原因是,他们不读文档doc)。

我将尝试找出一个能给出比我更严格答案的功能程序员。


1
这也是我对Option的理解。它显式地告诉程序员我们可以得到一个None,如果您愚蠢到足以记住要执行Some(T)但又没有抓住None的话,您就会遇到麻烦。
cflewis 2010年

1
Lewisham-我认为编译器会警告您,因为Some / None会形成代数数据类型(抽象的密封特征...)(但是我要从内存中删除)。
Michael Neale 2010年

6
在使用它的大多数语言中,Option类型的要点是,您会得到一个编译时类型错误,而不是运行时null异常-编译器可以知道您在使用数据时对None条件没有任何操作,这应该是类型错误。
贾斯汀·史密斯

15

对我来说,选项用于理解语法时真的很有趣。以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版本进行比较。


3
可惜的是,在Scala中,<-语法仅限于“列表理解语法”,因为它实际上与doHaskell的更通用语法或domonadClojure的monad库的形式相同。将其绑定到列表可以使它卖空。
SEH

11
Scala中的“用于理解”本质上是Haskell中的“要做”,它们不仅限于列表,您可以使用任何实现的方法:def map [B](f:A => B):C [B] def flatMap [B](f:A => C [B]):C [B] def filter(p:A =>布尔值):C [A]。IOW,任何monad
GClaramunt 2010年

2
@seh我赞成@GClaramunt的评论,但我不能强调他的观点。有没有在斯卡拉,内涵和列表之间的连接-除了后者使用前者。我推荐您访问stackoverflow.com/questions/1052476/…
Daniel C. Sobral

是的,知道没有关系,但是我同意值得指出。我在此答案的第一行评论,其中范式提及“列表理解语法”。与语言设计问题相反,这是一个教学问题。
seh 2010年

9

使用Option,您具有相当强大的合成功能:

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") )

你能完整解释一下吗?
Jesvin Jose

8

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

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


7

它不是用来帮助避免空检查的,它是用来强制进行空检查的。当您的班级有10个字段时,这一点变得很清楚,其中两个字段可以为null。您的系统还有50个其他类似的类。在Java世界中,您尝试通过使用思维强悍,命名约定甚至注释的某种组合来防止这些字段上的NPE。每个Java开发人员都在很大程度上失败了。Option类不仅使尝试理解代码的任何开发人员都可以从视觉上使“可空”值清晰可见,而且允许编译器强制执行此以前未讲过的约定。


6

[临摹此评论丹尼尔Spiewak ]

如果使用的唯一方法Option是模式匹配以获取值,那么是的,我同意,在null方面根本没有改善。但是,您缺少其功能的*巨大*类。唯一有说服力的原因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,而且我知道很多其他Scala开发人员都在同一条船上。大卫·波拉克(David Pollak)前几天对我说,他在Option(或 Box在Lift的情况下)使用这样的显式匹配,以作为编写代码的开发人员不能完全理解该语言及其标准库的标志。

我并不是要成为一个巨魔锤子,但在将bash视为无用之前,您真的需要研究它们在实践中实际上是如何实际使用的。我绝对同意Option *在*您*使用它的过程中并不引人注目,但您并未按照其设计方式使用它。


这里有一个可悲的后果:在运行中没有基于跳转的短路,因此每个后续语句都再次测试Optionfor None。如果将语句写为嵌套条件语句,则每个潜在的“失败”将仅被测试并采取一次行动。在您的示例中,对的结果fetchRowById进行三次有效检查:一次用于指导key初始化,另一次用于指导value,最后一次用于指导result。这是一种优雅的编写方式,但并非没有运行时成本。
SEH

4
我认为您误解了Scala的理解。第二个例子着重不是循环,编译器将其转换为一系列flatMap操作-如第一个例子所示。
凯文·赖特2010年

自从我在这里写评论以来已经很长时间了,但是我刚刚看到了Kevin的。凯文(Kevin),您写“您误会”时指的是谁?我看不出它如何可能是,因为我从来没有提及一个循环什么。
seh 2013年

6

这里似乎没有其他人提出的一点是,尽管您可以使用null引用,但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的数十亿美元错误,您也不知道。


3

Option [T]是monad,当您使用高阶函数来操纵值时,它确实很有用。

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


我将添加到推荐的阅读列表中,这是托尼·莫里斯(Tony Morris)最近发布的教程“莫纳德(Monad)是什么意思?”:projects.tmorris.net/public/what-does-monad-mean/artifacts/1.0/…–
兰德尔·舒尔茨

3

加上兰德尔的预告片,了解为什么用值表示潜在的价值缺失Option需要了解什么Option与Scala中许多其他类型(尤其是建模monad的类型)有什么共同之处。如果一个代表不存在null值,则该不存在与否区分不能参与其他单子类型共享的合同。

如果您不知道什么是单子,或者如果您不注意到它们在Scala的库中的表示方式,那么您将看不到什么Option玩法,也看不到缺少的东西。有使用许多好处Option,而不是零,这将是值得关注的,甚至在没有任何单子的概念(我讨论其中一些在“选项的成本/有些VS空”斯卡拉用户邮件列表线程这里),但说起它的隔离有点像谈论特定的链表实现的迭代器类型,想知道为什么这样做是必需的,而同时又在更通用的容器/迭代器/算法接口上丢失了。这里也有一个更广泛的界面在起作用,Option


非常感谢您提供的链接。这真的很有用。:)
missingfaktor 2010年

您对线程的评论非常简洁,我几乎错过了它的意义。我真的希望可以禁止null。
阿兰·奥德亚

2

我认为关键是在Synesso的答案中找到的:Option主要不是作为麻烦的null别名有用,而是作为可以帮助您理解逻辑的完整对象。

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

正如您所演示的,Option可以做的一件事就是模拟null。然后,您必须测试非常规值“ None”而不是非常规值“ null”。如果您忘记了任何一种情况,都会发生不好的事情。Option确实减少了它偶然发生的可能性,因为您必须键入“ get”(这应该提醒您它可能为null,er,我的意思是“无”),但这对于交换额外的包装器对象来说是很小的好处。 。

Option真正开始发挥作用的地方是帮助您处理“我想要的东西,但我实际上并没有一个”的概念。

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

如果为空,则可能要设置默认值。让我们比较一下Java和Scala:

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

代替一个有点麻烦的?:构造,我们有一个方法处理“如果我为null,请使用默认值”的想法。这样可以稍微清理一下代码。

也许仅当您具有实际价值时才想创建一个新对象。相比:

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

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

这不是一个很大的改进,但是如果您将所有内容加起来,那么在所有地方都保存非常高性能的代码(在此甚至要避免创建Some(x)包装对象的微小开销)就值得这样做。

除了用作提醒您有关空/无情况的设备以外,匹配用法本身并没有太大帮助。当它开始链接时,它才真正有用,例如,如果您有一个选项列表:

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情况一起折叠起来,准确地提取出所需的值。


2

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


1

这确实是一个编程风格的问题。使用Functional Java或编写自己的帮助器方法,可以拥有Option功能,但不能放弃Java语言:

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

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


0

事先承认这是一个简单的答案,Option是一个monad。


我知道那是单子。为什么还要添加一个有问题的“ monad”标签?
missingfaktor 2010年

^以上陈述并不意味着我理解单子是什么。:D
missingfaktor 2010年

4
Monad很酷。如果您不使用它们,或者至少不假装理解,那么您就不酷了;-)
范式

0

其实我和你一样怀疑。关于Option,这的确让我感到困扰:1)性能开销很大,因为每个人都会创建“一些”包装器。2)我必须在代码中使用大量Some和Option。

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

选件的优势:

  1. 语言更简单-无需其他语言构造
  2. 与其他单子类型统一

Nullable的优势:

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

因此,这里没有明显的赢家。还有一点。使用Option没有主要的语法优势。您可以定义如下内容:

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

或使用一些隐式转换来获取带点的漂亮语法。


有没有人对现代VM的性能造成过严格的基准测试?转义分析意味着可以在堆栈上分配许多临时Option对象(比堆便宜得多),并且世代GC相当有效地处理了较少的临时对象。当然,如果速度对您的项目比避免NPE更为重要,则选择可能不适合您。
贾斯汀W 2010年

不要提及没有数字支持的性能开销。在反对诸如Option之类的抽象时,这是一个极为常见的错误。如果您指向或发布了一个基准测试或删除了性能评论,我会很乐意撤消我的反对意见:)
Alain O'Dea 2010年


-3

Option工作的另一种情况是类型不能具有null值。无法在Int,Float,Double等值中存储null,但是使用Option可以使用None。

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.