讲动人的故事,写懂人的代码
Java是一门广受欢迎的编程语言。在2023年JetBrains全球开发者生态问卷调查中,有49%的受访程序员在过去一年中曾使用Java,紧跟JavaScript和Python之后。在本书撰写时,根据JetBrains的统计,程序员使用的Java版本排名前三的分别是Java8、Java17和Java11。
遗憾的是,Java这门广受欢迎的编程语言,长期受空指针异常(null pointer exception)问题的困扰。空指针异常是Java中常见的异常。它发生在程序试图使用一个值为null
的对象引用时。换句话说,当程序员试图通过一个空引用来访问对象的方法或属性时,程序就会抛出这个异常,并中止运行。
什么时候会发生空指针异常?下面这些场景就会发生。调用null
对象的方法。访问或修改null
对象的字段。将null
作为throw
语句的参数。使用null
对象进行同步(synchronized
)。访问或修改数组的元素,而数组引用为null
。
Java语言在发展过程中对空指针异常处理经历了一个逐步完善和改进的过程。
在Java语言早期(JDK 1.0 ~ 1.4),空指针异常是Java程序中常见的运行时异常。需要程序员编写类似下面的防御性代码对对象引用进行null
检查,以避免可能导致的空指针异常。
String value = null;
if (value != null && value.length() > 0) {
// ...
}
但大量的null
检查导致代码可读性下降,增加了出错的可能性。
Java5引入了@Nullable
和@NotNull
等注解,在编译期标识和检查可能为null
的引用。但这需要第三方工具如Findbugs或IDEA等的支持。这虽然提高了代码的可读性和健壮性,但需要额外的工具支持。
Java6和7没有引入与null
相关的新特性,空指针异常仍是Java程序员要面临的问题。虽然Guava等第三方类库提供了Optional
类等机制来封装null
,但未被纳入JDK。
受函数式编程启发,Java 8引入了Optional
类。Optional
类用来封装可能为null
的对象引用,提供了一系列方法避免显式的null
检查。Optional
是一个容器对象,通过类似Stream的方式来处理null
,使得代码更加简洁,如下面两行用于获取可能为null
的字符串长度的代码所示。
1 Optional<String> value = Optional.ofNullable(null);
2 int length = value.map(String::length).orElse(0);
第1行创建了一个Optional
对象,用于表示一个值存在或不存在。Optional.ofNullable()
是一个静态方法,它接受一个可能为null
的值,并返回一个Optional
对象。如果传入的参数不为null
,它会返回一个包含该值的Optional
对象。如果传入的参数为null
(就像这个例子),它会返回一个空的Optional
对象。在这个例子中,我们传入了null
,所以value
是一个空的Optional
对象。
第2行使用了Optional
的方法来安全地获取字符串的长度,即使原始值为null
。value
是一个 Optional<String>
类型的对象。map
方法接受一个函数作为参数,这个函数会被应用到 Optional
内部的值上(如果 Optional
不为空的话)。String::length
是一个方法引用,它指向 String
类的 length()
方法。当 map
被调用时,它会做以下操作。如果 value
包含一个非空的字符串,它会调用这个字符串的 length()
方法,并将结果包装在一个新的 Optional<Integer>
中。如果 value
是空的(在这个例子中确实是空的),map
会返回一个空的 Optional<Integer>
。
之后在这个 Optional<Integer>
上调用 .orElse(0)
方法。orElse
方法起如下作用。如果Optional
包含一个值,它会返回这个值。如果 Optional
为空,它会返回作为参数提供的默认值(在这个例子中是 0
)。无论是从 Optional
中提取的值,还是默认值,都是 int
类型的。最后,这个 int
值被赋给 length
变量。所以,转换为 int
类型实际上发生在 orElse
方法被调用的时候。这个过程是自动的,不需要显式的类型转换。
这种方法可以有效地避免空指针异常,同时提供了一个优雅的方式来处理可能为null
的值。在实际编程中,这种模式非常有用,特别是在处理可能不存在的值时。
Java9到Java14进一步完善和增强Optional
,如增加了or()
和ifPresentOrElse()
等方法,并在某些情况下进一步简化了对null
的处理。
Java 15引入sealed class来增强Java类层次,限制类的继承。这在一定程度上可以减少由null
引起的未知子类型的问题。
总的来说,Java对空指针异常的处理经历了从无到有,从局部到系统,从隐式到显式的发展过程。尽管引入了Optional
等机制,但null
引用和空指针异常仍是Java程序员需要面对的问题。未来Java有望通过引入新的类型系统特性如sealed class,配合pattern match等,进一步减少由null
引起的问题,但彻底消除null
是不现实的。
为了避免空指针异常,Java程序员可以进行遵循下面这些良好实践。在使用对象之前进行null
检查。使用Java 8引入的Optional
类。使用空对象设计模式。在适当的地方使用断言(assertion)。
Optional
的误用假设程序员使用了Java8引入的Optional
类,但未遵循良好实践而进行了误用,此时Java编译器能否提醒程序员修复这个误用?答案是不能,如代码清单2-1所示。
代码清单2-1 Java编译器无视对Optional的误用
1 package org.example;
2
3 import java.util.Optional;
4
5 public class OptionalMisuseExample {
6 public static Optional<String> getName(int id) {
7 // 模拟数据库查询
8 if (id == 1) {
9 return Optional.of("Alice");
10 } else {
11 return Optional.empty();
12 }
13 }
14
15 public static void printName(int id) {
16 Optional<String> name = getName(id);
17 // 错误使用:直接调用get()而不检查是否存在值
18 System.out.println("Name: " + name.get());
19 }
20
21 public static void main(String[] args) {
22 printName(1); // 这会正常工作
23 printName(2); // 这会抛出 NoSuchElementException
24 }
25 }
// Output of './gradlew run':
// > Task :app:run FAILED
// Exception in thread "main" Name: Alice
// java.util.NoSuchElementException: No value present
// at java.base/java.util.Optional.get(Optional.java:143)
// at org.example.OptionalMisuseExample.printName(OptionalMisuseExample.java:18)
// at org.example.OptionalMisuseExample.main(OptionalMisuseExample.java:23)
//
// FAILURE: Build failed with an exception.
代码清单2-1相应的没有行号的代码在github代码库中文件夹位置为book_LRBACP/ch02/null_pointer_fixer_java_optional_misuse。
这里所讨论的Java踩坑Optional
误用,以及之后的Rust避坑空指针异常,并不是暗示Java不如Rust好,而仅仅是为了提升自学者入门Rust的动力而已。Java让程序员不必操心指针的复杂性,是广受欢迎的编程语言。
如何运行代码清单2-1中的Java代码?
❓如何运行代码清单2-1中的Java代码?
最省事的方法是把没有行号的代码,复制粘贴到网页https://www.mycompiler.io/new/java左侧。然后删除第1行package
语句,把第5行OptionalMisuseExample
类名改为Main
。之后点击右上Run按钮,即可运行,并在右侧看到运行结果。
如果你想在本地电脑运行,那么可以使用下面的方法(为了节省篇幅,下面只提供要点。细节可以询问你最爱用的生成式AI聊天工具)。
gradle init --type java-application --dsl kotlin
google-java-format -replace ./app/src/main/java/org/example/App.java
(需要把App.java换成实际的文件名)./gradlew compileJava
,这会在app/build文件夹中编译源文件./gradlew build
,这会在app/build文件夹中构建jar包./gradlew run
./app/src/main/java/org/example
文件中创建新文件OptionalMisuseExample.java
,再重复上面的格式化代码、检查语法错误、编译和运行步骤代码清单2-1这段Java代码,主要展示了对Optional
的误用,以及Java编译器如何无视这种误用。代码模拟了一个根据ID查询名字的场景,突出了在使用Optional
时直接调用get()
方法而不先检查值是否存在的潜在危险。
第1-3行定义包名并导入Optional类。
第5行定义名为OptionalMisuseExample
的公共类。
第6-13行定义静态方法getName
,接受一个整数ID并返回Optional<String>
。第7-12行模拟数据库查询。如果ID为1,返回包含"Alice"的Optional
;否则返回空Optional
。
第15-19行定义静态方法printName
。第16行调用getName
方法获取Optional<String>
。第18行错误地使用Optional
,直接调用get()
方法而不检查值是否存在。
第21-24行是主方法。第22行调用printName(1)
,这会正常工作。第23行调用printName(2)
,这会抛出NoSuchElementException
。
在运行命令./gradlew compileJava
进行编译时,系统显示编译构建成功。但当运行命令./gradlew run
运行代码时,系统输出表明,当调用printName(1)
时,程序正常输出"Name: Alice"。当调用printName(2)
时,程序抛出NoSuchElementException
异常。
这个输出清楚地展示了Java编译器无视对Optional
的误用。尽管在printName
方法中直接调用get()
而不检查值是否存在是一个潜在的错误,但编译器并没有给出任何警告。只有在运行时,当尝试从空Optional
中获取值时,才会抛出异常。这会导致bug会成为编译阶段的漏网之鱼,或许一直会隐藏到生产环境才爆发,大大增加了程序员排查和修复bug的难度和压力。
这个例子强调了在使用Optional
时进行适当检查的重要性,以及仅依赖编译时检查是不够的。开发者需要谨慎使用Optional
,确保在调用get()
之前总是先检查值是否存在,以避免运行时异常,如代码清单2-2所示。
代码清单2-2 使用Optional良好实践正确处理空值的Java9代码
1 package org.example;
2
3 import java.util.Optional;
4
5 public class OptionalGoodPracticesExample {
6 public static Optional<String> getName(int id) {
7 // 模拟数据库查询
8 if (id == 1) {
9 return Optional.of("Alice");
10 } else {
11 return Optional.empty();
12 }
13 }
14
15 public static void printName(int id) {
16 getName(id)
17 .ifPresentOrElse(
18 name -> System.out.println("Name: " + name),
19 () -> System.out.println("No name found for id: " + id));
20 }
21
22 public static void main(String[] args) {
23 printName(1); // 这会打印名字
24 printName(2); // 这会打印未找到名字的消息
25 }
26 }
// Output:
// Name: Alice
// No name found for id: 2
代码清单2-2相应的没有行号的代码在github代码库中文件夹位置为book_LRBACP/ch02/null_pointer_fixer_java_optional_good_practices。
代码清单2-2的第15-20行printName
方法,就是与代码清单2-1的最大差异。
第16行调用getName
方法,传入id
参数,返回一个Optional<String>
对象。
第17行 .ifPresentOrElse
是Optional
类的一个方法,用于处理Optional
对象可能存在或不存在的两种情况。
第18行 name -> System.out.println("Name: " + name),
,这是ifPresentOrElse
方法的第一个参数,是一个 lambda 表达式。这里的 "->
" 是 Java 8 引入的 lambda 表达式语法。在这个上下文中,它代表一个函数接口的简写形式。箭头左边的 name
是参数。它代表 Optional
中的值,如果存在的话。箭头右边是当 Optional
有值时要执行的代码。可以将这行代码理解为一个简化的匿名函数,完整形式大致如下。
(String name) -> {
System.out.println("Name: " + name);
}
第19行: () -> System.out.println("No name found for id: " + id));
,这里的 "()
" 同样是 lambda 表达式的一部分。空括号 "()
" 表示这个 lambda 表达式没有参数。这是因为当 Optional
为空时,不需要任何输入参数就可以执行相应的代码。这行代码的完整形式大致如下。
() -> {
System.out.println("No name found for id: " + id);
}
这两个 lambda 表达式分别处理了 Optional
中有值和无值的两种情况。代码尝试通过给定的 id
获取一个名字,如果找到了就打印这个名字,如果没找到就打印一条未找到的消息。这使得代码更加简洁和富有表现力。这是 Java 8 及以后版本中函数式编程特性的一个很好的例子。
Option
不修复不罢休Rust如何避坑类似上面的”Java编译器无视对Optional
的误用“的情况?
答案是Rust编译器会报告误用Option
的错误情况。注意,这是错误,而不是警告。在Rust编译代码时,程序员可以无视警告而继续执行代码,但不能无视错误。程序员若无视错误,继续执行命令cargo run
来执行代码,那么会看到相同的编译错误报告,而无法运行程序。所以Rust编译器在此处也起到“不修复不罢休”的“护栏”作用,能在编译阶段有效地要求程序员修复空值的bug,不仅避免了程序抛出空指针所导致的异常,还能大幅缩短后期很晚才在生产环境发现bug所造成的返工时长,有效减少程序员修bug的工作压力,如代码清单2-3所示。
代码清单2-3 Rust编译器对误用Option
不修复不罢休
1 fn get_name(id: i32) -> Option<String> {
2 // 模拟数据库查询
3 if id == 1 {
4 Some(String::from("Alice"))
5 } else {
6 None
7 }
8 }
9
10 fn print_name(id: i32) {
11 // 直接使用 get_name(id) 的结果,而不进行匹配
12 println!("Name: {}", get_name(id));
13 }
14
15 fn main() {
16 print_name(1);
17 print_name(2);
18 }
// // Output of 'cargo build'
// error[E0277]: `Option<String>` doesn't implement `std::fmt::Display`
// --> src/main.rs:12:26
// |
// 12 | println!("Name: {}", get_name(id));
// | ^^^^^^^^^^^^ `Option<String>` cannot be formatted with the default formatter
// |
// = help: the trait `std::fmt::Display` is not implemented for `Option<String>`
// = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
// = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
//
// For more information about this error, try `rustc --explain E0277`.
// error: could not compile `null_pointer_fixer_rust_option_misuse` (bin "null_pointer_fixer_rust_option_misuse") due to 1 previous error
代码清单2-3相应的没有行号的代码在github代码库中文件夹位置为book_LRBACP/ch02/null_pointer_fixer_rust_option_misuse。
代码清单2-3的主要功能是演示了 Rust 编译器对于 Option
类型的严格处理,体现了Rust 编译器对误用 Option
不修复不罢休的原则。代码试图通过 ID 获取名字并打印,但在处理 Option
时存在错误,导致编译失败。
第1-8行定义了 get_name
函数。它的函数签名,接受一个 i32
类型的 id
,返回 Option<String>
类型。
什么是Option<String>
类型?Option<String>
是 枚举类型Option<T>
的一个具体实例,其中泛型参数 T
被具体类型 String
替代。Option<String>
继承了 Option<T>
的所有基本特性和方法,如 Some
、None
、unwrap()
、map()
等。Option<String>
专门用于处理可能存在或不存在的字符串值,提供了针对 String
类型的类型安全。
什么是Option<T>
?什么是枚举类型?
❓什么是Option<T>
?什么是枚举类型? Option<T>
是 Rust 标准库中的一个枚举类型,定义如下。
enum Option<T> {
Some(T),
None,
}
这里的 T
是一个泛型参数,可以是任何类型。关键字enum
定义了枚举类型Option<T>
。
Option<T>
能帮程序员避的最大的坑,是空指针解引用(null pointer dereference)。许多编程语言允许将null
赋值给任何引用类型。程序员可能忘记检查空值,直接使用可能为null的引用,导致运行时错误。这是最危险的bug,因为它可能导致程序崩溃和安全漏洞,在某些系统中会造成严重的后果。Rust使用Option<T>
来表示可能存在或不存在的值。程序员必须显式处理Some
和None
两种情况。编译器会强制程序员处理None
的情况,从而在编译时就避免了空指针解引用。
枚举类型(Enum)是Rust中一种强大的数据类型,它允许程序员定义一个类型,该类型可以是几种可能的变体(variant)之一。每个变体可以有不同的类型和关联的数据。
上面Option<T>
的定义展示了枚举类型的下面几个关键特征。关键字enum
用于定义枚举类型。枚举可以有泛型参数,如这里的<T>
。这使得枚举可以适用于不同的类型。枚举定义了多个变体,在这个例子中是Some(T)
和None
两个变体。变体可以携带数据。Some(T)
变体包含了类型为T
的值,表示有值;而None
不包含任何数据,表示无值。
Option<T>
的设计体现了枚举类型的核心优势,即它可以表示一个值要么存在(Some
),要么不存在(None
)。这种表达方式比使用空值更加安全和明确。
使用枚举类型,程序员可以进行以下操作。用模式匹配来处理不同的变体。在一个类型中表示多种可能性。将数据与它的变体关联起来。
枚举类型是Rust类型系统的一个强大特性。它使得代码更加安全,表达力更强,并能在编译时捕获许多潜在的错误。
Option<T>
适用以下场景。函数可能失败或没有返回值时。数据结构中的可选字段。初始化可能失败的资源。作为函数参数,表示该参数是可选的。在并发编程中安全地共享可能不存在的数据。
第2-7行模拟数据库查询。如果 id 为 1,返回变体 Some(String::from("Alice"))
;否则返回变体 None
。
String::from("Alice")
会创建一个新的 String
类型的实例,其内容与 "Alice"
相同。这个过程涉及到内存分配和数据复制。"Alice"
作为字符串字面量,通常存储在程序的只读数据段。新创建的 String
实例会在堆上分配内存,复制 "Alice"
的内容,并管理这块内存。新创建的 String
实例拥有其内容的所有权。这意味着当 String
实例超出作用域时,它会自动释放其持有的内存。
::
操作符是路径分隔符。它用于访问模块中的项,或者调用关联函数(类似于其他语言中的静态方法)。from
方法是 String
类型上的一个关联函数。它是 From
trait 的一部分,这个 trait 用于类型转换。"Alice"
是一个字符串字面量。
String::from("Alice")
与 "Alice".to_string()
在功能上是等价的。它比使用 String::new()
后再 .push_str("Alice")
更简洁。
对于短字符串,String::from
通常会预分配稍多一些的内存,以优化后续可能的增长操作。
当需要一个可变的字符串,或者需要获取字符串的所有权时,通常会使用 String::from
,如下面代码实例所示。
fn main() {
let s = String::from("Alice");
println!("Name: {}", s); // 输出:Name: Alice
// s 现在拥有 "Alice" 的所有权,我们可以修改它
let mut s = s; // 使 s 可变
s.push_str(" Smith");
println!("Full name: {}", s); // 输出:Full name: Alice Smith
}
String::from("Alice")
是在 Rust 中创建一个新的、可变的、拥有所有权的字符串的常用方法,它从字符串字面量创建了一个 String
实例。
什么是String
类型?什么是字符串字面量?
❓什么是String
类型?什么是字符串字面量?
在 Rust 中,String
是一个可变的、拥有数据所有权的、UTF-8 编码的字符串类型。它总是存储在堆上,这意味着它可以动态增长和缩小。String
适用于需要拥有或修改字符串数据的场景。
String
能帮程序员避的最大的坑,是内存安全问题。它通过所有权系统和自动内存管理,有效防止了悬垂指针、缓冲区溢出等常见的内存相关bug。
在 Rust 中,字符串字面量的类型是 &str
,它可以视作一个字符串切片,通常是静态的且不可变的。&
表示这是一个不可变引用,str
是 Rust 的原生字符串类型。
Rust 的原生字符串类型str
,也称为字符串切片。它是不可变的、固定大小的 UTF-8 字节序列的引用。str
通常以 &str
形式使用,可以指向栈、堆或静态内存中的字符串数据。str
没有所有权,通常以引用(&str
)的形式来借用。str
适用于借用字符串数据,如函数参数和字符串字面量。
第2-7行为何没有return
语句就能返回值?
❓为何没有return
语句就能返回值?
在 Rust 中,函数的最后一个表达式的值会被隐式地作为函数的返回值。这就是为什么在代码清单2-3中第3-7行,没有看到 return
关键字,但函数仍然能够返回值。因为第3-7行是一个 if-else 表达式,而表达式的结果就是函数的返回值。注意,表达式后面是没有分号的。
Rust 是一种表达式导向的语言,这意味着大多数构造都是表达式并有一个值。这就是为什么最后一个表达式可以作为返回值的原因。
Rust 确实有 return
语句。它可以用于显式地从函数中返回值,尤其是在函数的中间部分提前返回时。
第10-13行定义了 print_name
函数。第10行是函数签名,接受一个 i32
类型的 id
。
第12行直接使用 get_name(id)
的结果进行打印,没有对 Option
进行匹配或处理。
第15-18行是main 函数,分别调用 print_name
函数,传入参数 1 和 2。
从代码后面运行cargo build
命令的编译器报错信息能够看出, Option<String>
类型不能直接用于格式化字符串,因为它没有实现 std::fmt::Display
trait。这如何体现Rust编译器能帮程序员避坑Option
未处理空值情况?
Rust编译器虽然没有直接提示“未处理空值情况”,但它会阻止直接使用Option<T>
的进行打印的行为,这是遗忘处理空值很常见的场景。想来Rust应该是为此而故意让Option<T>
没有实现Display
trait。
这个编译错误体现了 Rust 的安全理念:强制开发者正确处理可能为空的值,避免在运行时出现未定义行为。Rust 编译器通过这种方式确保代码的健壮性和安全性,不允许开发者忽视 Option
类型可能为 None
的情况。
那在Rust中正确处理Option<T>中空值情况的代码该如何写?是否还有踩坑的地方?
为了处理Option<T>
中的空值,Rust为程序员提供了4种方法。其中前3种是推荐做法,最后1种会踩坑而不推荐,如代码清单2-4所示。
代码清单2-4 Rust提供了4种处理Option<T>
中空值的方法
1 fn get_name(id: i32) -> Option<String> {
2 // 模拟数据库查询
3 if id == 1 {
4 Some(String::from("Alice"))
5 } else {
6 None
7 }
8 }
9
10 fn print_name_by_match(id: i32) {
11 match get_name(id) {
12 Some(name) => println!("Name: {}", name),
13 None => println!("No name found for id: {}", id),
14 }
15 }
16
17 fn print_name_by_if_let(id: i32) {
18 if let Some(name) = get_name(id) {
19 println!("Name: {}", name);
20 } else {
21 println!("No name found for id: {}", id);
22 }
23 }
24
25 fn print_name_by_unwrap_or(id: i32) {
26 let name_unknown = get_name(id).unwrap_or(String::from("Unknown"));
27 let name_for_id = get_name(id).unwrap_or_else(|| format!("No name found for id: {}", id));
28 println!("Name: {}", name_unknown);
29 println!("Name: {}", name_for_id);
30 }
31
32 fn print_name_by_unwrap_not_recommended(id: i32) {
33 println!("Name: {}", get_name(id).unwrap());
34 }
35
36 fn main() {
37 print_name_by_match(1); // 这会打印名字
38 print_name_by_match(2); // 这会打印未找到名字的消息
39
40 print_name_by_if_let(1); // 这会打印名字
41 print_name_by_if_let(2); // 这会打印未找到名字的消息
42
43 print_name_by_unwrap_or(1); // 这会打印名字
44 print_name_by_unwrap_or(2); // 这会打印未找到名字的消息
45
46 print_name_by_unwrap_not_recommended(1); // 这会打印名字
47 print_name_by_unwrap_not_recommended(2); // 这会触发panic
48 }
// Output:
// Name: Alice
// No name found for id: 2
// Name: Alice
// No name found for id: 2
// Name: Alice
// Name: Alice
// Name: Unknown
// Name: No name found for id: 2
// Name: Alice
// thread 'main' panicked at src/main.rs:33:39:
// called `Option::unwrap()` on a `None` value
// note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
代码清单2-4相应的没有行号的代码在github代码库中文件夹位置为book_LRBACP/ch02/null_pointer_fixer_rust_good_practices。
代码清单2-4展示了Rust所提供的下面4种处理Option<T>
中空值的方法。
match
模式匹配表达式
第10-15行,是使用match
模式匹配表达式处理Option<String>
的两个变体的方式。
这种方式适合下面的场景。当需要对Some
和None
两种情况分别处理时。当需要提取Option
中的值并在代码中使用时,如第12行的Some
变体中name
的值。当逻辑较为复杂,需要针对不同情况执行不同代码时。
match
表达式提供了完整的模式匹配,能够清晰地处理所有可能的情况。它的穷尽性检查确保了所有情况都被考虑到,提高了代码的健壮性。注意,因为match
模式匹配是表达式,所以第10-15行这段代码里没有分号。
if let
语法
第17-23行,是使用if let
语法重点关注Some
变体的方式。if let
语法是一种模式匹配的简化形式,用于只关心一种匹配情况的场景。
这种方式适合下面的场景。当只关心Some
情况,或者Some
和None
的处理逻辑较为简单时。当不需要绑定None
中的值时。当避免使用更冗长的match
表达式,使代码更简洁时。
if let
提供了一种更简洁的方式来处理只关心一种模式的情况。它特别适合于只需要处理Some
情况,而None
情况的处理较为简单的场景。比如第17-23行这段代码中甚至都没出现None
关键字。
None
提供默认值的unwrap_or
方法
第25-30行,是使用能为None
提供默认值的unwrap_or
方法。unwrap_or
是定义在Option
类型上的方法。它能提供一种安全的方式来获取Option
中的值,同时指定一个默认值,以防值不存在或发生错误。如果Option
是Some(value)
,则返回value
;如果是None
,返回提供的默认值。第26行,unwrap_or
方法提供的默认值是字符串”Unknown”。但如果想要在默认值中添加id
号,那么就需要使用能包含闭包的unwrap_or_else
方法。第27行的|| format!("No name found for id: {}", id)
就是一个闭包。
闭包是一种可以捕获其周围环境中变量的匿名函数。在Rust中,闭包使用 |参数| 表达式
的语法。闭包的主要目的是创建一个可以在需要时执行的小型、局部的功能单元。它们常用于函数式编程模式,如高阶函数的参数。闭包能帮程序员避的最大的坑,是代码重复。在这个例子中,如果get_name
返回None
,那么闭包就提供了一种优雅的方式来生成默认值,而不需要编写额外的if-else语句。第27行的闭包,已经从周围环境捕获了id,所以就不需要参数,闭包中的||
,就表示没有参数。
这种方法适合下面的场景。当需要一个简单的默认值时。在不关心具体错误原因的情况下。快速原型开发时。
unwrap_or_else
允许我们为None
情况提供一个惰性求值的默认值。这在默认值计算比较复杂或者我们想要避免在Some
情况下不必要的计算时特别有用。
unwrap
方法
第32-34行,是使用在生产环境不推荐的unwrap
方法。与unwrap_or
一样,unwrap
也是定义在Option
类型上的方法。如果Option
是Some(value)
,则返回value
;如果是None
,则会引发panic。
panic是Rust中的一种错误处理机制,用于处理不可恢复的错误。当程序遇到无法继续执行的情况时,会触发panic。panic一般发生在下面的场景。显式调用panic!
宏。访问数组越界。整数除以零。使用unwrap()
方法处理None
值或Err
结果。
当panic发生时,程序会打印错误信息,开始展开(unwind)调用栈,清理资源(如释放内存),最终终止程序或当前线程,就如同代码清单2-4最后输出注释中的panic出错信息所显示的那样。
这种方法只适合下面的场景。在确定Option
一定是Some
的情况下使用。在原型开发或测试代码中快速获取值。在确实希望程序在遇到None
时崩溃的情况下使用。
unwrap
方法在遇到None
时会导致程序panic
。在生产代码中,这通常是不可接受的,因为它可能导致程序崩溃。使用unwrap
通常被认为是一种不安全和不优雅的编程实践,因为代码没有合理地处理错误情况。
Rust的编译器能帮助程序员避坑类似Java空指针异常那样的bug,那程序员在用Rust编程时,对于变量的使用,会踩什么坑?
如果喜欢这篇文章,别忘了给文章点个“在看”,好鼓励小吾继续写哦~😃
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。