选项[ T ]类的意义是什么?

我不能理解 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,那么在 main的第一行上对 display进行的调用一定会因为 NPE而失败。类似地,如果 getPerson2返回 None,则 display调用将再次失败,并出现类似的错误。

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

更新:

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

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

10861 次浏览

区别很微妙。请记住,如果 必须的返回一个值,那么它就是一个真正的函数——在这个意义上,null 并不被认为是一个“正常的返回值”,更确切地说,它是一个 底式/nothing。

但是,从实际意义上讲,当你调用一个可选返回值的函数时,你会:

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

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

我将尝试挖掘一个函数式程序员谁可以给出一个比我更严格的答案。

空返回值只是为了与 Java 兼容而存在。否则不应该使用它们。

事先承认这是一个油嘴滑舌的答案,Option 是一个单子。

除了 Randall 的 一个令人捉摸不透的答案之外,要理解为什么 Option表示潜在的值缺失,还需要理解 Option与 Scala 中的许多其他类型有什么共同之处ーー特别是类型建模单子(monads)。如果一个值表示空值的缺失,那么缺失与存在的区别就不能参与到其他一元类型共享的契约中。

如果你不知道单子是什么,或者你没有注意到它们在 Scala 的库中是如何表示的,你就不会看到 Option是如何工作的,你也不会看到你错过了什么。使用 Option而不使用 null 有很多好处,即使在没有单子概念的情况下也是值得注意的(我在 Scala 用户邮件列表线程 给你中讨论了其中的一些) ,但是谈论它的隔离就像谈论一个特定的链表实现的迭代器类型,想知道为什么它是必要的,同时错过了更一般的容器/迭代器/算法接口。这里还有一个更广泛的接口,Option提供了该接口的存在与不存在模型。

比较:

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)

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

我希望这能给我们带来一些启示,让我们的生活更轻松一些。

如果你强迫自己永远不要使用 get,你会得到更好的 Option点。这是因为 get相当于“ OK,把我送回空地”。

那么,就拿你的例子来说吧。如果不使用 get,你将如何调用 display? 这里有一些替代方案:

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 没有告诉你应该如何编写代码。它可能会温和地刺激你,但如果你想退回到没有安全网的状态,那是你的选择。


你说对了:

选项[ T ]的唯一优点是 它明确地告诉 程序员,这种方法可以 返回无?

除了“唯一”。但是让我用另一种方式重申: Option[T]相对于 T总台优势是类型安全。它确保您不会向可能不存在的对象发送 T方法,因为编译器不允许您这样做。

您说过在这两种情况下都必须测试是否为空,但是如果您忘记——或者不知道——必须检查 null,编译器会告诉您吗?或者你的用户会吗?

当然,由于 Scala 与 Java 的互操作性,它和 Java 一样允许使用 null。因此,如果你使用 Java 库,如果你使用写得很糟糕的 Scala 库,或者如果你使用写得很糟糕的 私事 Scala 库,你仍然需要处理空指针。

我能想到的 Option的另外两个重要优势是:

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

  • 一元可组合性。

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

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

对我来说,处理理解语法时的选项是非常有趣的。以 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传递给一个带有 Option 参数的函数,而不必担心。所以不检查 null,也不关心异常。将其与 Synesso示例中提供的 java 版本进行比较。

它不是用来帮助避免空检查的,而是用来强制执行空检查的。当类有10个字段(其中两个可能为空)时,这一点就变得清楚了。您的系统还有50个其他类似的类。在 Java 世界中,您可以尝试使用一些脑力、变数命名原则或者甚至注释的组合来阻止这些字段上的 NPE。而且每个 Java 开发人员在这方面都失败了很大程度。Option 类不仅使“ nullable”值对于任何试图理解代码的开发人员在视觉上变得清晰,而且允许编译器强制执行这个以前没有说出来的约定。

我认为问题的关键在于 Synesso 的回答: Option 是 没有,它主要用作 null 的一个麻烦的别名,但是作为一个成熟的对象,它可以帮助你解决逻辑问题。

Null 的问题在于它是对象的 缺乏。它没有可能帮助你处理它的方法(尽管作为一个语言设计者,你可以在你的语言中添加越来越长的特性列表,如果你真的喜欢的话,这些特性可以模拟一个对象)。

Option 可以做的一件事就是模拟 null; 然后必须测试非常值“ Nothing”,而不是非常值“ null”。如果你忘了,不管是哪种情况,都会发生不好的事情。选项确实降低了意外发生的可能性,因为您必须键入“ get”(这应该提醒您它是 也许吧 null,呃,我的意思是 Nothing) ,但是这对于交换额外的包装器对象是一个小的好处。

Option 真正开始展示它的力量的地方,是帮助你处理“我想要某样东西但是我实际上没有”的概念。

让我们考虑一些您可能希望用可能为空的事情来做的事情。

也许你想设置一个缺省值,如果你有一个空值。让我们比较一下 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 和聚合的例子所示。

它不是 很大的改进,但是如果您将所有内容加起来,那么在任何地方保存高性能代码都是非常值得的(在这种情况下,您希望避免创建 Some (x)包装器对象的微小开销)。

匹配用法本身并没有多大帮助,只是作为一种提醒您有关 null/Nothing 情况的设备。当你开始链接它的时候,它才是真正有用的,例如,如果你有一个选项列表:

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 为空的大小写,这个语句可以精确地提取您想要的值。

事实上,我和你一样怀疑。关于选项,它真的困扰我,1)有一个性能开销,因为有一个“一些”包装器创建无处不在。2)我必须在我的代码中使用大量的 Some 和 Option。

So to see advantages and disadvantages of this language design decision we should take into consideration alternatives. As Java just ignores the problem of nullability, it's not an alternative. The actual alternative provides Fantom programming language. There are nullable and non-nullable types there and ?. ?: operators instead of Scala's map/flatMap/getOrElse. I see the following bullets in the comparison:

选择的优势:

  1. 更简单的语言-不需要额外的语言结构
  2. 与其他单音节类型一致

Nullable 的优势:

  1. 典型情况下较短的句法
  2. 更好的性能(因为不需要为 map、 latMap 创建新的 Option 对象和 lambdas)

所以这里没有明显的赢家。还有一个音符。使用 Option 没有主要的语法优势。你可以这样定义:

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

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

选项[ T ]是单子,当您使用高阶函数操作值时,它非常有用。

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

你有相当强大的组合能力与选项:

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

[复制自 此评论 by Daniel Spiewak]

如果使用 Option的唯一方法是 模式匹配,以便获得 价值观,那么是的,我同意 完全没有进步。 但是,你错过了一个很大的课程 它的功能。唯一的 使用 Option的令人信服的理由是 如果你使用它的高阶 效用函数。实际上,你 需要利用它的一元性。 例如(假设一定数量 调整空气污染指数) :

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

你会发现我们从来没有 显式检查是否为空、无或 任何类似的东西 选择就是避免这一切 检查。你只是字符串计算 沿着直线前进,直到你 * really * 需要得到一个值 这一点,你可以决定是否 不是你想做的明确检查 (你应该 永远不会必须这样做) , 提供默认值,则引发 例外情况等。

我从来,从来没有做过任何明确的匹配 对抗 Option我知道很多 其他 Scala 开发人员 同一艘船 David Pollak 提到过 就在前几天 在 Option上进行这样的显式匹配(或 Box,如电梯)作为一个标志 编写代码的开发人员 不能完全理解这种语言 及其标准库。

我不想当巨魔锤子,但是 你真的需要看看 实际使用的语言特征 在实践之前,你猛击他们作为 没用,我完全同意 选择是相当没有吸引力的,因为 * 你 * 用过,但是你没有用 它的设计方式。

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

Http://functionaljava.org/examples/#option.bind

仅仅因为 Scala 默认包含它并不意味着它是特殊的。函数式语言的大多数方面都可以在这个库中找到,并且它可以与其他 Java 代码很好地共存。正如你可以选择使用空值来编写 Scala 程序一样,你也可以选择不使用空值来编写 Java 程序。

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

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

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

也就是说,你可以拥有 Option[Option[A]],它将由 NoneSome(None)Some(Some(a))居住,其中 aA的常住居民之一。这意味着如果您有某种类型的容器,并且希望能够在其中存储空指针,并将它们取出,那么您需要传回一些额外的布尔值,以了解是否实际取出了一个值。像这样的缺点在 java 容器 API 和一些无锁变体甚至不能提供它们的 比比皆是

null是一个一次性的构造,它不与自身组合,它只对引用类型可用,并且它迫使您以一种非总体的方式进行推理。

例如,当你检查

if (x == null) ...
else x.foo()

你必须在你的头整个 else分支进行 x != null和这已经被检查。但是,当使用类似的选项时

x match {
case None => ...
case Some(y) => y.foo
}

如果没有霍尔的 数十亿美元的错误,你就会知道它也不是 null

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

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

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