您正在查看: Solana 分类下的文章

Solana基础 - Rust 结构体与属性式和自定义派生宏

文章详细介绍了 Rust 语言中的 attribute-like 和 custom derive 宏的使用方法,通过具体代码示例展示了如何通过宏在编译时修改结构体,并解释了宏的工作原理和实现方式。
Rust 中的类似属性和自定义派生宏用于在编译时以某种方式修改一段 Rust 代码,通常是为了增加功能。

要理解 Rust 中的类似属性和自定义派生宏,我们首先需要简要介绍 Rust 中的实现结构。

结构的实现:impl

以下结构应该是相当容易理解的。当我们创建对特定结构进行操作的函数时,事情就变得有趣了。我们这样做的方法是使用 impl

struct Person {
    name: String,
    age: u8,
}

关联函数和方法是在 impl 块中为结构实现的。

关联函数可以与 Solidity 中为与结构交互创建库的场景进行比较。当我们定义 using lib for MyStruct 时,它允许我们使用语法 myStruct.associatedFunction()。这使得该函数可以通过 Self 关键字访问 myStruct

我们建议使用 Rust Playground,但对于更复杂的示例,你可能需要设置你的 IDE。

让我们看一个下面的示例:

struct Person {
    age: u8,
    name: String,
}

// 为 `Person` 结构实现方法 `new()`,允许初始化一个 `Person` 实例
impl Person {
    // 使用提供的 `name` 和 `age` 创建一个新的 `Person`
    fn new(name: String, age: u8) -> Self {
        Person { name, age }
    }

    fn can_drink(&self) -> bool {
        if self.age >= 21 as u8 {
            return true;
        }
        return false;
    }

    fn age_in_one_year(&self) -> u8 {
        return &self.age + 1;
    }
} 

fn main() {
    // 用法:创建一个带有名字和年龄的新 `Person` 实例
    let person = Person::new(String::from("Jesserc"), 19);

    // 使用一些实现函数
    println!("{:?}", person.can_drink()); // false
    println!("{:?}", person.age_in_one_year()); // 20
    println!("{:?}", person.name);
}

用法:

// 用法:创建一个带有名字和年龄的新 `Person` 实例
let person = Person::new(String::from("Jesserc"), 19);

// 使用一些实现函数
person.can_drink(); // false
person.age_in_one_year(); // 20

Rust Traits

Rust traits 是在不同的 impl 之间实现共享行为的一种方式。可以将它们视为 Solidity 中的接口或抽象合约——任何使用该接口的合约必须实现某些函数。

例如,假设我们有一个需要定义汽车和船的结构的场景。我们想附加一个方法,允许我们以每小时公里数检索它们的速度。在 Rust 中,我们可以通过使用单个 trait 并在两个结构之间共享该方法来实现这个目标。

如下所示:

// traits 用 `trait` 关键字定义,后跟其名称
trait Speed {
    fn get_speed_kph(&self) -> f64;
}

// 汽车结构
struct Car {
    speed_mph: f64,
}

// 船结构
struct Boat {
    speed_knots: f64,
}

// 使用 `impl` 关键字为类型实现 traits,如下所示
impl Speed for Car {
    fn get_speed_kph(&self) -> f64 {
        // 将英里每小时转换为公里每小时
        self.speed_mph * 1.60934
    }
}

// 我们也为 `Boat` 实现 `Speed` trait
impl Speed for Boat {
    fn get_speed_kph(&self) -> f64 {
        // 将节转换为公里每小时
        self.speed_knots * 1.852
    }
}

fn main() {
    // 初始化一个 `Car` 和 `Boat` 类型
    let car = Car { speed_mph: 60.0 };
    let boat = Boat { speed_knots: 30.0 };

    // 获取并打印以公里每小时为单位的速度
    let car_speed_kph = car.get_speed_kph();
    let boat_speed_kph = boat.get_speed_kph();

    println!("Car Speed: {} km/h", car_speed_kph); // 96.5604 km/h
    println!("Boat Speed: {} km/h", boat_speed_kph); // 55.56 km/h
}

宏如何修改结构

在我们关于类似函数的宏的教程中,我们看到了宏如何扩展代码,比如 println!(...)msg!(...) 在大型 Rust 代码中的用法。在 Solana 的上下文中,我们关心的另一种宏是 attribute-like 宏和 derive 宏。我们可以在 anchor 创建的启动程序中看到这三种宏(函数式、类似属性和派生):

Rust attribute and custom-derive macros

为了直观了解类似属性的宏做了什么,我们将创建两个宏:一个将字段添加到结构中,另一个将其删除。

示例 1:类似属性的宏,插入字段

为了更好地理解 Rust 的属性和宏如何工作,我们将创建一个 attribute-like macro,其功能为:

  1. 处理一个不包含 foobar 字段的结构,字段类型为 i32
  2. 将这些字段插入到结构中
  3. 创建一个包含称为 double_foo 的函数的 impl,该函数返回 foo 字段所持的整数值的两倍。

设置

首先我们创建一个新的 Rust 项目:

cargo new macro-demo --lib 
cd macro-demo
touch src/main.rs

在 Cargo.toml 文件中添加以下内容:

[lib]
proc-macro = true

[dependencies]
syn = {version="1.0.57",features=["full","fold"]}
quote = "1.0.8"

创建主程序

将以下代码粘贴到 src/main.rs 文件中。请确保阅读注释:

// src/main.rs
// 导入 macro_demo crate 并用 `*` 通配符引入所有项
// (实际上是此 crate 中的所有内容,包括我们在 `src/lib.rs` 中的宏)
use macro_demo::*;

// 将我们在 `src/lib.rs` 中创建的 `foo_bar_attribute` 过程属性宏应用于 `struct MyStruct`
// 该过程宏将生成一个带有指定字段和方法的新结构定义
#[foo_bar_attribute]
struct MyStruct {
    baz: i32,
}

fn main() {
    // 使用 `default()` 方法创建 `MyStruct` 的新实例
    // 此方法由宏生成的 `Default` trait 实现提供
    let demo = MyStruct::default();

    // 将 `demo` 的内容打印到控制台
    // 宏生成的 `Debug` trait 实现允许使用 `println!` 进行格式化输出
    println!("struct is {:?}", demo);

    // 在 `demo` 上调用 `double_foo()` 方法
    // 此方法由宏生成,返回 `foo` 字段值的两倍
    let double_foo = demo.double_foo();

    // 将调用 `double_foo` 的结果打印到控制台
    println!("double foo: {}", double_foo);
}

一些观察:

  • 结构 MyStruct 中没有字段 foo
  • double_foo 函数没有在上面的代码中定义,假设它存在。

现在让我们创建将修改 MyStruct 的类似属性的宏。

将 src/lib.rs 中的代码替换为以下代码(请确保阅读注释):

// src/lib.rs
// 导入必要的外部库
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemStruct};

// 声明一个使用 `proc_macro_attribute` 指令的过程属性宏
// 这使得宏可以作为属性使用
#[proc_macro_attribute]
// `foo_bar_attribute` 函数接受两个参数:
// _metadata:提供给宏的参数(如果有)
// _input:宏所应用的 TokenStream
pub fn foo_bar_attribute(_metadata: TokenStream, _input: TokenStream) -> TokenStream {
    // 将输入 TokenStream 解析为表示结构的 AST 节点
    let input = parse_macro_input!(_input as ItemStruct);
    let struct_name = &input.ident; // 获取结构的名称

    // 使用 quote! 宏构建输出 TokenStream
    // quote! 宏允许将 Rust 代码写成字符串的方式,但可以插入值
    TokenStream::from(quote! {
        // 为 #struct_name 派生 Debug trait,以启用使用 `println()` 的格式化输出
        #[derive(Debug)]
        // 定义具有两个字段:foo 和 bar 的新结构 #struct_name
        struct #struct_name {
            foo: i32,
            bar: i32,
        }

        // 为 #struct_name 实现 Default trait
        // 这提供了一个 default() 方法,用于创建 #struct_name 的新实例
        impl Default for #struct_name {
            // 默认方法返回一个新的 #struct_name 实例,其中 foo 设置为 10,bar 设置为 20
            fn default() -> Self {
                #struct_name { foo: 10, bar: 20}
            }
        }

        impl #struct_name {
            // 为 #struct_name 定义一个方法 double_foo
            // 此方法返回 foo 的双倍值
            fn double_foo(&self) -> i32 {
                self.foo * 2
            }
        }
    })
}

现在,为了测试我们的宏,我们使用 cargo run src/main.rs 运行代码。

我们会得到以下输出:

struct is MyStruct { foo: 10, bar: 20 }
double foo: 20

示例 2:类似属性的宏,删除字段

关于类似属性的宏,可以认为它们在修改结构时具有无限的能力。让我们重复上面的示例,但这次类似属性的宏将删除结构中的所有字段。

将 src/lib.rs 替换为以下内容:

// src/lib.rs
// 导入必要的外部库
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemStruct};

#[proc_macro_attribute]
pub fn destroy_attribute(_metadata: TokenStream, _input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(_input as ItemStruct);
    let struct_name = &input.ident; // 获取结构的名称

    TokenStream::from(quote! {
        // 返回一个具有相同名称的空结构
        #[derive(Debug)]
        struct #struct_name {
        }
    })
}

将 src/main.rs 替换为以下内容:

use macro_demo::*;

#[destroy_attribute]
struct MyStruct {
    baz: i32,
    qux: i32,
}

fn main() {
    let demo = MyStruct { baz: 3, qux: 4 };

    println!("struct is {:?}", demo);
}

当你尝试使用 cargo run src/main.rs 编译时,会收到以下错误消息:

Error: struct MyStruct has no field named baz

这可能看起来很奇怪,因为结构显然有这些字段。然而,类似属性的宏删除了它们!

#[derive(…)]

#[derive(…)] 宏的功能远没有类似属性宏强大。对于我们的目的而言,派生宏 增强 了结构,而不是改变它。(这不是一个精确的定义,但现在足够了)。

派生宏可以,除了其他外,向结构附加一个 impl

例如,如果我们尝试做以下操作:

struct Foo {
    bar: i32,
}

pub fn main() {
    let foo = Foo { bar: 3 };
    println!("{:?}", foo);
}

编译时将不会通过,因为结构不可“打印”。

为了使它们可打印,需要有一个 impl,其中有一个函数 fmt,返回结构的字符串表示。

如果我们这样做:

#[derive(Debug)]
struct Foo {
    bar: i32,
}

pub fn main() {
    let foo = Foo { bar: 3 };
    println!("{:?}", foo);
}

我们期望它打印:

Foo { bar: 3 }

派生属性以某种方式“增强”了 Foo,使得 println! 可以为其创建字符串表示。

总结

impl 是一组对结构进行操作的函数。它们通过使用与结构相同的名称“附加”到结构上。trait 强制工具的 impl 实现某些函数。在我们的示例中,我们通过语法 impl Speed for Car 将 trait Speed 附加到 impl Car。

类似属性的宏接收一个结构,可以完全重写它。

派生宏增强了结构,使其具有附加功能。

宏允许 Anchor 隐藏复杂性

让我们再看看 Anchor 在 anchor init 时创建的程序:

Rust attribute and custom-derive macros

属性 #[program] 在幕后修改模块。例如,它实现了一个路由器,自动将传入的区块链指令定向到模块内的适当函数。

结构 Initialize {} 被增强了额外的功能,以便在 Solana 框架中使用。

总结

宏是一个非常大的主题。我们在这里的目的是让你了解当你看到 #[program]#[derive(Accounts)] 时发生了什么。不要因它感到陌生而沮丧。你不需要能够编写宏才能编写 Solana 程序

不过,了解它们的作用希望能让你看到的程序变得不那么神秘。
转载:https://learnblockchain.cn/article/11373

Solana基础 - Rust 函数式过程宏

本教程解释了函数与函数式宏之间的区别。例如,为什么 msg! 后面有一个感叹号?本教程将解释这种语法。作为一种强类型语言,Rust 不能接受函数的任意数量的参数。例如,Python 的 print 函数可以接受任意数量的参数:

print(1)
print(1, 2)
print(1, 2, 3)

! 表示“函数”是一个函数式宏。Rust 的函数式宏通过 ! 符号来标识,例如 Solana 中的 println!(...)msg!(...)。在 Rust 中,打印内容的常规函数(非函数式宏)是 std::io::stdout().write,它只接受一个字节字符串作为参数。如果你想运行以下代码,如果你不想设置开发环境,Rust Playground 是一个方便的工具。我们使用以下示例(取自这里):

use std::io::Write;

fn main() {
    std::io::stdout().write(b"Hello, world!
").unwrap();
}

请注意,write 是一个函数,而不是宏,因为它没有 !。如果你尝试像上面在 Python 中那样做,代码将无法编译,因为 write 只接受一个参数:

// 这段代码无法编译
use std::io::Write;

fn main() {
    std::io::stdout().write(b"1
").unwrap();
    std::io::stdout().write(b"1", b"2
").unwrap();
    std::io::stdout().write(b"1", b"2", b"3
").unwrap();
}

因此,如果你想打印任意数量的参数,你需要为每种参数数量编写一个自定义的 print 函数来处理每种情况 —— 这是非常低效的!以下是这样代码的样子(非常不推荐!):

use std::io::Write;

// 打印一个参数
fn print1(arg1: &[u8]) -> () {
    std::io::stdout().write(arg1).unwrap();
}

// 打印两个参数
fn print2(arg1: &[u8], arg2: &[u8]) -> () {
    let combined_vec = [arg1, b" ", arg2].concat();
    let combined_slice = combined_vec.as_slice();
    std::io::stdout().write(combined_slice).unwrap();
}

// 打印三个参数
fn print3(arg1: &[u8], arg2: &[u8], arg3: &[u8]) -> () {
    let combined_vec = [arg1, b" ", arg2, b" ", arg3].concat();
    let combined_slice = combined_vec.as_slice();
    std::io::stdout().write(combined_slice).unwrap();
}

fn main() {
    print1(b"1
");
    print2(b"1", b"2
");
    print3(b"1", b"2", b"3
");
}

如果我们观察 print1print2print3 函数中的模式,它只是将参数插入到向量中并在它们之间添加一个空格,然后将向量转换回字节字符串(准确说是字节切片)。如果我们能像 println! 这样编写一段代码,并自动将其扩展为一个 print 函数,接受我们所需的任意数量的参数,那岂不是很好?这就是 Rust 宏的作用。Rust 宏将 Rust 代码作为输入,并以编程方式将其扩展为更多的 Rust 代码。 这帮助我们避免了必须为代码所需的每种 print 语句编写 print 函数的无聊工作。

宏的扩展

要查看 Rust 编译器如何扩展 println! 宏的示例,请查看 cargo expand github 仓库。结果相当冗长,所以我们不在这里展示。

将宏视为黑盒子是可以的

当宏由库提供时,它们非常方便,但手动编写宏非常繁琐,因为它需要字面解析 Rust 代码。

Rust 中不同类型的宏

我们给出的 println! 示例是一个函数式宏。Rust 还有其他类型的宏,但我们关心的另外两种是自定义派生宏属性式宏。让我们看看 anchor 创建的新程序:不同类型的宏我们将在接下来的教程中解释它们的工作原理。

转载:https://learnblockchain.cn/article/11374

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()的实现未显示
// 这段代码无法编译

在第一段代码中,ab相加,只需要从变量复制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的结构体的函数。与其为每种可能的类型编写一堆函数,我们可以使用一个泛型

下面的示例结构体可以是i32bool

// 推导调试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中的ResultOption

当我们可能收到“空”的东西时使用选项。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的函数中使用,因为它是返回ErrOk的语法糖。

?运算符用于从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)中提取字节向量的值。如果该方法无法将结构转换为字节向量,这将导致程序崩溃。

转载:https://learnblockchain.cn/article/11426

Solana基础 - Solidity开发者的Rust基础

本教程深入探讨了Solidity和Rust在控制流、数组、映射、结构体和常量等方面的语法对比,旨在帮助掌握Solidity的开发者快速上手Rust编程。

本教程介绍了在 Solidity 中最常用的语法,并展示了其在 Rust 中的等价实现。

如果你想了解 Rust 与 Solidity 之间的高层次差异,请查看链接的教程。本教程假设你已经了解 Solidity,假如你对 Solidity 不熟悉,请查看我们免费的 Solidity 教程

创建一个名为 tryrust 的新 Solana Anchor 项目并设置环境。

条件语句

我们可以说,开发人员在 Solidity 中可以根据特定条件控制执行流程的方式有 2 种:

  • If-Else 语句
  • 三元运算符

现在让我们看看上述内容在 Solidity 中的实现,以及它们在 Solana 中的翻译。

If-Else 语句

在 Solidity 中:

function ageChecker(uint256 age) public pure returns (string memory) {
    if (age >= 18) {
        return "你已满 18 岁或以上";
    } else {
        return "你未满 18 岁";
    }
}

在 Solana 中,在 lib.rs 中添加一个名为 age_checker 的新函数:

pub fn age_checker(ctx: Context<Initialize>, age: u64) -> Result<()> {
    if age >= 18 {
        msg!("你已满 18 岁或以上");
    } else {
        msg!("你未满 18 岁");
    }
    Ok(())
 }

请注意,条件 age >= 18 没有括号——这在 if 语句中是可选的。

要测试,请在 ./tests/tryrust.ts 中添加另一个 it 块:

it("年龄检查", async () => {
    // 在这里添加你的测试。
    const tx = await program.methods.ageChecker(new anchor.BN(35)).rpc();
    console.log("你的交易签名", tx);
});

运行测试后,我们应该得到以下日志:

Transaction executed in slot 77791:
  Signature: 2Av18ej2YjkRhzybbccPpwEtkw73VcBpDPZgC9iKrmf6mvwbqjA517garhrntWxKAM1ULL2eAv5vDWJ3SjnFZq6j
  Status: Ok
  Log Messages:
    Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX invoke [1]
    Program log: Instruction: AgeChecker
    Program log: 你已满 18 岁或以上
    Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX consumed 440 of 200000 compute units
    Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX success

三元运算符

在 Solidity 中将 if-else 语句分配给变量:

function ageChecker(uint256 age) public pure returns (bool a) {
    a = age % 2 == 0 ? true : false;
}

在 Solana 中,我们基本上只是将一个 if-else 语句分配给一个变量。下面的 Solana 程序与上述相同:

pub fn age_checker(ctx: Context<Initialize>, age: u64) -> Result<()> {
    let result = if age >= 18 {"你已满 18 岁或以上"} else { "你未满 18 岁" };
    msg!("{:?}", result);
    Ok(())
}

请注意,在 Rust 的三元运算符示例中,if/else 块以分号结尾,因为这是分配给一个变量的。

还要注意,内部值没有以分号结尾,因为它作为返回值返回给变量,就像在表达式而不是语句的情况下不在 Ok(()) 之后加分号一样。

如果年龄是偶数,程序将输出 true,否则为 false:

Transaction executed in slot 102358:
  Signature: 2zohZKhY56rLb7myFs8kabdwULJALENyvyFS5LC6yLM264BnkwsThMnotHNAssJbQEzQpmK4yd3ozs3zhG3GH1Gx
  Status: Ok
  Log Messages:
    Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX invoke [1]
    Program log: Instruction: AgeChecker
    Program log: true
    Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX consumed 792 of 200000 compute units
    Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX success

Rust 还有一个更强大的控制流结构叫做 match。让我们看看下面的使用 match 的示例:

pub fn age_checker(ctx: Context<Initialize>, age: u64) -> Result<()> {
    match age {
        1 => {
            // 如果年龄等于 1 则执行的代码块
            msg!("年龄是 1");
        },
        2 | 3 => {
            // 如果年龄等于 2 或 3 则执行的代码块
            msg!("年龄是 2 或 3");
        },
        4..=6 => {
            // 如果年龄在 4 到 6(包括)之间则执行的代码块
            msg!("年龄在 4 到 6 之间");
        },
        _ => {
            // 任何其他年龄的代码块
            msg!("年龄是其他值");
        }
    }
    Ok(())
}

For 循环

我们知道,for 循环允许遍历范围、集合和其他可迭代对象,在 Solidity 中是这样写的:

function loopOverSmth() public {
    for (uint256 i=0; i < 10; i++) {
        // 做一些事情...
    }
}

这在 Solana(Rust)中的等价实现为:

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    for i in 0..10 {
        // 做一些事情...
    }

    Ok(())
}

是的,简单如斯,但我们如何使用自定义步长遍历范围呢?以下是在 Solidity 中的预期行为:

function loopOverSmth() public {
    for (uint256 i=0; i < 10; i+=2) {
        // 做一些事情...

        // 将 i 增加 2
    }
}

在 Solana 中使用 step_by 的等价实现是:

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    for i in (0..10).step_by(2) {
        // 做一些事情...

        msg!("{}", i);
    }     

    Ok(())
}

运行测试后,我们应该获得以下日志:

Transaction executed in slot 126442:
  Signature: 3BSPA11TZVSbF8krjMnge1fgwNsL9odknD2twAsDeYEF39AzaJy1c5TmFCt6LEzLtvWnjzx7VyFKJ4VT1KQBpiwm
  Status: Ok
  Log Messages:
    Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX invoke [1]
    Program log: Instruction: Initialize
    Program log: 0
    Program log: 2
    Program log: 4
    Program log: 6
    Program log: 8
    Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX consumed 2830 of 200000 compute units
    Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX success

数组和向量

Rust 和 Solidity 在数组支持上有所不同。虽然 Solidity 原生支持固定和动态数组,但 Rust 仅内置支持固定数组。如果你想要动态长度的列表,请使用向量。

现在,让我们看一些示例,演示如何声明和初始化固定和动态数组。

固定数组

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    // 声明一个固定大小为 5 的 u32 数组
    let my_array: [u32; 5] = [10, 20, 30, 40, 50];

    // 访问数组元素
    let first_element = my_array[0];
    let third_element = my_array[2];

    // 声明一个固定大小为 3 的可变 u32 数组
    let mut mutable_array: [u32; 3] = [100, 200, 300];

    // 将第二个元素从 200 修改为 250
    mutable_array[1] = 250;

    // 你程序的其余逻辑

    Ok(())
}

动态数组

在 Solana 中模拟动态数组的方法涉及使用来自 Rust 标准库的 Vec(向量)。以下是一个示例:

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    // 使用 Vec 声明一个类似动态数组的结构
    let mut dynamic_array: Vec<u32> = Vec::new();

    // 向动态数组添加元素
    dynamic_array.push(10);
    dynamic_array.push(20);
    dynamic_array.push(30);

    // 访问动态数组的元素
    let first_element = dynamic_array[0];
    let third_element = dynamic_array[2];

    // 你程序的其余逻辑
    msg!("第三个元素 = {}", third_element);

    Ok(())
}

dynamic_array 变量必须声明为可变(mut),以允许进行变更(推送、弹出、覆盖索引等)。

程序在运行测试后应该记录如下内容:

Transaction executed in slot 195373:
  Signature: 4113irrcBsFbNaiZia5c84yfJpS4Hn4H1QawfUSHYoPuuQPj22JnVFtDMHmZDFkQ3vK15SrDUSTakh5fT4N8UVRf
  Status: Ok
  Log Messages:
    Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX invoke [1]
    Program log: Instruction: Initialize
    Program log: 第三个元素 = 30
    Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX consumed 1010 of 200000 compute units
    Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX success

映射

与 Solidity 不同,Solana 缺少内置的映射数据结构。然而,我们可以通过利用 Rust 标准库中的 HashMap 类型在 Solana 中复制键值映射的功能。与 EVM 链不同的是,我们在这里展示的映射是在内存中的,而不是存储中的。EVM 链没有内存哈希映射。我们将在稍后的教程中展示 Solana 中的存储映射。

让我们看看如何使用 HashMap 在 Solana 中创建一个映射。将提供的代码片段复制并粘贴到 lib.rs 文件中,并记得将程序 ID 替换为你自己的:

use anchor_lang::prelude::*;

declare_id!("53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX");

#[program]
pub mod tryrust {
    use super::*;
        // 导入 HashMap 库
    use std::collections::HashMap;

    pub fn initialize(ctx: Context<Initialize>, key: String, value: String) -> Result<()> {
        // 初始化映射
        let mut my_map = HashMap::new();

        // 向映射中添加键值对
        my_map.insert(key.to_string(), value.to_string());

        // 记录映射中与键对应的值
        msg!("我的名字是 {}", my_map[&key]);

        Ok(())
    }
}

my_map 变量也被声明为可变,以便进行编辑(即,添加/删除键→值对)。还注意我们是如何导入 HashMap 库的?

由于 initialize 函数接收两个参数,因此测试也需要进行更新:

it("已初始化!", async () => {
    // 在这里添加你的测试。
    const tx = await program.methods.initialize("name", "Bob").rpc();
    console.log("你的交易签名", tx);
});

当我们运行测试时,会看到以下日志:

Transaction executed in slot 216142:
  Signature: 5m4Cx26jaYT3c6YeJbLMDHppvki4Kmu3zTDMgk8Tao9v8b9sH7WgejETzymnHuUfr4hY25opptqniBuwDpncbnB9
  Status: Ok
  Log Messages:
    Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX invoke [1]
    Program log: Instruction: Initialize
    Program log: 我叫 Bob
    Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX consumed 2634 of 200000 compute units
    Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX success

结构体

在 Solidity 和 Solana 中,结构体用于定义可以包含多个字段的自定义数据结构。让我们看看在 Solidity 和 Solana 中的结构体示例。

在 Solidity 中:

contract SolidityStructs {

    // 在 Solidity 中定义结构体
    struct Person {
        string my_name;
        uint256 my_age;
    }

    // 创建结构体的实例
    Person person1;

    function initPerson1(string memory name, uint256 age) public {
        // 访问和修改结构体字段
        person1.my_name = name;
        person1.my_age = age;
    }
}

在 Solana 中与之对应的实现:

pub fn initialize(_ctx: Context<Initialize>, name: String, age: u64) -> Result<()> {
    // 在 Solana 中定义结构体
    struct Person {
        my_name: String,
        my_age: u64,
    }

    // 创建结构体的实例
    let mut person1: Person = Person {
        my_name: name,
        my_age: age,
    };

    msg!("{} 的年龄为 {} 岁", person1.my_name, person1.my_age);

    // 访问和修改结构体字段
    person1.my_name = "Bob".to_string();
    person1.my_age = 18;

    msg!("{} 的年龄为 {} 岁", person1.my_name, person1.my_age);

    Ok(())
}

练习 : 更新测试文件以传递两个参数 Alice 和 20 给 initialize 函数并运行测试,你应该得到以下日志:

Transaction executed in slot 324406:
  Signature: 2XBQKJLpkJbVuuonqzirN9CK5dNKnuu5NqNCGTGgQovWBfrdjRcVeckDmqtzyEPe4PP8xSN8vf2STNxWygE4BPZN
  Status: Ok
  Log Messages:
    Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX invoke [1]
    Program log: Instruction: Initialize
    Program log: Alice 的年龄为 20 岁
    Program log: Bob 的年龄为 18 岁
    Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX consumed 2601 of 200000 compute units
    Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX success

在提供的代码片段中,Solidity 实现将结构体的实例存储在存储中,而在 Solana 实现中,所有操作都发生在初始化函数中,且没有在链上存储。存储将在以后的教程中讨论。

Rust 中的常量

在 Rust 中声明常量变量非常简单。使用 const 关键字而不是 let 关键字。可以在 #[program] 块外声明这些常量。

use anchor_lang::prelude::*;

declare_id!("EiR8gcMCX11tYMRfoZ2vyheZsZ2NvdUTvYrRAUvTtYnL");

// *** 在这里声明常量 ***
const MEANING_OF_LIFE_AND_EXISTENCE: u64 = 42;

#[program]
pub mod tryrust {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        msg!(&format!("终极问题的答案: {}", MEANING_OF_LIFE_AND_EXISTENCE)); // 这里的新行
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize {}

usize 类型和类型转换

大多数时候我们可以假定在 Solana 中的无符号整数类型为 u64,但在测量列表的长度时,有一个例外:它将是 usize 类型。你需要将变量转换,如以下 Rust 代码所示:

use anchor_lang::prelude::*;

declare_id!("EiR8gcMCX11tYMRfoZ2vyheZsZ2NvdUTvYrRAUvTtYnL");

#[program]
pub mod usize_example {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {

       let mut dynamic_array: Vec<u32> = Vec::from([1,2,3,4,5,6]);
       let len = dynamic_array.len(); // 这个类型为 usize

       let another_var: u64 = 5; // 这个类型为 u64

       let len_plus_another_var = len as u64 + another_var;

       msg!("结果是 {}", len_plus_another_var);

       Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize {}

Try Catch

Rust 没有 try catch。故障预期会返回错误(就像我们在 Solana 回滚和错误的教程中所做的)或对于不可恢复性错误引发恐慌。

练习 : 编写一个 Solana / Rust 程序,接受一个 u64 向量,遍历它,将所有偶数推送到另一个向量中,然后打印新的向量。

转载:https://learnblockchain.cn/article/11406

Solana基础 - Solana程序是可升级的,并且没有构造函数

本文详细讲解了如何在Solana上使用Anchor框架部署程序,并解释了Solana程序的可变性和与Ethereum的差异。通过代码示例和命令行操作,展示了程序的部署、升级和测试过程。

在本教程中,我们将从幕后看看 Anchor 如何部署 Solana 程序。

让我们看看 Anchor 在我们运行 anchor init deploy_tutorial 时为我们创建的测试文件:

describe("deploy_tutorial", () => {
  // 配置客户端以使用本地集群。
  anchor.setProvider(anchor.AnchorProvider.env());

  const program = anchor.workspace.DeployTutorial as Program<DeployTutorial>;

  it("Is initialized!", async () => {
    // 在此添加你的测试。
    const tx = await program.methods.initialize().rpc();
    console.log("你的交易签名", tx);
  });
});

它生成的启动程序应该很熟悉:

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod deploy_tutorial {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize {}

上述程序在哪里以及何时被部署?

合约唯一合理的部署地点是在测试文件中的这一行:

const program = anchor.workspace.DeployTutorial as Program<DeployTutorial>;

但这并没有意义,因为我们本该期望那是一个异步函数。

Anchor 在后台默默地部署程序。

Solana 程序默认没有构造函数

对于来自其他面向对象语言的人来说,这可能看起来不寻常。Rust 没有对象或类。

在以太坊智能合约中,构造函数可以配置存储或设置字节码和不可变变量。

那么“部署步骤”到底在哪里?

(如果你仍在运行 Solana 验证器和 Solana 日志,建议你重启并清除这两个终端)

让我们进行常规设置。创建一个名为 program-deploy 的新 Anchor 项目,并确保在其他终端中运行验证器和日志。

与其运行 anchor test,不如在终端中运行以下命令:

anchor deploy

Anchor 测试的截图

在上面的日志截图中,我们可以看到程序被部署的时刻。

现在,令人感兴趣的部分是。再次运行 anchor deploy

Anchor 测试升级后的截图

程序被部署到同一地址,但这次是 升级,而非重新部署。

程序 ID 没有改变,程序被覆盖了

Solana 程序默认是可变的

这可能对以太坊开发者来说是一个震惊,因为在以太坊中假设是不可变的。

如果作者可以随意更改程序,那么这个程序有什么意义?确实可以使 Solana 程序不可变。假设作者会先部署可变版本,随着时间的推移且未发现任何错误后,再将其重新部署为不可变。

在功能上,这与管理员控制的代理没有任何区别,后期所有者将所有权放弃给零地址。但可以说,Solana 模型更干净,因为以太坊代理可能会出问题。

另一个含义是:Solana 不需要 delegatecall,因为它不需要

在 Solidity 合约中,delegatecall 的主要用途是通过发出 delegatecalls 到新实现合约来升级代理合约的功能能力。然而,由于 Solana 中程序的字节码可以升级,因此无需调用实现合约。

另一个推论是:Solana 没有 Solidity 那种不可变变量(只能在构造函数中设置的变量)

在不重新部署程序的情况下运行测试

由于 Anchor 默认重新部署程序,让我们演示如何在不重新部署的情况下运行测试。

将测试更改为以下内容:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";

import fs from 'fs'
let idl = JSON.parse(fs.readFileSync('target/idl/deployed.json', 'utf-8'))

describe("deployed", () => {
  // 配置客户端以使用本地集群。
  anchor.setProvider(anchor.AnchorProvider.env());

  // 更改为你的 programID
  const programID = "6p29sM4hEK8ZFT5AvsGJQG5nKUtHBKs13iVL6juo5Uqj";
  const program = new Program(idl, programID, anchor.getProvider());

  it("Is initialized!", async () => {
    // 在此添加你的测试。
    const tx = await program.methods.initialize().rpc();
    console.log("你的交易签名", tx);
  });
});

在你运行测试之前,建议清除 Solana 日志的终端并重启 solana-test-validator。

现在,使用以下测试命令:

anchor test --skip-local-validator --skip-deploy

现在查看日志终端:

跳过部署的 Anchor 测试截图

我们看到初始化指令已被执行,但程序既没有被部署也没有被升级,因为我们在 anchor test 中使用了 --skip-deploy 参数。

练习:要查看程序的字节码实际上发生了变化,部署两个打印不同 msg! 值的合约。具体步骤如下:

  1. lib.rs 中更新 initialize 函数,包括一个 msg! 语句,将字符串写入日志。
  2. anchor deploy
  3. anchor test --skip-local-validator --skip-deploy
  4. 检查日志以查看记录的消息
  5. 重复步骤 1 – 4,但更改 msg! 中的字符串
  6. 验证程序 ID 没有改变

你应该观察到消息字符串的变化,但程序 ID 保持不变。

总结

  • Solana 没有构造函数,程序“只是部署”。
  • Solana 没有不可变变量。
  • Solana 没有 delegatecall,程序可以“被更新”。

转载:https://learnblockchain.cn/article/11415