Solana基础 - Rust不寻常的语法
本文详尽地介绍了 Rust 的所有权、借用及其相关概念,包括 Rust 的复制类型、可变性、泛型、Option 和 Result 等内容。通过示例代码,深入解释了 Rust 的独特语法和其内在逻辑,尤其适合有 Solidity 或 JavaScript 背景的开发者。此外,文章结构清晰,包含代码示例和必要的注释,帮助读者更好地理解 Rust 编程语言。
来自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和b被复制到add函数
let s1 = String::from("hello");
let s2 = String::from(" world");
// 如果s1和s2被复制,可能会发生巨大的数据传输
// 如果字符串非常长
println!("{}", concat(s1, s2));
}
// 为了简洁,add()和concat()的实现未显示
// 这段代码无法编译在第一段代码中,a和b相加,只需要从变量复制64位数据(32位 * 2变量)。
然而,在字符串的情况下,我们并不总是知道要复制多少数据。如果字符串长达1GB,程序会显著变慢。
Rust希望我们明确如何处理大型数据。它不会像动态语言那样在后台自动复制。
因此,当我们做一些简单如_将字符串赋值给新变量_的操作时,Rust会做一些许多人认为意外的事情,正如我们在下一节所见。
Rust中的所有权
对于非复制类型(字符串、向量、结构体等),一旦值被赋给变量,该变量便“拥有”它。所有权的影响将在随后展示。
以下代码无法编译。解释在注释中:
// 非复制数据类型(字符串)上更改所有权的示例
let s1 = String::from("abc");
// s2成为`String::from("abc")`的所有者
let s2 = s1;
// 以下行编译失败,因为s1无法再访问其字符串值。
println!("{}", s1);
// 这一行编译成功,因为s2现在拥有字符串值。
println!("{}", s2);要修复上面的代码,我们有两个选择:使用&运算符或克隆s1。
选项 1:给s2一个s1的视图
在下面的代码中,注意重要的&前缀s1:
pub fn main() {
let s1 = String::from("abc");
let s2 = &s1; // s2现在可以查看`String::from("abc")`但不能拥有它
println!("{}", s1); // 这段代码编译,s1仍然保持其原始字符串值。
println!("{}", s2); // 这段代码编译,s2持有对s1中的字符串值的引用。
}如果我们希望另一个变量“查看”值(即获得只读访问权限),我们使用&运算符。
要给另一个变量或函数查看一个拥有的变量,我们在前面加上&。
可以将&视为非复制类型的“仅视图”模式。我们所称的“仅视图”的技术术语是借用。
选项 2:克隆s1
要理解我们如何可以克隆一个值,考虑以下示例:
fn main() {
let mut message = String::from("hello");
println!("{}", message);
message = message + " world";
println!("{}", message);
}上面的代码将打印“hello”,然后打印“hello world”。
但是,如果我们添加一个查看message的变量y,代码将无法再编译:
// 无法编译
fn main() {
let mut message = String::from("hello");
println!("{}", message);
let mut y = &message; // y正在查看message
message = message + " world";
println!("{}", message);
println!("{}", y); // y应该是"hello"还是"hello world"?
}Rust不接受上面的代码,因为变量message在被查看时不能重新赋值。
如果我们希望y能够在不干扰message的前提下复制message的值,我们可以选择克隆它:
fn main() {
let mut message = String::from("hello");
println!("{:?}", message);
let mut y = message.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。
// 推导调试trait,以便我们可以将结构体打印到控制台
#[derive(Debug)]
struct MyValues<T> {
foo: T,
}
pub fn main() {
let first_struct: MyValues<i32> = MyValues { foo: 1 }; // foo的类型为i32
let second_struct: MyValues<bool> = MyValues { foo: false }; // foo的类型为bool
println!("{:?}", first_struct);
println!("{:?}", second_struct);
}这里有一个便利之处:当我们在Solana中“存储”值时,我们希望能够灵活地存储数字、字符串或其他任何东西。
如果我们的结构体有多个字段,参数化类型的语法如下:
struct MyValues<T, U> {
foo: T,
bar: U,
}泛型在Rust中是一个非常大的主题,因此我们这里并没有给出完整的处理。不过,这对于理解大多数Solana程序来说已经足够。
选项、枚举和解引用 *
为了展示选项和枚举的重要性,我们来看以下示例:
fn main() {
let v = Vec::from([1,2,3,4,5]);
assert!(v.iter().max() == 5);
}这段代码编译失败,并显示以下错误:
6 | assert!(v.iter().max() == 5);
| ^ 预期为`Option<&{integer}>`,找到整数max()的输出不是一个整数,因为存在v可能为空的边缘情况。
Rust选项
为了处理这个边缘情况,Rust返回一个选项。选项是一个枚举,可以包含预期的值,或者指示“没有值”的特殊值。
要将选项转换为基础类型,我们使用unwrap()。unwrap()会在我们收到“没有值”的情况下导致恐慌,因此我们只应在希望发生恐慌或我们确定不会得到空值的情况下使用它。
为了让代码按预期工作,我们可以这样做:
fn main() {
let v = Vec::from([1,2,3,4,5]);
assert!(v.iter().max().unwrap() == 5);
}解引用 * 运算符
但它仍然无法工作!这次我们收到一个错误:
19 | assert!(v.iter().max().unwrap() == 5);
| ^^ 没有实现 `&{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
当我们可能收到“空”的东西时使用选项。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<()> {
// 创建`Person`结构的新实例
let init_person: Person = Person {
name: "Alice".to_string(),
age: 27,
};
// 将`init_person`结构编码为字节向量
let encoded_data: Vec<u8> = init_person.try_to_vec().unwrap();
// 将编码的数据解码回`Person`结构
let data: Person = decode(_ctx, encoded_data)?;
// 日志打印解码的人的名字和年龄
msg!("我的名字是 {:?},我今年 {:?}岁了。", data.name, data.age);
Ok(())
}
pub fn decode(_accounts: Context<Initialize>, encoded_data: Vec<u8>) -> Result<Person> {
// 将编码的数据解码回`Person`结构
let decoded_data: Person = Person::try_from_slice(&encoded_data).unwrap();
Ok(decoded_data)
}try_to_vec()方法将结构编码为字节向量,并返回Result<T, E>枚举,其中T是字节向量,而unwrap()方法用于从OK(T)中提取字节向量的值。如果该方法无法将结构转换为字节向量,这将导致程序崩溃。