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

mis*_*tor 82 java monads null functional-programming scala

我无法理解Option[T]Scala中的课程要点.我的意思是,我不能看到任何advanages Nonenull.

例如,考虑代码:

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 */
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

现在假设,该方法getPerson1返回null,然后display对第一行的调用main绑定失败NPE.同样,如果getPerson2返回None,则display调用将再次失败并出现类似的错误.

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

更新:

我根据@Mitch的建议编辑了我的代码.我仍然无法看到任何特别的优势Option[T].我必须测试特殊情况nullNone两种情况.:(

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

Dan*_*ral 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
Run Code Online (Sandbox Code Playgroud)

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

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


你在这里钉了它:

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

除了"唯一".但让我重申,以另一种方式:在主要的优势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)
Run Code Online (Sandbox Code Playgroud)

  • "强迫自己永远不要使用`get`" - >所以,换句话说:"你不要"得到它!" :) (5认同)

Syn*_*sso 31

相比:

val p = getPerson1 // a potentially null Person
val favouriteColour = if (p == null) p.favouriteColour else null
Run Code Online (Sandbox Code Playgroud)

有:

val p = getPerson2 // an Option[Person]
val favouriteColour = p.map(_.favouriteColour)
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

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

// 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)
Run Code Online (Sandbox Code Playgroud)

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

  • 我认为你的意思是flatMap,而不是map. (6认同)
  • 不.如果人是无(或父亲,母亲或姐妹),那么父亲妈妈姐妹将是无,但不会抛出任何错误. (5认同)
  • val favouriteColour = if(p == null)p.favouriteColour else null //恰好是Option帮助您避免的错误!这个答案已经存在多年,没有人发现这个错误! (2认同)

Mic*_*ale 22

差异很微妙.请记住,要真正成为一个函数,它必须返回一个值 - 在这个意义上,null实际上并不被认为是"正常的返回值",更多的是底层类型 /没有.

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

getPerson2 match {
   case Some(person) => //handle a person
   case None => //handle nothing 
}
Run Code Online (Sandbox Code Playgroud)

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

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

  • 在大多数使用它的语言中,Option类型的重点是,而不是运行时空异常,而是获得编译时类型错误 - 编译器可以知道在使用数据时没有对None条件的操作,是一个类型错误. (6认同)

par*_*tic 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
Run Code Online (Sandbox Code Playgroud)

如果任何分配是None,fathersMothersSister将会是,None但不会NullPointerException被提出.然后,您可以安全地传递fathersMothersSister给一个采用Option参数的函数,而不必担心.所以你不检查null,你不关心异常.将其与synesso示例中提供的java版本进行比较.

  • "对于Scala中的"理解"本质上是Haskell中的"do",它们不仅限于列表,你可以使用任何实现的东西:def map [B](f:A => B):C [B] def flatMap [B](f:A => C [B]):C [B] def滤波器(p:A =>布尔值):C [A].IOW,任何monad (11认同)
  • 令人遗憾的是,在Scala中,`< - `语法仅限于"列表理解语法",因为它与Haskell中更通用的`do`语法或Clojure的monad库中的`domonad`形式完全相同.将它绑在列表上卖得很短. (3认同)
  • @seh我赞成@ GClaramunt的评论,但我不能强调他的观点.在Scala中for-comprehensions和列表之间没有*连接 - 除了后者可用于前者.我推荐你http://stackoverflow.com/questions/1052476/can-someone-explain-scalas-yield. (2认同)

Vik*_*ang 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") )
Run Code Online (Sandbox Code Playgroud)


小智 8

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

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


Ada*_*ung 7

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


mis*_*tor 6

[临摹此评论丹尼尔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
Run Code Online (Sandbox Code Playgroud)

在那里,不是那么漂亮吗?如果我们使用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
Run Code Online (Sandbox Code Playgroud)

您会注意到我们*从不*明确检查null,None或其任何类似的东西.Option的重点是避免任何检查.你只需将计算字符串串联起来并向下移动,直到你真的*需要得到一个值.在这一点上,你可以决定你是否愿意做明确的检查(你应该永远要做的),提供默认值,抛出异常等.

我从来没有做过任何明确的匹配Option,而且我知道很多其他Scala开发人员都在同一条船上.前几天David Pollak向我提到他使用这种显式匹配Option(或者 Box,在Lift的情况下)作为编写代码的开发人员不完全理解语言及其标准库的标志.

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

  • 我想你误解了斯卡拉的理解.第二个例子显然不是一个循环,它由编译器翻译成一系列flatMap操作 - 按照第一个例子. (4认同)

Edw*_*ETT 6

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

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

null 是一种一次性的结构,它不与自身构成,它只适用于参考类型,它迫使你以非完全的方式推理.

例如,当你检查

if (x == null) ...
else x.foo()
Run Code Online (Sandbox Code Playgroud)

你必须在整个else分支中随身携带x != null并且已经检查过了.但是,在使用类似选项的东西时

x match {
case None => ...
case Some(y) => y.foo
}
Run Code Online (Sandbox Code Playgroud)

知道 y不是None建筑 - 而且你知道它也不null是,如果不是因为Hoare 十亿美元的错误.