什么是类的选项?
我无法理解Scala中的Option[T]
类。 我的意思是,我无法看到任何超过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
,那么在NPE
第一行display
的调用就会失败。 同样,如果getPerson2
返回None
,则display
调用将再次失败并出现类似的错误。
如果是这样,那么为什么Scala通过引入一个新的值包装( Option[T]
)而不是遵循Java中使用的简单方法来使事情复杂化?
更新:
我根据@Mitch的build议编辑了我的代码。 我仍然无法看到Option[T]
任何特别的优点。 在这两种情况下,我必须testing特殊的null
或None
。 🙁
如果我从@Michael的回答中得到了正确的理解, Option[T]
的唯一优点是它明确告诉程序员这个方法可以返回None吗? 这是deviseselect背后的唯一原因吗?
如果你强迫自己永远不会使用get
,那么你会更好地Option
。 那是因为get
相当于“好吧,把我送回空地”。
所以,以你的例子。 如何在不使用get
情况下调用display
? 这里有一些select:
getPerson2 foreach (_.display) for (person <- getPerson2) person.display getPerson2 match { case Some(person) => person.display case _ => } getPerson2.getOrElse(Person("Unknown", 0)).display
这些替代品都不会让你在不存在的东西上display
。
至于为什么get
存在,斯卡拉不会告诉你如何写你的代码。 它可能轻轻刺激你,但如果你想回落到没有安全网,这是你的select。
你钉在这里:
Option [T]的唯一好处是它明确告诉程序员这个方法可以返回None吗?
除了“唯一”。 但是让我以另一种方式重申一下: Option[T]
优于T
的主要优点是types安全。 它确保您不会将T
方法发送给可能不存在的对象,因为编译器不会让您。
你说你必须在两种情况下都testing可空性,但如果你忘了 – 或者不知道 – 你必须检查null,编译器会告诉你吗? 或者将你的用户?
当然,由于它与Java的互操作性,Scala允许像Java一样的空值。 所以,如果您使用Java库,如果使用写得不好的Scala库,或者如果使用写得不好的个人 Scala库,您仍然必须处理空指针。
Option
I的其他两个重要优点可以考虑的是:
-
文档:方法types签名会告诉你一个对象是否总是被返回。
-
Monadic可组合性。
后者需要更长的时间才能充分理解,并不适合于简单的例子,因为它只能显示复杂代码的优势。 所以,我将在下面举一个例子,但是我很清楚,除了那些已经获得它的人以外,这并不意味着什么。
for { person <- getUsers email <- person.getEmail // Assuming getEmail returns Option[String] } yield (person, email)
比较:
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属性绑定 ,它出现在Scala中作为map函数,允许我们在对象上链接操作,而不用担心它们是否为“null”。
稍微进一步讲一下这个简单的例子。 假设我们想find所有人喜欢的颜色。
// 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
也许我们想find一个人的父亲的母亲的妹妹的名字:
// 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)
我希望这可以让我们了解一些select如何让生活变得更轻松。
差别是微妙的。 记住要成为一个真正的函数,它必须返回一个值 – 在这个意义上,null并不真正被认为是一个“正常的返回值”,更多的是一个底部types /什么也不是。
但是,从实际意义上讲,当你调用一个可选的返回值时,你可以这样做:
getPerson2 match { case Some(person) => //handle a person case None => //handle nothing }
当然,你可以用null来做类似的事情 – 但是这使得调用getPerson2
的语义显而易见,因为它返回Option[Person]
(这是一个很好的实际的东西,除了依靠某人阅读文档并获得NPE,因为他们不要阅读文档)。
我会尽力挖掘一个function强大的程序员,他能给出比我更严格的答案。
对于我来说,用理解语法来处理选项是非常有趣的。 以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
如果任何fathersMothersSister
为None
,那么fathersMothersSister
将是None
但不会引发NullPointerException
。 然后,您可以安全地将fathersMothersSister
传递给采用Option参数的函数,而无需担心。 所以你不检查null,你不关心的例外。 将它与synesso示例中提供的java版本进行比较。
你有非常强大的select组成function:
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") )
也许有人指出这一点,但我没有看到它:
与Option [T]与空检查模式匹配的一个优点是Option是一个密封的类,所以如果忽略代码Some或None的情况下,Scala编译器会发出警告。 编译器有一个编译器标志,会将警告转化为错误。 所以可以避免在编译时而不是在运行时处理“不存在”的情况。 与使用空值相比,这是一个巨大的优势。
这不是在帮助避免一个空检查,它是在那里强制空检查。 当你的class级有10个字段,其中两个可能为空时,这一点就变得清晰了。 而你的系统有50个其他类似的类。 在Java世界里,你试图通过脑力活动,命名约定或者甚至注释的组合来防止那些领域的NPE。 而且每个Java开发人员在这方面都失败了很多。 Option类不仅让任何试图理解代码的开发人员清楚地看到“可空”值,而且允许编译器强制执行这个以前没有说出来的合同。
[由Daniel Spiewak的 评论复制]
如果使用
Option
的唯一方法是模式匹配,以获得值,那么是的,我同意它不会改善超过null。 但是,你错过了一大类function。 使用Option
的唯一有说服力的理由是如果你使用它的高阶实用function。 有效地,你需要使用它的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
那里,这不是很漂亮吗? 如果我们使用-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或者其他类似的结果。 期权的全部是避免任何检查。 你只需要计算一些string,然后向下移动,直到你真的需要得到一个值。 此时,您可以决定是否要进行显式检查(您不应该这样做),提供默认值,抛出exception等。
我从来没有做过任何明确的匹配
Option
,我知道很多其他的Scala开发者在同一条船上。 大卫·波拉克(David Pollak)前几天向我提到,他在Option
(或者在Box
的情况下,在Lift的情况下)使用了这样的显式匹配,表示编写代码的开发人员不完全理解语言及其标准库。我不是要成为一个巨魔锤子,但是你真的需要看看语言特征在实践中如何被实际使用,然后再把它们打成无用的。 我绝对同意,Option是相当不可靠的,因为你使用它,但是你没有按照它的devise方式使用它。
有一点似乎没有人在这里提出的是,虽然你可以有一个空引用,有一个区别引入的选项。
也就是说,你可以有Option[Option[A]]
,其中Some(None)
和Some(Some(a))
,其中a
是A
的常用居民之一。 这意味着如果你有某种types的容器,并且希望能够存储空指针,并把它们取出来,你需要传回一些额外的布尔值来知道你是否真的得到了一个值。 像这样的瑕疵在java容器API中比比皆是 ,一些无锁变体甚至不能提供它们。
null
是一次性构造,它不是自己构成的,只能用于引用types,并且迫使你以非全面的方式进行推理。
例如,当你检查
if (x == null) ... else x.foo()
你必须在整个else
分支中携带x != null
,并且这已经被检查过了。 但是,使用类似选项时
x match { case None => ... case Some(y) => y.foo }
如果不是由于霍尔的十亿美元的错误 ,你知道你的build造并不是None
– 而且你也知道它也不是null
。
Option [T]是一个monad,当你使用高阶函数来操作数值的时候,它是非常有用的。
我build议你阅读下面列出的文章,他们是很好的文章,告诉你为什么选项[T]是有用的,它如何在function上使用。
- 火星人对Monad:无视为有害的
- Monads是大象第1部分
再加上Randall 提供的答案 ,了解为什么潜在的价值缺失是由Option
来表示的,需要了解哪些Option
与Scala中的许多其他types共享,尤其是typesbuild模monad。 如果一个表示没有null的值,那么缺席存在区别就不能参与其他一元types共享的契约。
如果你不知道单子是什么,或者你没有注意到它们是如何在Scala的图书馆中呈现出来的,那么你就不会看到Option
跟着玩什么,而且你也看不到你错过了什么。 使用Option
而不是null值有很多好处,即使在没有monad概念的情况下也是值得注意的(我在这里讨论其中的一些“选项/一些vs null” scala用户邮件列表线程),但是谈话关于它的隔离有点像谈论一个特定的链表实现的迭代器types,想知道为什么它是必需的,一直遗漏在更一般的容器/迭代器/algorithm接口上。 这里也有一个更广泛的接口,而Option
提供了该接口的存在和不存在模型。
我认为在Synesso的答案中可以find关键:Option作为null的一个麻烦的别名主要不是有用的,而是作为一个完整的对象,可以用你的逻辑来帮助你。
null的问题是它缺less一个对象。 它没有任何方法可以帮助你处理它(尽pipe作为一个语言devise者,如果你真的喜欢它,你可以为你的语言添加越来越多的function列表,模仿一个对象)。
正如你所说的,Option可以做的一件事是模拟null; 那么你必须testing非常值“无”,而不是非常值“空”。 如果你忘记了,无论如何,坏事都会发生。 选项确实使它不太可能偶然发生,因为你必须键入“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(_))
斯卡拉略短,再次避免了错误的来源。 那么当你需要把事情联系在一起的时候,请考虑一下累积的好处,正如Synesso,Daniel和范例所示。
这并不是一个巨大的改进,但是如果你把所有的东西都加了起来,那么除了创buildSome(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.") }
现在,您可以将“无”情况和“空列表”情况一起放在一个方便的语句中,该语句可以精确地提取所需的值。
这真是一个编程风格的问题。 使用Functional Java或者编写自己的帮助器方法,可以获得选项function,但不会放弃Java语言:
http://functionaljava.org/examples/#Option.bind
只是因为Scala默认包含它并不是特别的。 函数式语言的大多数方面都可以在该库中使用,并且可以与其他Java代码很好地共存。 就像你可以select用空值来编程Scala一样,你可以select不用它们来编程Java。
空返回值仅用于与Java兼容。 否则不应该使用它们。
事先承认这是一个口齿不清的答案,Option是一个monad。
其实我跟你有一个疑问 关于选项它真的困扰我,1)有一个性能开销,因为有一些“一些”包装创造everywehre。 2)我必须在我的代码中使用很多的Some和Option。
所以要看这个语言devise决定的优点和缺点,我们应该考虑替代scheme。 由于Java只是忽略了可空性的问题,所以不是一种替代scheme。 实际的替代scheme提供了Fantom编程语言。 那里有可以为空和不可空的types? ?:运算符而不是Scala的map / flatMap / getOrElse。 我在比较中看到下面的项目符号:
期权的优势:
- 更简单的语言 – 不需要额外的语言结构
- 与其他一元types一致
可空的优点:
- 在典型情况下更短的语法
- 更好的性能(因为您不需要为map,flatMap创build新的Option对象和lambdaexpression式)
所以这里没有明显的赢家。 还有一个笔记。 使用Option没有主要的语法优势。 你可以定义如下的东西:
def nullableMap[T](value: T, f: T => T) = if (value == null) null else f(value)
或者使用一些隐式转换来获得带点的私有语法。
具有显式选项types的真正优点是,您不能在所有地方的98%中使用它们,从而静态地排除空例外。 (另外2%的types系统会提醒你在实际访问时检查是否正确)
Option的另一种情况是在types不能有空值的情况下。 在Int,Float,Double等值中不能存储null,但是使用Option可以使用None。
在Java中,您将需要使用这些types的盒装版本(整数,…)。