unsafe? trait IDENTIFIER GenericParams? ( : TypeParamBounds? )? WhereClause? {
InnerAttribute*
AssociatedItem*
}
-
A
trait
describes an abstract interface that types can implement. -
This interface consists of
associated items
, which come in three varieties. -
Trait declaration defines a trait in the
type namespace
of the module or block where it is located.Associated items
are defined as members of the trait within their respective namespaces.Associated types
are defined in the type namespace.Associated constants
andassociated functions
are defined in the value namespace.
-
All traits define an implicit type parameter
Self
that refers to “the type that is implementing this interface”. -
Traits may also contain additional type parameters. These type parameters, including
Self
, may be constrained by other traits and so forth as usual. -
Traits are implemented for specific types through separate implementations.
-
Trait functions may omit the function body by replacing it with a semicolon. This indicates that the implementation must define the function.
-
If the trait function defines a body, this definition acts as a default for any implementation which does not override it. Similarly, associated constants may omit the equals sign and expression to indicate implementations must define the constant value. Associated types must never define the type, the type may only be specified in an implementation.
-
Trait functions
are not allowed to beconst
.
trait
描述了一个类能实现的抽象接口。- 这个
接口
由一些相关项目
构成,包括:- functions 函数
- types 类型
- constants 常数
- trait 的声明在该声明所在的 module 或 block 中的命名空间定义了一个 trait
- 所有特征都定义了一个隐式类型参数 Self ,它指的是“实现该接口的类型”。特征还可能包含其他类型参数。这些类型参数,包括 Self ,可能会像往常一样受到其他特征等的约束。
- 对一个特定类型的 trait 实现是与 trait 的定义相分开的
- 特征函数可以通过用分号替换来省略函数体。这表明实现必须定义该函数。
- 如果特征函数定义了一个函数体,则此定义将充当任何不覆盖它的实现的默认值。
- 类似地,关联的常量可以省略等号和表达式以指示实现必须定义常量值。
- 关联类型绝不能定义该类型,该类型只能在实现中指定。
- trait function 不能为 const ?
// Examples of associated trait items with and without definitions.
trait Example {
const CONST_NO_DEFAULT: i32;
const CONST_WITH_DEFAULT: i32 = 99;
type TypeNoDefault;
fn method_without_default(&self);
fn method_with_default(&self) {}
}
泛型
项可能会使用 traits 作为 type parameters 的限制(bounds)。
Trait and lifetime bounds provide a way for generic items to restrict which types and lifetimes are used as their parameters. Bounds can be provided on any type in a where clause. There are also shorter forms for certain common cases:
特征和生命周期界限为通用项提供了一种方法来限制将哪些类型和生命周期用作其参数。可以在 where
子句中为任何类型提供界限。对于某些常见情况,还有更简短的形式:
- Bounds written after declaring a generic parameter:
fn f<A: Copy>() {}
is the same asfn f<A>() where A: Copy {}
.- 声明通用参数后写入的界限:
fn f<A: Copy>() {}
与fn f
() where A: Copy {}
。
- In trait declarations as supertraits:
trait Circle : Shape {}
is equivalent totrait Circle where Self : Shape {}
.- 在作为 supertraits 的特征声明中:
trait Circle : Shape {}
相当于trait Circle where Self : Shape {}
。
- In trait declarations as bounds on associated types:
trait A { type B: Copy; }
is equivalent totrait A where Self::B: Copy { type B; }
.- 在特征声明中作为关联类型的边界:
trait A { type B: Copy; }
相当于trait A where Self::B: Copy { type B; }
。
Bounds on an item must be satisfied when using the item. When type checking and borrow checking a generic item, the bounds can be used to determine that a trait is implemented for a type. For example, given Ty: Trait
使用物品时必须满足物品的限制。当类型检查和借用检查通用项时,边界可用于确定是否为类型实现了特征。例如,给定 Ty: Trait
- In the body of a generic function, methods from
Trait
can be called onTy
values. Likewise associated constants on theTrait
can be used.- 在泛型函数体内,可以在 Ty 上调用 Trait 中的方法 价值观。同样可以使用 Trait 上的关联常量。
- Associated types from
Trait
can be used.- 可以使用 Trait 中的关联类型。
- Generic functions and types with a
T: Trait
bounds can be used withTy
being used forT
.- 带有 T: Trait 边界可以与 Ty 一起使用 用于 T 。
A trait object
is an opaque value of another type that implements a set of traits. The set of traits is made up of an object safe base trait
plus any number of auto traits.
特征对象是实现一组特征的另一种类型的不透明值。该特征集由 对象安全基本特征(object safe base trait)
加上任意数量的 自动特征 (auto traits)
组成。
Trait objects implement the base trait
, its auto traits
, and any supertraits of the base trait.
Trait 对象实现基本特征、其自动特征和任何基本特征的超级特征。
Trait objects are written as the keyword dyn
followed by a set of trait bounds
, but with the following restrictions on the trait bounds.
All traits except the first trait must be auto traits, there may not be more than one lifetime, and opt-out bounds (e.g. ?Sized
) are not allowed. Furthermore, paths to traits may be parenthesized.
Trait 对象被编写为关键字 dyn 后跟一组 特征边界
,但对特征边界有以下限制。
除第一个特征之外的所有特征都必须是自动特征,不能有超过一个生命周期,并且不允许选择退出边界(例如?Sized )。
此外,特征的路径可以用括号括起来。
For example, given a trait Trait
, the following are all trait objects:例如,给定一个特征 Trait ,以下都是特征对象:
dyn Trait
dyn Trait + Send
dyn Trait + Send + Sync
dyn Trait + 'static
dyn Trait + Send + 'static
dyn Trait +
dyn 'static + Trait
.dyn (Trait)
Two trait object types alias each other if the base traits alias each other and if the sets of auto traits are the same and the lifetime bounds are the same. For example, dyn Trait + Send + UnwindSafe
is the same as dyn Trait + UnwindSafe + Send
.
Due to the opaqueness of which concrete type the value is of, trait objects are dynamically sized types. Like all DSTs, trait objects are used behind some type of pointer; for example &dyn SomeTrait
or Box<dyn SomeTrait>
. Each instance of a pointer to a trait object includes:
由于值属于哪种具体类型的不透明性,特征对象是 动态调整大小的类型。像所有 DSTs 一样 ,特征对象用在某种类型的指针后面;例如 &dyn SomeTrait
或 Box<dyn SomeTrait>
。指向特征对象的指针的每个实例包括:
- a pointer to an instance of a type
T
that implementsSomeTrait
- 指向实现
SomeTrait
的类型T
实例的指针
- 指向实现
- a virtual method table , often just called a vtable , which contains, for each method of
SomeTrait
and its supertraits thatT
implements, a pointer toT
’s implementation (i.e. a function pointer).- 虚拟方法表,通常简称为 vtable ,其中包含 T 实现的 SomeTrait 及其超特征的每个方法,一个指向 T 实现的指针(即函数指针)。
The purpose of trait objects is to permit “late binding” of methods. Calling a method on a trait object results in virtual dispatch at runtime: that is, a function pointer is loaded from the trait object vtable and invoked indirectly. The actual implementation for each vtable entry can vary on an object-by-object basis.
特征对象的目的是允许方法的“后期绑定”。在特征对象上调用方法会导致运行时虚拟分派:也就是说,从特征对象 vtable 加载函数指针并间接调用。每个 vtable 条目的实际实现可能因对象而异。
An example of a trait object:
trait Printable {
fn stringify(&self) -> String;
}
impl Printable for i32 {
fn stringify(&self) -> String { self.to_string() }
}
fn print(a: Box<dyn Printable>) {
println!("{}", a.stringify());
}
fn main() {
print(Box::new(10) as Box<dyn Printable>);
}
In this example, the trait Printable occurs as a trait object in both the type signature of print, and the cast expression in main.
在此示例中,特征 Printable 作为特征对象出现在 print 的类型签名和 main 中的强制转换表达式中。
Trait Object Lifetime Bounds
特征对象生命周期界限
Since a trait object can contain references, the lifetimes of those references need to be expressed as part of the trait object. This lifetime is written as Trait + 'a
. There are defaults
that allow this lifetime to usually be inferred with a sensible choice.
由于特征对象可以包含引用,因此这些引用的生命周期 需要表达为特征对象的一部分。此 lifetime 可写为 Trait + 'a
.有一些默认值允许通常通过明智的选择来推断此生命周期。
// inner attribute
#![Attr]
// outer attribute
#[Arrt]
An attribute
is a general, free-form metadatum that is interpreted according to name, convention, language, and compiler version. Attributes are modeled on Attributes in ECMA-335, with the syntax coming from ECMA-334 (C#).
Inner attributes , written with a bang (!
) after the hash (#
), apply to the item that the attribute is declared within.
- Outer attributes , written without the bang after the hash, apply to the thing that follows the attribute.
The attribute consists of a path to the attribute, followed by an optional delimited token tree whose interpretation is defined by the attribute. Attributes other than macro attributes also allow the input to be an equals sign (=
) followed by an expression. See the meta item syntax below for more details.
An attribute may be unsafe to apply. To avoid undefined behavior when using these attributes, certain obligations that cannot be checked by the compiler must be met. To assert these have been, the attribute is wrapped in unsafe(..)
, e.g. #[unsafe(no_mangle)]
.
The following attributes are unsafe:
Attributes can be classified into the following kinds:
Attributes may be applied to many things in the language:
- All item declarations accept outer attributes while external blocks, functions, implementations, and modules accept inner attributes.
- Most statements accept outer attributes (see Expression Attributes for limitations on expression statements).
- Block expressions accept outer and inner attributes, but only when they are the outer expression of an expression statement or the final expression of another block expression.
- Enum variants and struct and union fields accept outer attributes.
- Match expression arms accept outer attributes.
- Generic lifetime or type parameter accept outer attributes.
- Expressions accept outer attributes in limited situations, see Expression Attributes for details.
- Function, closure and function pointer parameters accept outer attributes. This includes attributes on variadic parameters denoted with
...
in function pointers and external blocks.
Examples:
// General metadata applied to the enclosing module or crate.
#![crate_type = "lib"]
// A function marked as a unit test
#[test]
fn test_foo() {
/* ... */
}
// A conditionally-compiled module
#[cfg(target_os = "linux")]
mod bar {
/* ... */
}
// A lint attribute used to suppress a warning/error
#[allow(non_camel_case_types)]
type int8_t = i8;
// Inner attribute applies to the entire function.
fn some_unused_variables() {
#![allow(unused_variables)]
let x = ();
let y = ();
let z = ();
}
The following is an index of all built-in attributes.
- Conditional compilation
- Testing
test
— Marks a function as a test.ignore
— Disables a test function.should_panic
— Indicates a test should generate a panic.
- Derive
derive
— Automatic trait implementations.automatically_derived
— Marker for implementations created byderive
.
- Macros
macro_export
— Exports amacro_rules
macro for cross-crate usage.macro_use
— Expands macro visibility, or imports macros from other crates.proc_macro
— Defines a function-like macro.proc_macro_derive
— Defines a derive macro.proc_macro_attribute
— Defines an attribute macro.
- Diagnostics
allow
,expect
,warn
,deny
,forbid
— Alters the default lint level.deprecated
— Generates deprecation notices.must_use
— Generates a lint for unused values.diagnostic::on_unimplemented
— Hints the compiler to emit a certain error message if a trait is not implemented.
- ABI, linking, symbols, and FFI
link
— Specifies a native library to link with anextern
block.link_name
— Specifies the name of the symbol for functions or statics in anextern
block.link_ordinal
— Specifies the ordinal of the symbol for functions or statics in anextern
block.no_link
— Prevents linking an extern crate.repr
— Controls type layout.crate_type
— Specifies the type of crate (library, executable, etc.).no_main
— Disables emitting themain
symbol.export_name
— Specifies the exported symbol name for a function or static.link_section
— Specifies the section of an object file to use for a function or static.no_mangle
— Disables symbol name encoding.used
— Forces the compiler to keep a static item in the output object file.crate_name
— Specifies the crate name.
- Code generation
inline
— Hint to inline code.cold
— Hint that a function is unlikely to be called.no_builtins
— Disables use of certain built-in functions.target_feature
— Configure platform-specific code generation.track_caller
- Pass the parent call location tostd::panic::Location::caller()
.instruction_set
- Specify the instruction set used to generate a functions code
- Documentation
doc
— Specifies documentation. See The Rustdoc Book for more information. Doc comments are transformed intodoc
attributes.
- Preludes
no_std
— Removes std from the prelude.no_implicit_prelude
— Disables prelude lookups within a module.
- Modules
path
— Specifies the filename for a module.
- Limits
recursion_limit
— Sets the maximum recursion limit for certain compile-time operations.type_length_limit
— Sets the maximum size of a polymorphic type.
- Runtime
panic_handler
— Sets the function to handle panics.global_allocator
— Sets the global memory allocator.windows_subsystem
— Specifies the windows subsystem to link with.
- Features
feature
— Used to enable unstable or experimental compiler features. See The Unstable Book for features implemented inrustc
.
- Type System
non_exhaustive
— Indicate that a type will have more fields/variants added in future.
- Debugger
debugger_visualizer
— Embeds a file that specifies debugger output for a type.collapse_debuginfo
— Controls how macro invocations are encoded in debuginfo.
derive
属性允许为数据结构自动生成新的程序项。它使用 MetaListPaths元项属性句法(为程序项)指定一系列要实现的 trait 或指定要执行的派生宏的路径。
例如,下面的派生属性将为结构体 Foo
创建一个实现 PartialEq
trait 和 Clone
trait 的实现(impl
item),类型参数 T
将被派生出的实现(impl
)加上 PartialEq
或^1^ Clone
约束:
#[test]
#[should_panic(expected = "values don't match")]
fn mytest() {
assert_eq!(1, 2, "values don't match");
}
1、自动派生常用的 traits: Rust 提供了一些常用的 traits,可以通过 #[derive()]
属性自动为结构体或枚举实现这些 traits。一些常见的可派生 traits 包括:
Debug
:通过实现Debug
trait,可以使用println!("{:?}", my_struct)
来打印结构体的调试信息。Clone
:通过实现Clone
trait,可以使用my_struct.clone()
创建结构体的克隆副本。PartialEq
和Eq
:通过实现PartialEq
trait,可以进行结构体的部分相等性比较,而Eq
trait 则实现了完全相等性比较。PartialOrd
和Ord
:通过实现PartialOrd
trait,可以对结构体进行部分有序性比较,而Ord
trait 实现了完全有序性比较。
2、自定义 traits 的自动派生: 除了派生常见的 traits,您还可以自定义 traits,并使用 #[derive()] 属性为结构体或枚举自动生成实现代码。例如,如果您定义了一个名为 MyTrait 的自定义 trait,并希望为结构体自动实现它,可以这样写:
trait MyTrait {
// trait 方法定义
}
#[derive(MyTrait)]
struct MyStruct {
// 结构体字段
}
注意,对于自定义 traits 的派生,您需要在编写 MyTrait trait 时手动实现 #[derive(MyTrait)] 的逻辑,或者使用第三方库提供的派生宏。
3、手动实现 traits: 某些 traits 无法通过 #[derive()] 属性自动派生,因为它们可能需要更多的信息或自定义实现。在这种情况下,您需要手动为结构体或枚举实现这些 traits。手动实现 traits 通常涉及为每个 trait 方法提供具体的实现代码。以下是手动实现 MyTrait trait 的示例:
trait MyTrait {
// trait 方法定义
}
struct MyStruct {
// 结构体字段
}
impl MyTrait for MyStruct {
// MyTrait 方法的具体实现
}
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {rect1:?}");
}
在 {}
中加入 :?
指示符告诉 println!
我们想要使用叫做 Debug
的输出格式。Debug
是一个 trait,它允许我们以一种对开发者有帮助的方式打印结构体,以便当我们调试代码时能看到它的值。
当我们有一个更大的结构体时,能有更易读一点的输出就好了,为此可以使用 {:#?}
替换 println!
字符串中的 {:?}
。在这个例子中使用 {:#?}
风格将会输出如下:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/rectangles`
rect1 is Rectangle {
width: 30,
height: 50,
}
另一种使用 Debug
格式打印数值的方法是使用 dbg!
宏。dbg!
宏接收一个表达式的所有权(与 println!
宏相反,后者接收的是引用),打印出代码中调用 dbg! 宏时所在的文件和行号,以及该表达式的结果值,并返回该值的所有权。
注意:调用 dbg!
宏会打印到标准错误控制台流(stderr
),与 println!
不同,后者会打印到标准输出控制台流(stdout
)。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let scale = 2;
let rect1 = Rectangle {
width: dbg!(30 * scale),
height: 50,
};
dbg!(&rect1);
}
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.61s
Running `target/debug/rectangles`
[src/main.rs:10:16] 30 * scale = 60
[src/main.rs:14:5] &rect1 = Rectangle {
width: 60,
height: 50,
}
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
}
为了使函数定义于 Rectangle
的上下文中,我们开始了一个 impl
块(impl
是 implementation 的缩写),这个 impl
块中的所有内容都将与 Rectangle
类型相关联。接着将 area
函数移动到 impl
大括号中,并将签名中的第一个(在这里也是唯一一个)参数和函数体中其他地方的对应参数改成 self
。然后在 main
中将我们先前调用 area
方法并传递 rect1
作为参数的地方,改成使用 方法语法 ( method syntax )在 Rectangle
实例上调用 area
方法。方法语法获取一个实例并加上一个点号,后跟方法名、圆括号以及任何参数。
在 area
的签名中,使用 &self
来替代 rectangle: &Rectangle
,&self
实际上是 self: &Self
的缩写。在一个 impl
块中,Self
类型是 impl
块的类型的别名。方法的第一个参数必须有一个名为 self
的 Self
类型的参数,所以 Rust 让你在第一个参数位置上只用 self
这个名字来简化。注意,我们仍然需要在 self
前面使用 &
来表示这个方法借用了 Self
实例,就像我们在 rectangle: &Rectangle
中做的那样。方法可以选择获得 self
的所有权,或者像我们这里一样不可变地借用 self
,或者可变地借用 self
,就跟其他参数一样。
这里选择 &self
的理由跟在函数版本中使用 &Rectangle
是相同的:我们并不想获取所有权,只希望能够读取结构体中的数据,而不是写入。如果想要在方法中改变调用方法的实例,需要将第一个参数改为 &mut self
。通过仅仅使用 self
作为第一个参数来使方法获取实例的所有权是很少见的;这种技术通常用在当方法将 self
转换成别的实例的时候,这时我们想要防止调用者在转换之后使用原始的实例。
使用方法替代函数,除了可使用方法语法和不需要在每个函数签名中重复 self
的类型之外,其主要好处在于组织性。我们将某个类型实例能做的所有事情都一起放入 impl
块中,而不是让将来的用户在我们的库中到处寻找 Rectangle
的功能。
Rust 并没有一个与 ->
等效的运算符;相反,Rust 有一个叫 自动引用和解引用 ( automatic referencing and dereferencing )的功能。方法调用是 Rust 中少数几个拥有这种行为的地方。
它是这样工作的:当使用 object.something()
调用方法时,Rust 会自动为 object
添加 &
、&mut
或 *
以便使 object
与方法签名匹配。
p1.distance(&p2);
(&p1).distance(&p2);
第一行看起来简洁的多。这种自动引用的行为之所以有效,是因为方法有一个明确的接收者———— self
的类型。在给出接收者和方法名的前提下,Rust 可以明确地计算出方法是仅仅读取(&self
),做出修改(&mut self
)或者是获取所有权(self
)。事实上,Rust 对方法接收者的隐式借用让所有权在实践中更友好。
所有在 impl
块中定义的函数被称为 关联函数 ( associated functions ),因为它们与 impl
后面命名的类型相关。我们可以定义不以 self
为第一参数的关联函数(因此不是方法),因为它们并不作用于一个结构体的实例。我们已经使用了一个这样的函数:在 String
类型上定义的 String::from
函数。
不是方法的关联函数经常被用作返回一个结构体新实例的构造函数。这些函数的名称通常为 new
,但 new
并不是一个关键字。例如我们可以提供一个叫做 square
关联函数,它接受一个维度参数并且同时作为宽和高,这样可以更轻松的创建一个正方形 Rectangle
而不必指定两次同样的值:
impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}
关键字 Self
在函数的返回类型中代指在 impl
关键字后出现的类型,在这里是 Rectangle
使用结构体名和 ::
语法来调用这个关联函数:比如 let sq = Rectangle::square(3);
。这个函数位于结构体的命名空间中:::
语法用于关联函数和模块创建的命名空间。第七章会讲到模块。
这一部分会分析一个 Option
的案例,Option
是标准库定义的另一个枚举。Option
类型应用广泛因为它编码了一个非常普遍的场景,即一个值要么有值要么没值。
例如,如果请求一个非空列表的第一项,会得到一个值,如果请求一个空的列表,就什么也不会得到。从类型系统的角度来表达这个概念就意味着编译器需要检查是否处理了所有应该处理的情况,这样就可以避免在其他编程语言中非常常见的 bug。
编程语言的设计经常要考虑包含哪些功能,但考虑排除哪些功能也很重要。Rust 并没有很多其他语言中有的空值功能。 空值 (Null )是一个值,它代表没有值。在有空值的语言中,变量总是这两种状态之一:空值和非空值。
Tony Hoare,null 的发明者,在他 2009 年的演讲 “Null References: The Billion Dollar Mistake” 中曾经说到:
I call it my billion-dollar mistake. At that time, I was designing the first comprehensive type system for references in an object-oriented language. My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.
我称之为我十亿美元的错误。当时,我在为一个面向对象语言设计第一个综合性的面向引用的类型系统。我的目标是通过编译器的自动检查来保证所有引用的使用都应该是绝对安全的。不过我未能抵抗住引入一个空引用的诱惑,仅仅是因为它是这么的容易实现。这引发了无数错误、漏洞和系统崩溃,在之后的四十多年中造成了数十亿美元的苦痛和伤害。
空值的问题在于当你尝试像一个非空值那样使用一个空值,会出现某种形式的错误。因为空和非空的属性无处不在,非常容易出现这类错误。
然而,空值尝试表达的概念仍然是有意义的:空值是一个因为某种原因目前无效或缺失的值。
问题不在于概念而在于具体的实现。为此,Rust 并没有空值,不过它确实拥有一个可以编码存在或不存在概念的枚举。这个枚举是 Option<T>
,而且它定义于标准库中,如下:
enum Option<T> {
None,
Some(T),
}
Option<T>
枚举是如此有用以至于它甚至被包含在了 prelude 之中,你不需要将其显式引入作用域。另外,它的成员也是如此,可以不需要 Option::
前缀来直接使用 Some
和 None
。即便如此 Option<T>
也仍是常规的枚举,Some(T)
和 None
仍是 Option<T>
的成员。
<T>
语法是一个我们还未讲到的 Rust 功能。它是一个泛型类型参数,第十章会更详细的讲解泛型。目前,所有你需要知道的就是 <T>
意味着 Option
枚举的 Some
成员可以包含任意类型的数据,同时每一个用于 T
位置的具体类型使得 Option<T>
整体作为不同的类型。这里是一些包含数字类型和字符串类型 Option
值的例子:
let some_number = Some(5);
let some_char = Some('e');
let absent_number: Option<i32> = None;
some_number
的类型是 Option<i32>
。some_char
的类型是 Option<char>
,是不同于 some_number
的类型。因为我们在 Some
成员中指定了值,Rust 可以推断其类型。对于 absent_number
,Rust 需要我们指定 Option
整体的类型,因为编译器只通过 None
值无法推断出 Some
成员保存的值的类型。这里我们告诉 Rust 希望 absent_number
是 Option<i32>
类型的。
当有一个 Some
值时,我们就知道存在一个值,而这个值保存在 Some
中。当有个 None
值时,在某种意义上,它跟空值具有相同的意义:并没有一个有效的值。那么,Option<T>
为什么就比空值要好呢?
简而言之,因为 Option<T>
和 T
(这里 T
可以是任何类型)是不同的类型,编译器不允许像一个肯定有效的值那样使用 Option<T>
。例如,这段代码不能编译,因为它尝试将 Option<i8>
与 i8
相加:
let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y;
$ cargo run
Compiling enums v0.1.0 (file:///projects/enums)
error[E0277]: cannot add `Option<i8>` to `i8`
--> src/main.rs:5:17
|
5 | let sum = x + y;
| ^ no implementation for `i8 + Option<i8>`
|
= help: the trait `Add<Option<i8>>` is not implemented for `i8`
= help: the following other types implement trait `Add<Rhs>`:
`&'a i8` implements `Add<i8>`
`&i8` implements `Add<&i8>`
`i8` implements `Add<&i8>`
`i8` implements `Add`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `enums` (bin "enums") due to 1 previous error
很好!事实上,错误信息意味着 Rust 不知道该如何将 Option<i8>
与 i8
相加,因为它们的类型不同。当在 Rust 中拥有一个像 i8
这样类型的值时,编译器确保它总是有一个有效的值。我们可以自信使用而无需做空值检查。只有当使用 Option<i8>
(或者任何用到的类型)的时候需要担心可能没有值,而编译器会确保我们在使用值之前处理了为空的情况。
换句话说,在对 Option<T>
进行运算之前必须将其转换为 T
。通常这能帮助我们捕获到空值最常见的问题之一:假设某值不为空但实际上为空的情况。
消除了错误地假设一个非空值的风险,会让你对代码更加有信心。为了拥有一个可能为空的值,你必须要显式的将其放入对应类型的 Option<T>
中。接着,当使用这个值时,必须明确的处理值为空的情况。只要一个值不是 Option<T>
类型,你就 可以 安全的认定它的值不为空。这是 Rust 的一个经过深思熟虑的设计决策,来限制空值的泛滥以增加 Rust 代码的安全性。
那么当有一个 Option<T>
的值时,如何从 Some
成员中取出 T
的值来使用它呢?Option<T>
枚举拥有大量用于各种情况的方法:你可以查看它的文档。熟悉 Option<T>
的方法将对你的 Rust 之旅非常有用。
总的来说,为了使用 Option<T>
值,需要编写处理每个成员的代码。你想要一些代码只当拥有 Some(T)
值时运行,允许这些代码使用其中的 T
。也希望一些代码只在值为 None
时运行,这些代码并没有一个可用的 T
值。match
表达式就是这么一个处理枚举的控制流结构:它会根据枚举的成员运行不同的代码,这些代码可以使用匹配到的值中的数据。
PS: C/C++如果程序员不够小心注意,很容易造成有空指针的情况,并且忘了检查,就会导致对空指针的使用!!(CSC-2024 C++ 惨痛的教训 - 依赖关系分析遍中没有对函数可能传入空指针进行检查,导致对空指针的操作!)
为什么要引入空指针?如果不得不引入某种为“空”的状态,最好 wrap 一下!!就像 rust 中的 Option 一样!