选项[T]类有什么意义?

时间:2010-01-16 22:39:26

标签: java scala null functional-programming monads

我无法理解Scala中Option[T]类的观点。我的意思是,我无法看到None超过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返回None,则display调用将再次失败并出现类似错误。

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

更新

我根据@Mitch的建议编辑了我的代码。我仍然无法看到Option[T]的任何特定优势。在这两种情况下,我都必须测试异常nullNone。 :(

如果我从@Michael's reply正确理解,Option[T]的唯一优势是它明确告诉程序员此方法可以返回无吗?这是设计选择背后的唯一原因吗?

18 个答案:

答案 0 :(得分:71)

如果你强迫自己永远不要使用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]的唯一优势   它明确地告诉了   程序员,这种方法可以   没有?

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

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

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

我能想到的Option的其他两个重要优点是:

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

  • Monadic可组合性。

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

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

答案 1 :(得分: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 ,它作为 map 函数出现在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)

我希望这能说明选择如何让生活变得更轻松。

答案 2 :(得分:21)

差异很微妙。请记住,要真正成为一个函数,必须返回一个值 - 在这个意义上,null实际上并不被认为是“正常的返回值”,更多的是bottom type /无。

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

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

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

我会尝试挖掘一个功能强大的程序员,他能给出比我更严格的答案。

答案 3 :(得分: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

如果任何分配为None,则fathersMothersSister将为None,但不会引发NullPointerException。然后,您可以安全地将fathersMothersSister传递给采用选项参数的函数,而无需担心。所以你不检查null,你不关心异常。将其与 synesso 示例中提供的java版本进行比较。

答案 4 :(得分: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") )

答案 5 :(得分:8)

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

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

答案 6 :(得分:7)

它没有帮助避免空检查,它是强制空检查。当你的类有10个字段时,这一点就变得清晰了,其中两个字段可能为null。您的系统还有50个其他类似的类。在Java世界中,您尝试使用mental horesepower,命名约定或甚至注释的某些组合来阻止这些字段上的NPE。每个Java开发人员都在这方面失败了很多。 Option类不仅使任何试图理解代码的开发人员在视觉上清楚地看到“可空”值,而且允许编译器强制执行此先前未说明的合同。

答案 7 :(得分:6)

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

那就是Option[Option[A]] NoneSome(None)Some(Some(a))a,其中Anull的常住居民之一{1}}。这意味着如果你有某种容器,并希望能够在其中存储空指针并将它们取出,你需要传回一些额外的布尔值来知道你是否真的得到了一个值。像这样的疣在java容器API中比比皆是,而一些无锁的变体甚至无法提供它们。

if (x == null) ... else x.foo() 是一种一次性构造,它不会与自身构成,它只适用于参考类型,它会迫使你以非完全的方式推理。

例如,当您检查

else

你必须在x != null x match { case None => ... case Some(y) => y.foo } None分支中随身携带并且已经检查过了。但是,当使用类似选项

的东西时
null

知道 y不是{{1}}构造 - 而且你知道它不是{{1}},如果它不是Hoare的{{1}} billion dollar mistake }}

答案 8 :(得分:6)

[从this comment复制Daniel Spiewak]

  

如果使用Option的唯一方法是   模式匹配,以获得   价值观,然后是的,我同意它   总是没有改善null。   但是,你错过了一个*巨大的*类   它的功能。唯一的   使用Option的令人信服的理由是   如果你正在使用它的高阶   效用函数。实际上,你   需要使用它的monadic性质。   例如(假设一定数量   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 - 推导:

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

你会注意到我们*从不*   显式检查null,无或   任何类似的。整点   选择是避免任何这种情况   检查。你只是字符串计算   沿着这条线走下去直到你   *真的*需要获得一个价值。在   那一点,你可以决定是否   不是你想做明确的检查   (你应该从不必须这样做),   提供一个默认值,抛出一个   例外等。

     

我永远不会做任何明确的匹配   反对Option,我知道很多   其他Scala开发人员   同一条船。大卫波拉克提到了   就在我使用的另一天   Option上的这种显式匹配(或   Box,在Lift的情况下)作为标志   编写代码的开发人员   不完全理解语言   及其标准库。

     

我不是故意成为巨魔锤,而是   你真的需要看看如何   语言功能*实际上*使用   在你将它们抨击之前的实践中   无用。我绝对同意   选项非常不具吸引力*你*   使用它,但你没有使用它   它的设计方式。

答案 9 :(得分:3)

添加到Randall的teaser of an answer,了解为什么Option表示可能缺少某个值需要了解哪些Option与Scala中的许多其他类型共享,特别是类型建模monad 。如果一个代表缺少null值,则缺席 - 存在区分不能参与其他monadic类型共享的契约。

如果您不知道monad是什么,或者您没有注意到它们在Scala库中的表现方式,您将看不到Option一起播放的内容,您看不到你错过了什么。即使在没有任何monad概念的情况下,使用Option而不是null也有很多好处,这是值得注意的(我在“Option of Option / Some vs null” scala-user <中讨论了其中的一些) / em>邮件列表线程here),但谈论它隔离有点像谈论特定的链表实现的迭代器类型,想知道为什么它是必要的,一直错过更通用的容器/迭代器/算法接口。这里还有一个更广泛的界面,Option提供了该界面的存在和缺席模型。

答案 10 :(得分:3)

选项[T]是一个monad,当你使用高阶函数来操作值时它非常有用。

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

答案 11 :(得分:2)

我认为密钥可以在Synesso的答案中找到:选项主要用作null的繁琐别名,但作为一个完整的对象,可以帮助你解决逻辑问题。< / p>

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

正如您所演示的,Option可以做的一件事就是模仿null;然后你必须测试非常值“无”而不是非常值“null”。如果你忘了,在任何一种情况下,都会发生不好的事情。 Option确实不太可能偶然发生,因为你必须输入“get”(这应该提醒你可能 null,呃,我的意思是没有),但这是一个小的好处换取额外的包装对象。

选项真正开始显示它的力量正在帮助你处理我想要的东西的概念,但我实际上没有一个。

让我们考虑一些你可能想做的事情,可能是空的。

如果您有空值,可能要设置默认值。让我们比较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和范例中的示例所示。

这不是一个巨大的改进,但是如果你把所有内容都添加起来,除了非常高性能的代码之外,它在任何地方都是值得的(你想要避免创建一些代码的微小开销) (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 case和List-is-empty案例一起折叠,从而精确地提取出您想要的值。

答案 12 :(得分:1)

仅返回Null返回值以与Java兼容。否则你不应该使用它们。

答案 13 :(得分:1)

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

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

仅仅因为Scala默认包含它并不会使它变得特别。功能语言的大多数方面都可以在该库中使用,它可以与其他Java代码很好地共存。就像您可以选择使用空值编写Scala一样,您可以选择在没有它们的情况下编写Java。

答案 14 :(得分:0)

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

答案 15 :(得分:0)

其实我和你有同感。关于Option它真的困扰我1)有一个性能开销,因为每一个都创建了一些“一些”包装器。 2)我必须在我的代码中使用很多Some和Option。

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

选项的优势:

  1. 更简单的语言 - 无需其他语言结构
  2. 与其他monadic类型的制服
  3. Nullable的优势:

    1. 典型案例中的语法较短
    2. 更好的性能(因为您不需要为map,flatMap创建新的Option对象和lambdas)
    3. 所以这里没有明显的赢家。还有一点需要注意。使用Option没有主要的语法优势。您可以定义类似的内容:

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

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

答案 16 :(得分:0)

事先承认这是一个明智的答案,Option是一个monad。

答案 17 :(得分:-3)

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

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