Rust 中的 Option::None 和其他语言中的 null 有什么区别?

M.A*_*ary 6 null rust

我想知道 RustOption::None和其他编程语言中的“null”之间的主要区别是什么。为什么被None认为更好?

Sil*_*olo 27

关于如何没有价值的简短历史教训!

一开始,有 C。在 C 中,我们有这些称为指针的东西。通常,稍后初始化指针或使其成为可选指针很有用,因此 C 社区决定在内存中存在一个特殊位置,通常是内存地址零,我们都同意这意味着“这里没有值” ”。返回的函数T*可以 return NULL(在 C 中用全部大写字母编写,因为它是一个宏)来指示失败或缺少值。在当时的时代,在像 C 这样的低级语言中,这种方法相当有效。我们刚刚从汇编语言的原始状态中升起,进入了类型化(并且相对安全)语言的领域。

C++ 和后来的 Java 基本上模仿了这种方法。在 C++ 中,每个指针都可以NULL(后来nullptr添加,它不是,并且类型安全性稍高NULL)。在 Java 中,问题变得更加清晰:每个非原始值(这基本上意味着每个不是数字或字符的值)都可以是null。如果我的函数采用 aString作为参数,那么我总是必须准备好处理一些天真的年轻程序员向我传递 a 的情况null。如果我String从函数中得到 a 结果,那么我必须检查文档以查看它是否实际上不存在。

这给了我们一个机会NullPointerException,即使在今天,这个网站上每天都会有数十个年轻程序员提出的问题被落入陷阱。

很明显,在现代,“每个值都可能是null”在静态类型语言中并不是一种可持续的方法。(就其本质而言,动态类型语言往往更愿意在每一点上处理失败,因此,例如,nil在 Ruby 或NonePython 中,动态类型语言的存在稍微不那么令人震惊。)

Kotlin 经常被誉为“更好的 Java”,它通过引入可空类型来解决这个问题。现在,并非所有类型都可以为空。如果我有一个String,那么我实际上就有了一个String。如果我希望myString可以为 null,那么我可以选择使用 为 null String?,它是一个字符串或 的类型null。至关重要的是,这是类型安全的。如果我有一个 type 值,那么在进行检查或做出断言之前String?我无法调用它的方法。因此,如果有 type ,那么除非我首先执行以下操作之一,否则我无法执行此操作。StringnullxString?x.toLowerCase()

  1. 将其放入if (x != null), 以确保x不是null(或证明我的情况的其他形式的控制流)。
  2. 使用?空安全调用运算符执行x?.toLowerCase(). 这将编译为一个if (x != null)检查,并返回一个String?is ,null如果原始字符串是null
  3. 使用!!来断言该值不是null。断言经过检查,如果结果是错误的,则会抛出异常。

请注意,(3) 是 Java 每次默认执行的操作。不同之处在于,在 Kotlin 中,“我断言我比类型检查器更了解”的情况是选择加入的,并且您必须不遗余力地进入可以获得空指针的情况例外。(我掩盖了平台类型,这是类型系统中与 Java 互操作的一种方便的 hack。这里并不是真正密切相关)

可空类型是 Kotlin 解决问题的方式,也是 Typescript(使用--strict模式)和 Scala 3(打开空检查)处理问题的方式。然而,如果不对编译器进行重大更改,它在 Rust 中并不真正可行。这是因为可为null 的类型需要语言支持子类型。在像 Java 或 Kotlin 这样首先使用面向对象原则构建的语言中,很容易引入新的子类型。String是 、 的子类型String?,也是Any(本质上java.lang.Object)和 的子类型,也是Any?(任何值 和null)的子类型。因此,像这样的字符串"A"具有主要类型String但它也通过子类型具有所有其他类型。

Rust 并没有这个概念。Rust 有一些用于特征对象的类型强制,但这并不是真正的子类型。说它String是 的子类型 dyn Display是不正确的,只是 a String(在未调整大小的上下文中)可以自动强制转换为 a dyn Display

所以我们需要回到绘图板。可空类型在这里不起作用。不过,幸运的是,还有另一种方法可以处理“这个值可能存在也可能不存在”的问题,它被称为可选类型。我不会冒险猜测哪种语言首先尝试了这个想法,但它肯定是由 Haskell 普及的,并且在更多函数式语言中很常见。

在函数式语言中,我们经常有一个主要类型的概念,类似于 OOP 语言。也就是说,一个值x具有“最佳”类型T。在具有子类型的 OOP 语言中,x可能有其他类型,它们是的超类型T。然而,在没有子类型的函数式语言中,x实际上只有一种类型。还有其他类型可以与统一T,例如 (用 Haskell 表示法编写) forall a. ax但说is的类型并不正确forall a. a

Kotlin 中的整个可空类型技巧依赖于a和 a的"abc"事实,而 while只是 a 。由于我们没有子类型,因此我们需要两个单独的值来表示和情况。StringString?nullString?StringString?

如果我们在 Rust 中有这样的结构

struct Foo(i32);
Run Code Online (Sandbox Code Playgroud)

然后Foo(0)是一个类型的值Foo。时期。故事结局。它不是Foo?, 或可选的Foo, 或类似的东西。它只有一种类型。

然而,有一个相关值称为Some(Foo(0))which is an Option<Foo>。请注意,Foo(0)Some(Foo(0))不是相同的值,它们只是碰巧以相当自然的方式相关。不同之处在于,虽然 aFoo 必须存在,但 anOption<Foo>可以是Some(Foo(0)) 或者可以是 a None,这有点像nullKotlin 中的 our 。None在我们做任何事情之前,我们仍然必须检查该值是否是。在 Rust 中,我们通常通过模式匹配来实现这一点,或者使用为我们进行模式匹配的几个内置函数之一。这是相同的想法,只是使用函数式编程的技术来实现。

因此,如果我们想从一个值中获取值Option,或者如果它不存在则获取默认值,我们可以这样写

my_option.unwrap_or(0)
Run Code Online (Sandbox Code Playgroud)

如果我们想对内部(如果存在)做某事,或者null如果不存在(如果不存在),那么我们会写

my_option.and_then(|inner_value| ...)
Run Code Online (Sandbox Code Playgroud)

这基本上就是?.Kotlin 中所做的事情。如果我们想断言某个值存在并且否则会出现恐慌,我们可以这样写

my_option.unwrap()
Run Code Online (Sandbox Code Playgroud)

最后,我们用于处理此问题的通用瑞士军刀是模式匹配。

match my_option {
  None => {
    ...
  }
  Some(value) => {
    ...
  }
}
Run Code Online (Sandbox Code Playgroud)

因此,我们有两种不同的方法来处理这个问题:基于子类型的可空类型和基于组合的可选类型。“哪个更好”正在形成一些观点,但我将尝试总结我所看到的双方的论点。

可空类型的拥护者倾向于关注人体工程学。能够快速进行“空检查”然后使用相同的值非常方便,而不必不断地跳过解包和包装值的麻烦。能够将文字值传递给期望的函数String?,或者Int?不用担心捆绑它们或不断检查它们是否在 a 中,这也很好Some

另一方面,可选类型的优点是不那么“神奇”。如果 Kotlin 中不存在可为 null 的类型,我们就有点运气不好了。但如果OptionR​​ust 中不存在,有人可以用几行代码自己编写它。它不是特殊的语法,它只是一种存在的类型,并且在其上定义了一些(普通)函数。它也是由合成组成的,这意味着(自然)它的合成更好。也就是说,(T?)?is 相当于T?(前者仍然只有一个null值),而Option<Option<T>>不同于Option<T>。我不建议编写Option<Option<T>>直接返回的函数,但是在编写泛型函数时您最终可能会被这个问题困扰(即您的函数返回S?并且调用者恰好用 实例化SInt?

我在这篇文章中更深入地探讨了差异,其中有人问了基本相同的问题,但用的是 Elm 而不是 Rust。


Sch*_*ern 5

这个问题有两个隐含的假设:首先,其他语言的“空值”(以及nils 和undefs 和nones)都是相同的。他们不是。

\n

第二个假设是“null”和“none”提供类似的功能。null 有许多不同的用途:值未知(SQL 三元逻辑)、值是哨兵(C 的空指针和空字节)、出现错误(Perl 的)undef、以及表示没有值(Rust\'sOption::none)。

\n

Null 本身至少有三种不同的形式:无效值、关键字和特殊对象或类型。

\n

关键字为空

\n

许多语言选择特定的关键字来指示 null。Perl 有undef,Go 和 Ruby 有nil,Python 有None。它们很有用,因为它们不同于 false 或 0。

\n

无效值为 null

\n

与关键字不同,这些是语言中表示特定值的东西,但无论如何都被用作 null。最好的例子是 C 的空指针和空字节。

\n

特殊对象和类型为 null

\n

语言越来越多地使用特殊对象和类型来表示 null。这些具有关键字的所有优点,但不是一般的“出了问题”,它们可以在每个域中具有非常具体的含义。它们还可以是用户定义的,提供超出语言设计者预期的灵活性。而且,如果您使用对象,它们可以附加附加信息。

\n

例如,std::ptr::null在 Rust 中表示空原始指针。Go 有error接口。Rust 有Result::ErrOption::None

\n
\n

空为未知

\n

大多数编程语言使用双值或二进制逻辑:true 或 false。但有些,特别是 SQL,使用三值或三元逻辑:真、假和未知。这里 null 的意思是“我们不知道这个值是什么”。它不是 true,它不是 false,它不是错误,它不是空,它不是零:该值是未知的。这改变了逻辑的工作方式。

\n

如果你将未知与任何事物(甚至其本身)进行比较,结果就是未知的。这使您可以根据不完整的数据得出逻辑结论。例如,爱丽丝身高 7\'4",我们不知道 Bob 有多高。Alice 比 Bob 高吗?未知。

\n

空表示未初始化

\n

当你声明一个变量时,它必须包含一些值。即使在 C 语言中,众所周知,它也没有您初始化变量,它也包含内存中恰好存在的任何值。现代语言将为您初始化值,但它们必须将其初始化为某个值。通常,某些东西是空的。

\n

空作为哨兵

\n

当您有一个事物列表并且需要知道该列表何时完成时,您可以使用哨兵值。这是列表中无效的任何值。例如,如果您有一个正数列表,则可以使用 -1。

\n

典型的例子是C。在C中,你不知道一个数组有多长。你需要一些东西来告诉你停下来。您可以传递数组的大小,也可以使用哨兵值。您读取数组,直到看到哨兵。C 中的字符串只是一个以“空字节”结尾的字符数组,即 0,它是无效字符。如果您有一个指针数组,则可以以空指针结束它。缺点是 1) 并不总是存在真正无效的值,2) 如果你忘记了哨兵,你就会离开数组的末尾,并且会发生不好的事情。

\n

一个更现代的例子是如何知道停止迭代。例如,在 Go 中for thing := range things { ... },当范围内没有更多项导致循环退出时, range, 将返回。虽然这更灵活,但它与经典哨兵有相同的问题:您需要一个永远不会出现在列表中的值。如果 null 是有效值怎么办?nilfor

\n

Python 和 Ruby 等语言选择通过在迭代完成时引发特殊异常来解决此问题。两者都会引发StopIteration它们的循环将捕获的内容,然后退出循环。这避免了选择哨兵值的问题,Python 和 Ruby 迭代器可以返回任何内容。

\n

空作为错误

\n

虽然许多语言使用异常进行错误处理,但有些语言则不然。相反,它们返回一个特殊值来指示错误,通常为空。C、Go、Perl 和 Rust 都是很好的例子。

\n

这与哨兵有同样的问题,您需要使用一个不是有效返回值的值。有时这是不可能的。例如,C 中的函数只能返回给定类型的单个值。如果它返回一个指针,它可以返回一个空指针来指示错误。但如果它返回一个数字,您必须选择一个有效的数字作为错误值。将错误和返回值混为一谈是一个问题。

\n

Go 通过允许函数返回多个值(通常是返回值和error. 然后可以独立检查这两个值。Rust 只能返回单个值,因此它通过返回特殊类型来解决这个问题Result。它包含带有Ok返回值或Err带有错误代码的 。

\n

在 Rust 和 Go 中,它们不仅仅是值,而且可以调用它们的方法来扩展它们的功能。Rust 的Result::Err另一个优点是它是一种特殊类型,你不能意外地将 a 用作Result::Err其他类型。

\n

Null 表示没有值

\n

最后,我们“没有给定的选项”。很简单,有一组有效的选项,但结果却是没有。这与“未知”不同,因为我们知道该值不在有效值集中。例如,如果我问“这辆车是哪种水果”,结果将为 null,这意味着“这辆车不是任何水果”。

\n

当询问集合中某个键的值并且该键不存在时,您将得到“无值”。例如,Rust 的 HashMapget将返回None如果键不存在

\n

这不是一个错误,尽管他们经常感到困惑。例如,ArgumentError如果您向函数传递无意义的内容,Ruby 就会引发异常。例如,array.first(-2)询问前 -2 个值,这是无意义的,并且会引发ArgumentError.

\n
\n

选项与结果

\n

这最终让我们回到了Option::None。它是 null 的“特殊类型”版本,具有许多优点。

\n

Rust 可用于Option多种用途:指示未初始化的值、指示简单错误、无值以及用于 Rust 特定的事物。该文档提供了大量示例

\n
    \n
  • 初始值
  • \n
  • 未在整个输入范围内定义的函数的返回值(部分函数)
  • \n
  • 否则报告简单错误的返回值,其中错误时不返回 None
  • \n
  • 可选结构字段
  • \n
  • 可以借用的结构体字段或 \xe2\x80\x9ctaken\xe2\x80\x9d
  • \n
  • 可选函数参数
  • \n
  • 可空指针
  • \n
  • 摆脱困境
  • \n
\n

在如此多的地方使用它会削弱它作为特殊类型的价值。它也与Result您应该用来从函数返回结果的内容重叠。

\n

我见过的最好的建议是用来Result<Option<T>, E>区分三种可能性:有效结果、无结果和错误。Jeremy Fitzhardinge 提供了一个很好的示例,说明查询键/值存储会返回什么

\n
\n

...我大力提倡返回,Result<Option<T>, E>其中的Ok(Some(value))意思是“这是你要求的东西”,Ok(None)意思是“你要求的东西肯定不存在”,Err(...)意思是“我不能说是否你所要求的东西存在或不存在,因为出了问题”。

\n
\n