拥有 Solidity 或 Javascript 背景的读者可能会觉得 Rust 中 &、mut、<_>、unwrap() 和 ? 的用法和语法很奇怪(甚至难看)。本章将解释这些术语的含义。
如果不能马上完全消化,也不用担心。如果你忘记了这些语法的定义,随时可以回过头来复习本教程。
所有权与借用(引用 & 与解引用操作符 *):
Rust 的 Copy 类型
为了理解 & 和 *,我们首先需要了解 Rust 中的“Copy 类型”(copy type)。Copy 类型是指由于体积足够小,复制其值所带来的开销微乎其微的数据类型。以下值属于 Copy 类型:
- 整数、无符号整数和浮点数
- 布尔值
- char
它们之所以被称为“Copy 类型”,是因为它们具有固定且较小的内存大小。
另一方面,向量(vectors)、字符串(strings)和结构体(structs)的大小可以是任意的,因此它们不是 Copy 类型。
为什么 Rust 要区分 Copy 类型和非 Copy 类型
请看下面的 Rust 代码:
pub fn main() {
let a: u32 = 2;
let b: u32 = 3;
println!("{}", add(a, b)); // a and b a are copied to the add function
let s1 = String::from("hello");
let s2 = String::from(" world");
// if s1 and s2 are copied, this could be a huge data transfer
// if the strings are very long
println!("{}", concat(s1, s2));
}
// implementations of add() and concat() are not shown for brevity
// this code does not compile
在第一段将 a 和 b 相加的代码中,只需要将 64 位的数据从变量复制到函数中(32 位 * 2 个变量)。
但在字符串的情况下,我们往往无法预先知道要复制多少数据。如果字符串长达 1 GB,程序将会出现明显的卡顿。
Rust 希望我们显式地说明我们想如何处理大型数据。它不会像动态语言那样在后台悄悄进行复制。
因此,当我们进行一些诸如 将字符串赋值给新变量 这样简单的操作时,Rust 的行为可能会出乎很多人的意料,我们将在下一节中看到这一点。
Rust 中的所有权
对于非 Copy 类型(如 Strings、vectors、structs 等),一旦该值被赋给一个变量,该变量就“拥有”(owns)了它。所有权的含义将在稍后进行演示。
以下代码将无法编译。解释见注释:
// Example of changing ownership on a non-copy datatype (string)
let s1 = String::from("abc");
// s2 becomes the owner of `String::from("abc")`
let s2 = s1;
// The following line will fail to compile because s1 can no longer access its string value.
println!("{}", s1);
// This line compiles successfully because s2 now owns the string value.
println!("{}", s2);
为了修复上面的代码,我们有两个选项:使用 & 操作符或者克隆(clone)s1。
选项 1:赋予 s2 对 s1 的视图
在下面的代码中,请注意 s1 前面重要的 & 符号:
pub fn main() {
let s1 = String::from("abc");
let s2 = &s1; // s2 can now view `String::from("abc")` but not own it
println!("{}", s1); // This compiles, s1 still holds its original string value.
println!("{}", s2); // This compiles, s2 holds a reference to the string value in s1.
}
如果我们想让另一个变量“查看”(view)该值(即获得只读访问权限),我们需要使用 & 操作符。
要赋予另一个变量或函数某个被拥有(owned)变量的视图,我们在该变量前加上 &。
可以将 & 理解为非 Copy 类型的“仅查看”模式,这可能会有所帮助。我们在技术上将这种“仅查看”称为 借用(borrowing)。
选项 2:克隆 s1
为了理解如何克隆一个值,请看以下示例:
fn main() {
let mut message = String::from("hello");
println!("{}", message);
message = message + " world";
println!("{}", message);
}
上面的代码将如预期般打印出 “hello”,然后打印出 “hello world”。
然而,如果我们添加另一个用来查看 message 的变量 y,代码将无法编译:
// Does not compile
fn main() {
let mut message = String::from("hello");
println!("{}", message);
let mut y = &message; // y is viewing message
message = message + " world";
println!("{}", message);
println!("{}", y); // should y be "hello" or "hello world"?
}
Rust 不接受上述代码,因为变量 message 在被查看期间无法被重新赋值。
如果我们希望 y 能够复制 message 的值,同时又不会干扰 message 后续的操作,我们可以将其克隆:
fn main() {
let mut message = String::from("hello");
println!("{:?}", message);
let mut y = message.clone(); // change this to clone
message = message + " world";
println!("{:?}", message);
println!("{:?}", y);
}
上述代码将打印:
hello
hello world
hello
所有权问题仅存在于非 Copy 类型中
如果我们将 String(非 Copy 类型)替换为 Copy 类型(如整数),就不会遇到上述任何问题。Rust 会非常乐意复制 Copy 类型,因为这种开销可以忽略不计。
let s1 = 3;
let s2 = s1;
println!("{}", s1);
println!("{}", s2);
mut 关键字
在 Rust 中,除非指定了 mut 关键字,否则所有变量默认都是不可变的(immutable)。
以下代码将无法编译:
pub fn main() {
let counter = 0;
counter = counter + 1;
println!("{}", counter);
}
如果我们尝试编译上述代码,将会得到以下错误:

幸运的是,如果你忘记包含 mut 关键字,编译器通常足够聪明,能够清楚地指出错误。下面的代码插入了 mut 关键字,使代码得以成功编译:
pub fn main() {
let mut counter = 0;
counter = counter + 1;
println!("{}", counter);
}
Rust 中的泛型:< > 语法
假设有一个函数,它接收一个任意类型的值,并返回一个带有 foo 字段(包含该值)的结构体。与其为每种可能的类型编写一大堆函数,不如使用泛型(generic)。
以下示例中的结构体可以是 i32 类型,也可以是 bool 类型。
// derive the debug trait so we can print the struct to the console
#[derive(Debug)]
struct MyValues<T> {
foo: T,
}
pub fn main() {
let first_struct: MyValues<i32> = MyValues { foo: 1 }; // foo has type i32
let second_struct: MyValues<bool> = MyValues { foo: false }; // foo has type bool
println!("{:?}", first_struct);
println!("{:?}", second_struct);
}
这就体现出了它的便利之处:当我们在 Solana 中将值“存入存储”时,无论是存储数字、字符串还是其他内容,我们都希望具备很高的灵活性。
如果我们的结构体包含多个字段,参数化类型的语法如下所示:
struct MyValues<T, U> {
foo: T,
bar: U,
}
泛型在 Rust 中是一个非常庞大的主题,因此这里绝不是在进行全面的论述。不过,这足以让你对大多数 Solana 程序有相当程度的理解。
Options、Enums 与解引用 *
为了展示 options 和 enums 的重要性,请看下面的示例:
fn main() {
let v = Vec::from([1,2,3,4,5]);
assert!(v.iter().max() == 5);
}
代码编译失败,出现以下错误:
6 | assert!(v.iter().max() == 5);
| ^ expected `Option<&{integer}>`, found integer
max() 的输出并不是一个整数,这是因为存在向量 v 可能为空的极端情况(corner case)。
Rust Option
为了处理这种极端情况,Rust 会转而返回一个 Option。Option 是一个枚举(enum),它可以包含预期返回的值,也可以包含一个表示“这里没有任何东西”的特殊值。
要将 Option 转换为其底层类型,我们使用 unwrap()。如果我们接收到的是“空值”(nothing),unwrap() 将会导致 panic,因此我们只应在希望引发 panic 或者确定不会获得空值的情况下使用它。
为了让代码按照预期运行,我们可以采取以下做法:
fn main() {
let v = Vec::from([1,2,3,4,5]);
assert!(v.iter().max().unwrap() == 5);
}
解引用操作符 *
但它仍然无法运行!这一次我们收到了另一个错误
19 | assert!(v.iter().max().unwrap() == 5);
| ^^ no implementation for `&{integer} == {integer}`
等式左侧的项是一个整数的视图(即 &),而右侧的项是一个真正的整数。
要将整数的“视图”转换为常规的整数,我们需要使用“解引用”(dereference)操作。这是通过在值前面加上 * 操作符来实现的。
fn main() {
let v = Vec::from([1,2,3,4,5]);
assert!(*v.iter().max().unwrap() == 5);
}
由于数组的元素是 Copy 类型,解引用操作符将在后台自动复制 max().unwrap() 返回的 5。
你可以把 * 想象成在不干扰原始值的情况下“撤销”了 & 的操作。
在非 Copy 类型上使用 * 操作符是一个复杂的主题。就目前而言,你只需要知道:如果你接收了一个 Copy 类型的视图(借用),并且需要将其转为“正常”类型,使用 * 操作符即可。
Rust 中的 Result 与 Option
Option 用于当我们可能接收到“空”内容时的场景。而 Result(和 Anchor 程序一直返回的那个 Result 一样)则用于当我们可能会收到错误的场景。
Result 枚举
Rust 中的 Result<T, E> 枚举用于函数的执行可能会成功并返回类型 T 的值(泛型类型),或者失败并返回类型 E 的错误(泛型错误类型)的情况。它被设计用于处理可能导致成功结果或错误条件的操作。
enum Result<T, E> {
Ok(T),
Err(E),
}
在 Rust 中,? 操作符专门用于 Result<T, E> 枚举,而 unwrap() 则可同时用于 Result<T, E> 和 Option<T> 枚举。
? 操作符
? 操作符只能在返回 Result 的函数中使用,因为它是用于返回 Err 或 Ok 的语法糖。
? 操作符用于从 Result<T, E> 枚举中提取数据;如果函数成功执行,它会返回 Ok(T) 变体;如果发生错误,它会向上抛出错误 Err(E)。unwrap() 方法的作用方式类似,且可同时应用于 Result<T, E> 和 Option<T> 枚举,但应谨慎使用,因为一旦发生错误,它存在导致程序崩溃的风险。
现在,请看下面的代码:
pub fn encode_and_decode(_ctx: Context<Initialize>) -> Result<()> {
// Create a new instance of the `Person` struct
let init_person: Person = Person {
name: "Alice".to_string(),
age: 27,
};
// Encode the `init_person` struct into a byte vector
let encoded_data: Vec<u8> = init_person.try_to_vec().unwrap();
// Decode the encoded data back into a `Person` struct
let data: Person = decode(_ctx, encoded_data)?;
// Logs the decoded person's name and age
msg!("My name is {:?}, I am {:?} years old.", data.name, data.age);
Ok(())
}
pub fn decode(_accounts: Context<Initialize>, encoded_data: Vec<u8>) -> Result<Person> {
// Decode the encoded data back into a `Person` struct
let decoded_data: Person = Person::try_from_slice(&encoded_data).unwrap();
Ok(decoded_data)
}
try_to_vec() 方法将结构体编码为字节向量,并返回一个 Result<T, E> 枚举,其中 T 代表字节向量;而 unwrap() 方法则用于从 Ok(T) 中提取字节向量的值。如果该方法未能成功将结构体转换为字节向量,程序将会崩溃。
通过 RareSkills 了解更多
本教程是我们免费 Solana 课程 的一部分。
原文发布于 2024 年 2 月 14 日