Rust 的不寻常语法
更新日期:Feb 19
来自 Solidity 或 Javascript 背景的读者可能会觉得 Rust 对&
、mut
、<_>
、unwrap()
和?
的使用和语法很奇怪(甚至丑陋)。本章将解释这些语法的含义。
如果一切没有马上理解,不要担心。如果你忘记了语法定义,随时可以回到本教程。
所有权和借用(引用&
和解引用运算符*
):
Rust 复制类型
要理解&
和*
,我们首先需要了解 Rust 中的“复制类型”。复制类型是一个数据类型,其大小足够小,使得复制值的开销微不足道。以下值是复制类型:
- 整数、无符号整数和浮点数
- 布尔值
- 字符
它们之所以是“复制类型”,是因为它们具有固定的小尺寸。
另一方面,向量、字符串和结构体可以是任意大的,因此它们不是复制类型。
Rust 为什么区分复制类型和非复制类型
考虑以下 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 个变量)。
然而,在字符串的情况下,我们并不总是提前知道要复制多少数据。如果字符串长度为 1GB,程序运行速度将会受到严重影响。
Rust 希望我们明确表达希望如何处理大数据。它不会像动态语言那样在后台复制它。
因此,当我们做一些简单的事情,比如将字符串分配给一个新变量时,Rust 会做一些很多人觉得意想不到的事情,我们将在下一节中看到。
Rust 中的所有权
对于非复制类型(字符串、向量、结构体等),一旦将值分配给变量,该变量就“拥有”它。所有权的影响将很快展示。
以下代码将无法编译。注释中有解释:
// 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);
要修复上面的代码,我们有两个选项:使用&
运算符或克隆s1
。
选项 1:s2
查看(view
)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.
}
如果我们希望另一个变量“查看”该值(即获得只读访问权限),我们使用&
运算符。
为了让另一个变量或函数查看一个拥有的变量,我们在其前面加上&
。
将&
视为非复制类型的“只读”模式可能有所帮助。我们称之为“只读”的技术术语是借用。
选项 2:克隆s1
要了解如何克隆一个值,请考虑以下示例:
fn main() {
let mut message = String::from("hello");
println!("{}", message);
message = message + " world";
println!("{}", message);
}
上面的代码将按预期打印“hello”,然后“hello world”。
然而,如果我们添加另一个变量y
来查看message
,代码将不再编译:
// 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
所有权仅适用于非复制类型
如果我们用一个复制类型(如整数)替换我们的字符串(这是一个非复制类型),我们将不会遇到上述任何问题。Rust 将愉快地复制 复制类型,因为开销微不足道。
let s1 = 3;
let s2 = s1;
println!("{}", s1);
println!("{}", s2);
mut
关键字
在 Rust 中,默认情况下,所有变量都是不可变的,除非指定了mut
关键字。
以下代码将无法编译:
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
的结构体的函数。与其为每种可能的类型编写一堆函数,不如使用泛型。
下面的示例结构体可以是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 程序有一个很好的理解。
Option、枚举和解引用*
为了展示选项和枚举的重要性,让我们考虑以下示例:
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
由于向量v
可能为空,max()
的输出不是整数。
Rust Option
为了处理这种情况,Rust 返回一个 Option。Option 是一个枚举,可以包含预期值,也可以包含“没有内容”的特殊值。
要将选项转换为底层类型,我们使用unwrap()
。如果我们收到“没有内容”,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}`
等式左侧的术语是整数的视图(即&),右侧的术语是实际整数。
要将整数的“视图”转换为常规整数,我们需要使用“解引用”操作。这是当我们在值前面加上*
运算符时发生的。
fn main() {
let v = Vec::from([1,2,3,4,5]);
assert!(*v.iter().max().unwrap() == 5);
}
由于数组的元素是复制类型,解引用运算符将默默地复制max().unwrap()
返回的 5。
你可以将*
视为在不干扰原始值的情况下“撤消”&
。
对非复制类型使用*
运算符是一个复杂的问题。目前,你需要知道的是,如果你收到一个复制类型的视图(借用),并且需要将其转换为“正常”类型,请使用**运算符。
Rust 中的 Result 与 Option
当可能收到“空”内容时,我们使用 option。当我们可能收到错误时,我们使用Result
(与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 课程的一部分。