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


Solana基础 - Solana中的Require、Revert和自定义错误


文章详细介绍了在 Solana 的 Anchor 框架中如何处理函数参数的限制,类似于以太坊中的 require 语句。通过代码示例展示了如何使用 require! 宏和错误处理机制来确保函数参数的有效性,并解释了 Solana 和以太坊在错误处理上的差异。

在以太坊中,我们经常会看到一个 require 语句来限制函数参数可以接受的值。考虑以下示例:

function foobar(uint256 x) public {
    require(x < 100, "I'm not happy with the number you picked");
    // 其余的函数逻辑
}

在上面的代码中,如果 foobar 被传递一个 100 或更大的值,交易将会回退。

我们如何在 Solana 中实现这一点,或者更具体地说,在 Anchor 框架中实现?

Anchor 对 Solidity 的自定义错误和 require 语句有等效的实现。它们的 文档 在这一主题上相当不错,但我们也将解释如何在函数参数不符合预期时停止交易。

下面的 Solana 程序有一个 limit_range 函数,只接受 10 到 100 之间(包括)的值:

use anchor_lang::prelude::*;

declare_id!("8o3ehd3XnyDocd9hG1uz5trbmSRB7gaLaE9BCXDpEnMY");

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

    pub fn limit_range(ctx: Context<LimitRange>, a: u64) -> Result<()> {
        if a < 10 {
            return err!(MyError::AisTooSmall);
        }
        if a > 100 {
            return err!(MyError::AisTooBig);
        }
        msg!("Result = {}", a);
        Ok(())
    }
}

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

#[error_code]
pub enum MyError {
    #[msg("a is too big")]
    AisTooBig,
    #[msg("a is too small")]
    AisTooSmall,
}

以下代码单元测试了上述程序:

import * as anchor from "@coral-xyz/anchor";
import { Program, AnchorError } from "@coral-xyz/anchor"
import { Day4 } from "../target/types/day4";
import { assert } from "chai";

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

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

  it("Input test", async () => {
    // 在此添加测试。
    try {
      const tx = await program.methods.limitRange(new anchor.BN(9)).rpc();
      console.log("Your transaction signature", tx);
    } catch (_err) {
      assert.isTrue(_err instanceof AnchorError);
      const err: AnchorError = _err;
      const errMsg =
        "a is too small";
      assert.strictEqual(err.error.errorMessage, errMsg);
      console.log("Error number:", err.error.errorCode.number);
    }

    try {
      const tx = await program.methods.limitRange(new anchor.BN(101)).rpc();
      console.log("Your transaction signature", tx);
    } catch (_err) {
      assert.isTrue(_err instanceof AnchorError);
      const err: AnchorError = _err;
      const errMsg =
        "a is too big";
      assert.strictEqual(err.error.errorMessage, errMsg);
      console.log("Error number:", err.error.errorCode.number);
    }
  });
});

练习

  1. 你注意到错误编号有什么模式?如果你改变 enum MyError 中错误的顺序,错误代码会发生什么?
  2. 使用这段代码块,将新函数和错误添加到现有代码中:
#[program]
pub mod day_4 {
    use super::*;

    pub fn limit_range(ctxThen : Context<LimitRange>, a: u64) -> Result<()> {
        require!(a >= 10, MyError::AisTooSmall);
        require!(a <= 100, MyError::AisTooBig);
        msg!("Result = {}", a);
        Ok(())
    }

    // 新函数
    pub fn func(ctx: Context<LimitRange>) -> Result<()> {
        msg!("Will this print?");
        return err!(MyError::AlwaysErrors);
    }
}

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

#[error_code]
pub enum MyError {
    #[msg("a is too small")]
    AisTooSmall,
    #[msg("a is too big")]
    AisTooBig,
    #[msg("Always errors")]  // 新错误,你认为错误代码是什么?
    AlwaysErrors,
}

并添加这个测试:

it("Error test", async () => {
    // 在此添加测试。
    try {
      const tx = await program.methods.func().rpc();
      console.log("Your transaction signature", tx);
    } catch (_err) {
      assert.isTrue(_err instanceof AnchorError);
      const err: AnchorError = _err;
      const errMsg =
        "Always errors";
      assert.strictEqual(err.error.errorMessage, errMsg);
      console.log("Error number:", err.error.errorCode.number);
    }
  });

在你运行这个之前,你认为新的错误代码将是什么?

以太坊和 Solana 在处理无效参数的交易停止方式的显著区别是,Ethereum 触发回退,而 Solana 返回一个错误。

使用 require 语句

有一个 require! 宏,概念上与 Solidity 的 require 相同,我们可以用它来简化我们的代码。将 if 检查(需要三行)切换到 require! 调用,之前的代码转换如下:

pub fn limit_range(ctx: Context<LimitRange>, a: u64) -> Result<()> {
    require!(a >= 10, Day4Error::AisTooSmall);
    require!(a <= 100, Day4Error::AisTooBig);

    msg!("Result = {}", a);
    Ok(())
}

在以太坊中,我们知道如果函数回退,则不会记日志,即使回退是在记录之后发生。例如,下面合约中的 tryToLog 调用不会记录任何内容,因为函数回退:

contract DoesNotLog {
    event SomeEvent(uint256);

    function tryToLog() public {
        emit SomeEvent(100);
        require(false);
    }
}

练习:如果你在 Solana 程序函数中返回错误语句之前放置 msg! 宏,结果会怎么样?如果你用 Ok(()) 替代 return err!,结果会怎么样?下面的函数在返回错误时先记录一些信息,看看 msg! 宏的内容是否被记录。

pub fn func(ctx: Context<ReturnError>) -> Result<()> {
    msg!("Will this print?");
    return err!(Day4Error::AlwaysErrors);
}

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

#[error_code]
pub enum Day4Error {
    #[msg("AlwaysErrors")]
    AlwaysErrors,
}

在底层,require! 宏与返回错误没有什么不同,它只是语法糖。

预期结果是,当你返回 Ok(()) 时,“Will this print?” 会被打印,而当你返回错误时则不会打印。

关于 Solana 和 Solidity 在处理错误方面的区别

在 Solidity 中,require 语句通过 revert 操作码停止执行。Solana 并不会停止执行,而是简单返回一个不同的值。这类似于操作系统如何在成功时返回 0 或 1。如果返回了 0(相当于返回 Ok(())),那么一切顺利。

因此,Solana 程序应该始终返回某个值——要么是 Ok(()) 或者是 Error

在 Anchor 中,错误是具有 #[error_code] 属性的枚举。

请注意,Solana 中的所有函数返回类型都是 Result<()>。一个 result 是一种可能是 Ok(()) 或错误的类型。

问题与解答

为什么 Ok(()) 结尾没有分号?

如果你添加它,你的代码将无法编译。如果 Rust 中最后一条语句没有分号,那么该行的值会被返回。

为什么 Ok(()) 有一对额外的括号?

() 在 Rust 中表示“单位”,你可以将其视为 C 中的 void 或 Haskell 中的 Nothing。这里,Ok 是包含单位类型的枚举。这就是返回的内容。在 Rust 中,不返回任何内容的函数隐式返回单位类型。没有分号的 Ok(())return Ok(()) 在语法上是等价的。请注意末尾的分号。

上述 if 语句为何缺少括号?

在 Rust 中,这些是可选的。

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


Solana基础 - Solana Anchor 程序 IDL


本文详细介绍了 Solana 如何使用 IDL(接口定义语言)来描述如何与 Solana 程序交互,并通过 Anchor 框架自动生成 IDL 文件。文章还展示了如何通过 Rust 编写 Solana 程序,并通过 TypeScript 单元测试进行验证。

IDL(接口定义语言)是一个描述如何与Solana程序交互的JSON文件。它是由Anchor框架自动生成的。

名为“initialize”的函数没有什么特别之处——这是Anchor选择的一个名称。在本教程中,我们将学习Typescript单元测试如何能够“找到”适当的函数。

让我们创建一个新的项目,名为anchor-function-tutorial,并将initialize函数中的名称更改为boaty_mc_boatface,保持其他一切不变。

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

现在让我们将测试更改为如下:

it("调用boaty mcboatface", async () => {
  // 在这里添加你的测试。
  const tx = await program.methods.boatyMcBoatface().rpc();
  console.log("你的交易签名", tx);
});

现在运行测试 anchor test --skip-local-validator

它按预期运行。那么这个魔法是如何工作的呢?

测试如何知道initialize函数?

当Anchor构建Solana程序时,它会创建一个IDL(接口定义语言)。

它存储在target/idl/anchor_function_tutorial.json中。这个文件被称为anchor_function_tutorial.json是因为anchor_function_tutorial是程序的名称。请注意,Anchor将连字符转换为下划线!

让我们打开它。

{
  "version": "0.1.0",
  "name": "anchor_function_tutorial",
  "instructions": [
    {
      "name": "boatyMcBoatface",
      "accounts": [],
      "args": []
    }
  ]
}

“instructions”的列表是该程序支持的公开函数,粗略上等同于以太坊合约中的外部和公共函数。Solana中的IDL文件在与合约的交互方式上,与Solidity中的ABI文件具有相似的角色。

我们之前看到函数不接受任何参数,这就是为什么args列表为空。稍后我们将解释“accounts”是什么。

一个显著的区别是:Rust中的函数是蛇形命名的,但Anchor在JavaScript中将它们格式化为驼峰命名。这是为了尊重这些语言的命名约定:Rust倾向于使用蛇形命名,而JavaScript通常使用驼峰命名。

这个JSON文件是“methods”对象知道支持哪些函数的方式。

当我们运行测试时,我们期望它通过,这意味着该测试正确地调用了Solana程序:

运行Solana测试套件

练习:boaty_mc_boatface函数添加一个接收u64的参数。再次运行anchor build。然后再次打开target/idl/anchor_function_tutorial.json文件。它会改变吗?

现在让我们开始创建一个Solana程序,该程序具有基本加法和减法的函数并打印结果。Solana函数不能像Solidity那样返回值,因此我们将不得不打印它们。(Solana有其他方法传递值,以后我们会讨论这些)。让我们创建两个函数,像这样:

pub fn add(ctx: Context<Initialize>, a: u64, b: u64) -> Result<()> {
  let sum = a + b;
  msg!("和是 {}", sum);  
    Ok(())
}

pub fn sub(ctx: Context<Initialize>, a: u64, b: u64) -> Result<()> {
  let difference = a - b;
  msg!("差是 {}", difference);  
    Ok(())
}

并将我们的单元测试更改为如下:

it("应该加法", async () => {
  const tx = await program.methods.add(new anchor.BN(1), new anchor.BN(2)).rpc();
  console.log("你的交易签名", tx);
});

it("应该减法", async () => {
  const tx = await program.methods.sub(new anchor.BN(10), new anchor.BN(3)).rpc();
  console.log("你的交易签名", tx);
});

练习:muldivmodulo实现类似的函数,并编写单元测试以触发每一个。

那么Initialize结构体呢?

现在还有一个狡猾的事情发生。我们已经保持Initialize结构体不变,并在函数间重新使用它。同样,名称并不重要。让我们将结构体名称更改为Empty并重新运行测试。

//...
  // 在此处更改结构体名称
    pub fn add(ctx: Context<Empty>, a: u64, b: u64) -> Result<()> {
        let sum = a + b;
        msg!("和是 {}", sum);
        Ok(())
    }
//...

// 在这里也更改结构体名称
#[derive(Accounts)]
pub struct Empty {}

再次强调,名称Empty在这里完全是任意的。

练习: 将结构体名称Empty更改为BoatyMcBoatface并重新运行测试。

什么是#[derive(Accounts)]结构体?

这个#语法是Anchor框架定义的Rust属性。我们将在后面的教程中对此进行进一步说明。现在,我们想关注IDL中的accounts键以及它与程序中定义的结构体之间的关系。

Accounts IDL键

下面我们截图展示以上程序的IDL。所以我们可以看到Rust属性#[derive(Accounts)]中的“Accounts”和IDL中的“accounts”键之间的关系:

IDL的截图

在我们的例子中,上面JSON IDL中的accounts键(用紫色箭头标记)是空的。但对于大多数有用的Solana事务,这并不是这样,稍后我们将学习。

由于BoatyMcBoatface的账户结构体为空,因此IDL中的账户列表也为空。

现在让我们看看当结构体非空时发生什么。复制下面的代码并替换lib.rs中的内容。

use anchor_lang::prelude::*;

declare_id!("8PSAL9t1RMb7BcewhsSFrRQDq61Y7YXC5kHUxMk5b39Z");

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

    pub fn non_empty_account_example(ctx: Context<NonEmptyAccountExample>) -> Result<()> {
        Ok(())
    }
}

#[derive(Accounts)]
pub struct NonEmptyAccountExample<'info> {
    signer: Signer<'info>,
    another_signer: Signer<'info>,
}

现在运行anchor build——让我们看看返回的新IDL。

{
  "version": "0.1.0",
  "name": "anchor_function_tutorial",
  "instructions": [
    {
      "name": "nonEmptyAccountExample",
      "accounts": [
        {
          "name": "signer",
          "isMut": false,
          "isSigner": true
        },
        {
          "name": "anotherSigner",
          "isMut": false,
          "isSigner": true
        }
      ],
      "args": []
    }
  ],
  "metadata": {
    "address": "8PSAL9t1RMb7BcewhsSFrRQDq61Y7YXC5kHUxMk5b39Z"
  }
}

请注意“accounts”不再为空,而是用结构体中的字段“signer”和“anotherSigner”填充。(请注意,another_signer从蛇形命名转换为驼峰命名)。IDL已经更新以匹配我们刚刚更改的结构体,特别是我们添加的账户数量。

我们将在后续教程中进一步深入“Signer”的内容,但现在你可以将其视为与以太坊中的tx.origin的类似物。

另一个程序和IDL示例。

为了总结我们到目前为止学到的一切,让我们构建另一个具有不同函数和账户结构体的程序。

use anchor_lang::prelude::*;

declare_id!("8PSAL9t1RMb7BcewhsSFrRQDq61Y7YXC5kHUxMk5b39Z");

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

    pub fn function_a(ctx: Context<NonEmptyAccountExample>) -> Result<()> {
        Ok(())
    }

    pub fn function_b(ctx: Context<Empty>, firstArg: u64) -> Result<()> {
        Ok(())
    }
}

#[derive(Accounts)]
pub struct NonEmptyAccountExample<'info> {
    signer: Signer<'info>,
    another_signer: Signer<'info>,
}

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

现在用anchor build构建它

让我们再次查看IDL文件target/idl/anchor_function_tutorial.json,并将这些文件并排放置:

IDL文件截图结果

你能看到IDL文件与上面的程序之间的关系吗?

函数function_a没有参数,这在IDL中显示为args键下的空数组。

它的Context采用NonEmptyAccountExample结构体。该结构体NonEmptyAccountExample有两个签名字段:signeranother_signer。请注意,这些在IDL的function_a的accounts键中重复作为元素。你可以看到Anchor将Rust的蛇形命名转换为IDL中的驼峰命名。

Anchor 0.30更新 Anchor不再自动执行此转换(发布说明)。

函数function_b接受一个u64参数。它的上下文结构体是空的,因此function_b在IDL中的accounts键是一个空数组。

通常,我们期望IDL的accounts键中的项数组与函数在其ctx参数中采用的账户结构体的键匹配。

总结

在本章中:

  • 我们了解到Solana使用IDL(接口定义语言)来显示如何与Solana程序交互以及IDL中出现的字段。
  • 我们介绍了通过#[derive(Accounts)]修改的结构体以及它如何与函数参数相关联。
  • Anchor将Rust中的snake_case函数转化为Typescript测试中的camelCase函数。

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