我无法理解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]
的任何特定优势。在这两种情况下,我都必须测试异常null
或None
。 :(
如果我从@Michael's reply正确理解,Option[T]
的唯一优势是它明确告诉程序员此方法可以返回无吗?这是设计选择背后的唯一原因吗?
答案 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]]
None
,Some(None)
,Some(Some(a))
和a
,其中A
是null
的常住居民之一{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。我在比较中看到了以下项目符号:
选项的优势:
Nullable的优势:
所以这里没有明显的赢家。还有一点需要注意。使用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,...)。