Cairo 语言介绍

本文介绍了Cairo编程语言的基础知识,包括Cairo在Starknet中的作用、开发环境的搭建、语言的语法要点和数据类型(felt252、整数、bool)、复合类型(元组、结构体、枚举)、字符串处理、控制流以及数组和字典的使用。着重讲解了Cairo与Rust相似的语法结构,以及Cairo中数据类型和错误处理的特殊性。

Cairo 是一种领域特定编程语言,专为可证明、可验证的计算而设计,尤其是在像 Starknet 这样的零知识系统(以太坊上的二层网络 (L2))中。

Cairo 专门用于支持基于 STARK 的程序执行证明。这允许在链下高效地验证计算,然后通过简洁、无需信任的证明在链上进行证明。

虽然该语言是为区块链用例而创建的,但 Cairo 足够通用,可以支持具有密码完整性的链下可验证计算。与 Solidity 不同,Cairo 可以在智能合约的上下文之外运行。

本文概述了该语言的工作原理。我们将介绍主要数据类型、控制流机制和常用数据结构。

Cairo 在 Starknet 中的角色

Starknet 使用 STARK(可扩展的知识透明论证)来实现链下复杂计算的执行,同时保留以太坊的安全性和去中心化。

所有 Starknet 智能合约均以 Cairo 编写。这些合约编译成名为 Sierra 的中间表示形式,然后编译成 Casm(Cairo 汇编),这是一种 CairoVM 可以理解的低级语言。CairoVM 确定性地执行 Casm 指令,生成执行跟踪,并确保程序遵循 STARK 证明生成所需的约束。

本文介绍了 Cairo 编程语言的基础知识,并展示了如何在智能合约上下文之外将其用作通用语言。在继续下一节之前,请按照以下步骤设置开发环境。

设置开发环境

  1. 创建一个空目录并进入该目录。

该目录可以使用任何名称,在本例中,它被称为 cairo_playground

mkdir cairo_playground && cd cairo_playground

Copy

  1. cairo_playground 目录中创建一个源文件夹:
mkdir src

Copy

  1. src 文件夹中,创建两个文件:playground.cairo名称可以不同)和 lib.cairo
touch src/playground.cairo && touch src/lib.cairo

Copy

  1. 将以下内容添加到新文件中。

playground.cairo:

##[executable]
fn main() {
       // Print message to terminal.
       println!("Hello from Rareskills!!!");
}

Copy

lib.cairo:

mod playground;

Copy

  1. 在项目根目录(cairo_playground)中创建一个 Scarb.toml 文件:
touch Scarb.toml

Copy

添加以下内容:

[package]
name = "cairo_playground" # HAS TO BE THE NAME OF THE ROOT DIRECTORY
version = "0.1.0"
edition = "2024_07"

[cairo]
enable-gas = false

[dependencies]
cairo_execute = "2.12.0"

[[target.executable]]
## A PATH TO THE FUNCTION WITH THE #[executable] ANNOTATION
## <root-directory>::<file-name>::<function-name>
function = "cairo_playground::playground::main"

Copy

#[executable] 注解将在后面的小节中解释。

完成设置后,目录应具有类似于以下的结构:

Cairo project folder structure

最后,要测试 Cairo 程序(playgroung.cairo),请运行以下命令:

scarb execute

Copy

Cairo 语言语法要点 & 数据类型

Cairo 的语法灵感来自 Rust,但针对可证明、可验证的计算进行了优化。在我们探索数据类型和逻辑之前,必须了解基本构建块,例如 Cairo 中的变量和函数声明。

声明变量:letmutconst

Cairo 是一种静态类型语言,所有变量都必须在编译时声明其类型。let 关键字用于变量声明期间,后跟一个名称、一个冒号、一个类型,然后是一个值:

// let <NAME>: <dataType> = <value>;

let count: u8 = 42;
let name: felt252 = 'bob';
let active: bool = true;

Copy

变量可变性

默认情况下,Cairo 中的变量是不可变的。分配后无法修改。要启用修改,可以使用 mut 关键字,如下所示。

// let mut <NAME>: <Type> = <value>;

let mut total: u128 = 0;
total = total + 10;

Copy

声明常量

在 Cairo 中,const 关键字用于定义在编译期间已知且在运行时无法更改的固定值。常量被硬编码到程序源代码中,这意味着它们不占用内存,并且访问它们的运行时成本为零。

以下是如何声明常量:

// const <NAME>: <Type> = <value>;
const DECIMALS: u8 = 18;

Copy

在 Cairo 中声明函数

Cairo 中的函数使用 fn 关键字声明。它们支持参数传递、返回值并遵循严格的类型系统。

让我们看一下下面的 multiply 函数。该函数接受两个类型为 felt252 的参数(x,y),并且还在箭头(-> felt252 {..)之后返回一个 felt252 值,如下所示:

// the function takes two parameters: `x` and `y`,
// both of type `felt252`, and returns a value of type `felt252`.
fn multiply(x: felt252, y: felt252) -> felt252 {
    // The result of the multiplication expression is implicitly returned.
    x * y
}

##[executable]
fn main() {
   // Calls the multiply function with literal felt252 values: 3 and 4.
   let result = multiply(3, 4);  // result = 12
   println!("This is the value of multiply(3, 4): {}", result);
}

Copy

在函数中,我们可以使用 return 关键字显式地返回值。但是,如上面的 multiply 函数所示,也可以隐式地返回值。当函数体中的最后一个表达式没有后跟分号时,其结果将自动返回。

\#[executable] 属性解释

#[executable] 属性将函数标记为可以由 Cairo 运行器直接调用的入口点。Cairo 运行器是负责执行已编译 Cairo 代码的程序,它查找标记有 #[executable] 的函数作为起始点。

如果没有此属性,该函数将不会作为顶级入口点公开,并且无法自行执行。

在上面的示例中,multiply 函数是一个常规函数:它可以被程序中的其他函数调用,但不能自行直接执行。相比之下,main 函数标有 #[executable] 属性,该属性将其指定为可以由 Cairo 运行器直接运行的入口点。

要执行上面的 main,请在你的终端中输入 scarb execute 命令。输出如下所示:

scarb exercute command in the termal execution and print result

注意: #[executable] 属性不适用于智能合约。 事实上,上面的示例不是智能合约,而是一个常规的 Cairo 程序。这是可能的,因为与 Solidity 不同,Cairo 是一种通用语言,可以在智能合约的上下文之外执行。使用 Cairo 运行器,你可以编写和运行独立的程序,而无需将它们部署到 Starknet。

在 Cairo 函数中打印数据

现在你已经了解了如何运行独立的 Cairo 程序,让我们来探讨 Cairo 如何允许你将值打印到终端。

该语言提供了两个来打印标准数据类型:

  • println!(它打印输出,后跟一个换行符)
  • print!(它打印行内输出,没有换行符)。

这两个宏都至少接受一个参数:一个 ByteArray 字符串,该字符串可能包含零个或多个占位符(例如,{}{var}),后跟一个或多个参数,这些参数按顺序或按名称替换到这些占位符中。

请参阅以下代码以了解如何格式化 print!println!

##[executable]
fn main() {
    let version = 2;
    let released = 2023;

    //contains one parameter: a ByteArray string
    println!("Welcome to the Cairo programming language!");

    // Positional formatting
    println!("Version: {}, Released in: {}", version, released);

    // Mixing named and positional placeholders
    println!("Cairo v{} was released in {released}", version);
}

Copy

Cairo 中的数据类型

现在我们已经了解了如何在 Cairo 中声明变量,让我们来探索 Cairo 中的主要数据类型。

1. felt252:核心数值类型

在 Cairo 中,最基本的数据类型是字段元素,表示为 felt252。它是该语言中的默认数值类型,表示 Cairo VM 使用的素数域的元素。该字段如下所示:

                                                               $p = 2^{251} + 17*2^{192} + 1$

Copy

这意味着 felt252 值可以从 0 到 $p - 1$。对 felt252 值执行的所有算术运算都是此字段上的模算术。当结果超过 p−1 时,它会回绕到 0,类似于时钟上的小时回绕。

以下代码显示了大于 felt252 最大值(p - 1)的算术运算如何回绕到零。

// The actual maximum value for felt252 in Cairo (p - 1 where p is the prime modulus)
const MAX_FELT252: felt252 = 3618502788666131213697322783095070105623107215331596699973092056135872020480;

##[executable]
fn main() {
    let mut anyvalue = -5;
    let result = MAX_FELT252 + anyvalue;

    // When adding -5 to MAX_FELT252, we get MAX_FELT252 - 5 (still less than p)
    if result != 0 {
        println!("Result is less than p: {}", result);
        println!("This means MAX_FELT252 - {} did not wrap to 0", 5);
    }

    // Now let's try adding a positive value that will cause wrapping
    anyvalue = 1; // Reset to 1 to test wrapping
    let wrap_result = MAX_FELT252 + anyvalue;
    if wrap_result == 0 {
        println!("Confirmed: MAX_FELT252 + {} wraps to 0", anyvalue);
    } else {
        println!("Unexpected: MAX_FELT252 + {} = {}", anyvalue, wrap_result);
    }

    // Test with a larger positive value
    anyvalue = 10;
    let wrap_result_10 = MAX_FELT252 + anyvalue;
    println!("MAX_FELT252 + {} = {}", anyvalue, wrap_result_10);
}

Copy

终端输出:

image.png

由于这种回绕行为,如果不小心处理,可能会发生由意外回绕引起的算术错误(即,溢出)。

为了解决这个问题,Cairo 还提供了固定宽度整数类型 u8..u256 和有符号整数 i8..i256,它们会在运行时检查溢出/下溢。如果操作尝试超出有效范围,程序将发生 panic(即,由于错误而停止)。

在需要进行极端优化时,请使用 felt252,因为所有其他类型最终都在底层表示为 felt252。对于一般算术和安全性,建议使用整数类型,因为它们提供内置的溢出保护。

felt252 中的除法

Cairo 的 felt252 类型中的字段元素在有限字段算术原则下运行,这意味着它们不支持余数或像固定宽度整数这样的传统整数除法。相反,除法 a / b 计算为 a × b^(-1) mod P,其中 b 是一个非零值。 b⁻¹ 被称为 bP 的模乘法逆元。

如果 a = 1 且 b=2,我们将有 1 × 2⁻¹

                                 Since,   $2 × (P+1)/2 = P+1 \equiv 1 \pmod P$.

                                 $1 ÷ 2 ≡ (P + 1)/2 \pmod P$.

Copy

在下面的代码块中,我们将展示上面的证明是正确的,并了解 felt252 有余数或没有余数的除法的行为。

use core::felt252_div;

##[executable]
fn main() {
        // (p + 1) / 2
    let P_plus_1_halved = 1809251394333065606848661391547535052811553607665798349986546028067936010241;

    assert!(felt252_div(1, 2) == P_plus_1_halved);
    println!("this is the value of felt252_div(1, 2): {}", felt252_div(1, 2));

    //divisions with zero remainder
    assert!(felt252_div(2, 1) == 2);
    println!("this is the value of felt252_div(2, 1): {}", felt252_div(2, 1));
    assert!(felt252_div(15, 5) == 3);
    println!("this is the value of felt252_div(15, 5): {}", felt252_div(15, 5));

    //division with remainder
    println!("this is the value of felt252_div(7, 3): {}", felt252_div(7, 3));
    println!("this is the value of felt252_div(4, 3): {}", felt252_div(4, 3));

}

Copy

终端输出:

Terminal output from running Cairo program

如上面的测试所示,Cairo 字段中的除法在没有余数时的工作方式与整数除法类似。

但是,当除法有余数时,它会不同。例如,如果我们用 3 除 4,我们不是在问“3 能整除 4 多少次”,而是在问“什么值乘以 3 在这个字段中得到 4?”

                                                       $n\cdot 3 \equiv 4 \pmod p$

Copy

在字段算术中,答案是 4 与 3 的模逆元的乘积。这确保了结果在乘以 3 时,会产生该字段的素数的模 4。

当变量在没有类型的情况下声明时会发生什么?

在 Cairo 中,当你分配一个数值字面量而不指定类型时,如下所示,编译器会自动假定该值的类型为 felt252

let count = 42;
// count's is of type felt252

Copy

这是因为 felt252 是 Cairo 的默认数值类型,类似于在某些其他语言中默认使用 int 的方式。

2. 无符号整数:u8..u256

在 Cairo 中,固定宽度整数,如 u8u16u32u64u128 都是较大的 felt252 类型的子集,这意味着它们的值可以完全容纳在 felt252 中;它们可以安全地表示为字段元素,因为它们的最大值小于 felt252 的最大值。

表 1:无符号整数范围

类型 大小(位) 范围
u8 8 位 0 到 255
u64 64 位 0 到 2⁶⁴ – 1
u128 128 位 0 到 2¹²⁸ – 1
u256 256 位 0 到 2²⁵⁶ – 1(复合

如表 1 所示,u256 超过了 felt252 的最大值,因此无法容纳在单个字段元素中。在底层,Cairo 将 u256 表示为由两个 u128 值组成的结构体:

struct u256 {
    low: u128,  // Least significant 128 bits
    high: u128, // Most significant 128 bits
}

Copy

例如,类型为 u256 的值 7 如下所示分为两半:

let value: u256 = 7;
//    __________________________256-bit_____________________________
//   |                                                              |
// 0x0000000000000000000000000000000000000000000000000000000000000007

//    ________high 128-bit__________   __________low 128-bit_________
//   |                              | |                              |
// 0x00000000000000000000000000000000 00000000000000000000000000000007

Copy

3. 有符号整数:i8i16i32i64i128

Cairo 中的有符号整数使用小写字母 i 后跟位宽来编写,例如 i8i16i32i64i128。每个有符号类型都可以表示以零为中心的值范围,使用以下公式计算:

                                                $Range=−2 ^{n−1}$    to    $2^{n−1} −1$

Copy

例如,i8 的范围是 -128..127

Cairo 中有符号和无符号整数的溢出/下溢行为

在下面的代码中,我们使用 u256(作为参考)来测试整数(有符号和无符号)在遇到溢出/下溢时的行为。

// Maximum value for u256: 2^256 - 1
const MAX_U256: u256 = 115792089237316195423570985008687907853269984665640564039457584007913129639935;

fn add_u256(a: u256, b: u256) -> u256 {
    a + b
}

fn sub_u256(a: u256, b: u256) -> u256 {
    a - b
}

fn multiply_u256(a: u256, b: u256) -> u256 {
    a * b
}

##[executable]
fn main() {
    println!("Testing u256 panic behavior");
    println!("MAX_U256: {}", MAX_U256);

    // Note: calls that panic will terminate the entire program immediately
    //(comment out all other panic calls to see each result individually)

    let result = sub_u256(MAX_U256, 1);
    println!("result(less than MAX_U256): {}", result);

    // This will panic on underflow
    let result = sub_u256(0, 1);
    println!("Underflow result: {}", result);
    //returns -> error: Panicked with 0x753235365f616464204f766572666c6f77 ('u256_add Overflow').

    // This will panic on overflow
    let result = add_u256(MAX_U256, 1);
    println!("Overflow result: {}", result);
    //returns -> error: Panicked with 0x753235365f616464204f766572666c6f77 ('u256_add Overflow').

    // This will also panic on overflow
    let mult_result = multiply_u256(MAX_U256, 2);  //
    println!("Mult result: {}", mult_result);
    //returns -> error: Panicked with 0x753235365f6d756c204f766572666c6f77 ('u256_mul Overflow').
}

Copy

如上所示,所有超过 u256 最大值的算术运算都会导致 panic 错误。

4. bool:true 或 false

bool 用于表示逻辑值:truefalse。在内部,bool 编码为值为 0(false)或 1(true)的 felt252

Cairo 复合类型

复合类型将多个值组合在一起,从而可以在 Cairo 中实现结构化和富有表现力的数据表示。

元组

元组保存不同类型的固定值集。它们对于从函数返回多个值或临时分组相关数据很有用。

let pair: (felt252, bool) = (42, true);
let (num, flag) = pair;  // Destructuring

// Accessing tuple elements by index
let first_element = pair.0;
let second_element = pair.1;

Copy

结构体

结构体是具有命名字段的自定义数据类型。

struct Point {
    x: felt252,
    y: felt252,
}

let p = Point { x: 3, y: 4 };

// Accessing struct fields
let x_coordinate = p.x;
let y_coordinate = p.y;

Copy

枚举

枚举是具有多个命名变体的类型,其中每个变体可以选择保存数据。它们非常适合表示可以是几种不同类型之一的值。

enum Direction {
    North,
    South,
    East,
    West,
}

// Enum with associated data
enum Message {
    Quit,
    Move: Point,
    Write: felt252,
    Color: (felt252, felt252, felt252),
}

// Using enums with pattern matching
let msg = Message::Move(Point { x: 10, y: 20 });
match msg {
    Message::Quit => { /* handle quit */ },
    Message::Move(point) => { /* handle move with point data */ },
    Message::Write(text) => { /* handle write with text */ },
    Message::Color((r, g, b)) => { /* handle color with RGB values */ },
}

Copy

字符串、短字符串、ByteArray

与高级语言相比,Cairo 中的文本处理是较低级别的。该语言没有像 Rust 或 JavaScript 中那样的传统 String 类型,但它提供了两个用于处理文本数据的核心原语:

  • 短字符串:编码为 felt252 的字符串字面量,限制为 31 个字节。
  • ByteArray:一种用于动态大小的 UTF-8 字符串和字节序列的内置类型,具有用于 UTF-8 解码和操作的实用工具。

让我们详细了解这些字符串类型。

短字符串:felt252 中的紧凑型 ASCII

当你的字符串表示形式很短或不超过 31 个 ASCII 字符时,你可以将其表示为短字符串。Cairo 中的短字符串直接打包到单个 felt252 中,每个字符都使用其 ASCII 值(1 字节 = 8 位)进行编码。由于 felt252 包含 252 位,因此你可以在单个字段元素中存储最多 31 个 ASCII 字符。

让我们以小写 'hello world' 为例,总共 11 个字符,远未达到 31 个字符的限制。

// Note the single quotes around the string.
let greeting = 'hello world';  // Fits within 31 ASCII characters

// 'hello world'
// → ASCII bytes: 68 65 6C 6C 6F 20 77 6F 72 6C 64
// → Hex: 0x68656c6c6f20776f726c64

Copy

如果我们将 'hello world' 示例中的每个字符映射到其 ASCII 码,并将这些字节打包到单个十六进制值中,从左到右,我们将得到:0x68656c6c6f20776f726c64

Byte Arrays 字符串

Cairo 中的 ByteArray 类型旨在处理 UTF-8 编码的字符串和超出单个 felt252 的 31 字节限制的任意字节序列。这使其对于管理动态长度的数据至关重要。

// Note the double quotes around the long string.
let long_string: ByteArray = "Hello, Cairo! This is a longer string that exceeds 31 bytes and demonstrates ByteArray usage perfectly.";

Copy

在内部,ByteArray 使用混合存储结构。下面的代码块显示了 ByteArray 结构体如何包含三个字段,这些字段协同工作以存储字节数据:

pub struct ByteArray {
    pub(crate) data: Array<bytes31>,      // Full 31-byte chunks
    pub(crate) pending_word: felt252,     // Incomplete bytes (up to 30 bytes)
    pub(crate) pending_word_len: usize,   // Number of bytes in pending_word
}

Copy

  • data 字段保存存储为 bytes31 的完整 31 字节块。
  • pending_word 字段保存未形成完整块的剩余字节。
  • pending_word_len 跟踪存储在 pending_word 中的字节的精确数量。

pending_word 最多可以存储 30 个字节,而不是 31 个字节。如果你正好有 31 个可用字节,它们将作为 data 中的完整块存储。对于总共短于 31 个字节的字节数组,data 保持为空,所有内容都位于 pending_word 中。

现在,让我们创建几个 ByteArray 示例,以了解如何根据长度存储数据:

##[executable]
fn main() {
    // Short string (≤30 bytes) - stored entirely in pending_word
    let short_data: ByteArray = "Hello Cairo developers!";  // 23 bytes in pending_word

    // Medium string (31-60 bytes) - one chunk in data + remainder in pending_word
    let medium_data: ByteArray = "This is a longer string that demonstrates ByteArray storage";  // 58 bytes total

    // Long string (>62 bytes) - multiple chunks in data + remainder in pending_word
    let long_data: ByteArray = "ByteArray stores data efficiently using 31-byte chunks in the data field, with any remaining bytes stored in pending_word field";  // 127 bytes total
}

Copy

Cairo 中的控制流

Cairo 支持标准控制流结构,例如条件语句和循环,这些结构允许开发人员编写分支程序。

ifelse if``else

Cairo 使用 ifelse ifelse 块进行分支逻辑,就像在 Rust 或其他主流语言中一样。

以下示例显示了如何在 Cairo 中编写 if 语句。

use core::felt252_div;

##[executable]
fn main() {
    let x: u32 = 5; // Explicitly type as u32

    let wrecked_pie = felt252_div(22, 7);

    let _result = if x > 10 {
        wrecked_pie - 1000
    } else if x == 10 {
        0
    } else {
        wrecked_pie
    };

    println!("this is the value of result: {}", _result);
}

Copy

请注意,如果我们在上面的示例中将 x 定义为 felt252,则程序将在编译时失败。这是因为 felt252 未实现 PartialOrd trait,这是使用比较运算符(如 <><=>=)所必需的。此限制是 Cairo 中经过深思熟虑的设计选择,旨在防止因依赖字段元素的数值排序而可能产生的密码学错误。

循环(loopwhilefor

Cairo 支持三种主要形式的循环:loopwhilefor,每种循环都有特定的用例和约束。

loopsloop 关键字创建一个无限循环,类似于其他语言中的 while true。它无限期地运行,直到使用 break 语句显式退出。当事先不知道迭代次数,并且你依赖于内部条件来终止循环时,此结构很有用。

以下是使用 loop 对数字求和直到满足条件的示例:

fn loop_sum(limit: felt252) -> felt252 {
    let mut i = 0;
    let mut sum = 0;

    loop {
        if i == limit {
            break;
        }
        sum += i;
        i += 1;
    }

    sum
}

Copy

在此示例中,loop 无限期地继续,直到 i == limit,此时 break 退出循环。

while:只要给定的条件求值为 true,Cairo 中的 while 循环就会执行。当在运行时评估结束条件时,它最适合有条件迭代。循环条件必须是确定性的,并且基于在执行期间已知的值。

let mut i = 0;
while i < 5 {
    // Do something
    i += 1;
}

Copy

for:Cairo 中的 for 循环仅适用于静态定义的范围。这意味着你可以使用语法 for i in 0..n 迭代常量或字面量范围,其中 n 必须是编译时常量或循环开始时已知的价值。

在下面的示例中,我们使用 for 关键字循环遍历一个数组(我们将在后面解释)。

use core::array::ArrayTrait;

##[executable]
fn main() {
    let mut a = ArrayTrait::new();
    a.append(10);
    a.append(20);
    a.append(30);
    a.append(40);
    a.append(50);

    let len = a.len();

    for i in 0..len {
        let val = a.at(i);
        // You can use `val` here however you need
        let _ = val;
    }
}

Copy

Cairo 中的数组和字典

Cairo 中的数组是相同类型值的有序集合。由于 Cairo 的不可变存储模型,一旦添加了现有元素,就无法修改。可以使用 append() 将元素追加到末尾,并使用 pop_front() 从前面删除元素,pop_front() 返回 Option<T> 并推进逻辑起始位置。这种类似队列的行为允许 FIFO(先进先出)操作。

数组是使用 Array<T> 类型实现的,其方法由 array::ArrayTrait 提供。 因此,使用 ArrayTrait::new() 调用创建新数组。

以下代码显示了如何创建新数组。

use array::ArrayTrait;

let mut numbers = ArrayTrait::<felt252>::new();

Copy

局部(存储器)数组默认是不可变的。因此,我们使用 let mut 使它们可变,如下所示。 之后,我们可以通过调用 append(value) 将项目添加到数组中。

numbers.append(10); // 元素 10 被追加到索引 0
numbers.append(20); // 元素 10 被追加到索引 1

Copy

或者,我们可以使用 array! 在编译时按顺序追加条目:

let arr = array![1, 2, 3, 4, 5];

Copy

数组方法

每个数组都支持内置方法,这些方法通过 array::ArrayTrait 公开。以下是 Cairo 的数组方法:

  • .new(): 创建一个空数组。
  • .append(value): 将一个条目添加到数组的末尾。
  • .pop_front(): 从数组前端移除元素
  • .len(): 返回元素的数量。
  • .pop_front(): 移除并返回最后一个元素。
  • isEmpty(): 如果数组为空,则返回 true,否则返回 false
  • .get(index)at(index): 读取特定索引处的条目。

在 Cairo 中,.get(index).at(index) 都用于访问数组中的元素,但它们的行为有所不同。.get(index) 方法返回一个 Option<T>,这意味着如果索引在界限内,结果可能是 Option::Some(value),如果不是,则可能是 Option::None。这使得 .get() 成为更安全的选择,尤其是在无法保证索引有效的情况下。

另一方面,.at(index) 直接为你提供值,而无需将其包装在 Option 中。虽然这在已知索引有效时使访问更简单,但它带来了一个重要的权衡:如果索引超出范围,程序将 panic 并崩溃。

Cairo 中多种数据类型的数组

不能直接在单个数组中存储多个不同的数据类型,因为数组是同质的(要求所有元素都具有相同的类型)。

但是,你可以使用自定义枚举结构体来将不同的类型包装在单个统一类型中,从而解决此限制。 下面的示例展示了如何使用枚举来解决此问题。

use core::array::ArrayTrait;

//注意:
// Drop trait 允许在此类型超出范围时自动清理。
// 像(如)felt252、u8、bool 等基本类型具有自动 Drop 实现,但是
// 像枚举和结构体这样的自定义类型通常需要显式派生 Drop。
// Felt252Dict 或其他不可删除的类型无法实现 Drop。
##[derive(Drop)]
enum MixedValue {
    Felt: felt252,
    SmallNumber: u8,
    Flag: bool,
    FeltArray: Array<felt252>,
}

##[executable]
fn main() {
    let mut mixed: Array<MixedValue> = ArrayTrait::new();

    mixed.append(MixedValue::Felt(2025));
    mixed.append(MixedValue::SmallNumber(7_u8));
    mixed.append(MixedValue::Flag(true));

    let mut nested_array: Array<felt252> = ArrayTrait::new();
    nested_array.append(1);
    nested_array.append(2);
    nested_array.append(3);

    mixed.append(MixedValue::FeltArray(nested_array));

}

Copy

字典 (Felt252Dict<T> 数据类型)

与 Solidity 中的映射类似,Felt252Dict<T> 是一种类似于字典的数据类型,表示键值对的集合,其中每个键都是唯一的,并与相应的value T 相关联。它的功能或方法在核心库的 Felt252DictTrait trait 中实现。

键类型被限制为 felt252,而它们的值数据类型是指定的。在内部,Felt252Dict<T> 作为条目列表工作,每个键关联的值都初始化为零。一旦设置了一个新的条目,零值将被设置为之前的条目。因此,如果输入一个不存在的键,Felt252DictTrait 下的 zero_default 方法将被调用以返回 0,而不是错误或未定义的值。但是,此 trait 不适用于复杂类型(原因在下一个小节中)。

这是一个关于如何使用 Felt252Dict<T> 键值对的简单例子。

use core::dict::Felt252Dict;

##[executable]
fn main() {
    // 创建字典
    let mut balances: Felt252Dict<u64> = Default::default();

    // 仅插入 'clark'
    balances.insert('clark', 50);

    // 获取 'clark' 的余额
    let clark_balance = balances.get('clark');
    println!("This is clark_balance: {}", clark_balance);
    assert!(clark_balance == 100, "clark_balance is not 100");

    // 尝试获取 'jane' — 未插入,返回 0
    let jane_balance = balances.get('jane');
    println!("This is jane_balance: {}", jane_balance);

    // 通过检查返回值是否为 0 来证明 jane 未插入
    assert!(jane_balance == 25, "jane_balance should be 0 since she was never added");
}

Copy

当我们运行上面的代码时,第一个断言将会失败,因为键 'clark' 被插入的值为 50,因此,条件 clark_balance == 100 的计算结果为 false。

如果我们注释掉第一个断言以允许第二个断言运行,程序将继续检索 'jane' 的余额,她从未插入到字典中。在 Cairo 中,在尚未明确插入的键上调用 .get('jane') 会返回值类型的默认值, 在本例中为 0

Cairo 程序 panic 结果

字典内部的复合类型

let mut dict: Felt252Dict<u64> = Default::default();
// 所有可能的键现在都有值 0(u64 的零值)
let value = dict.get(999); // 返回 0,即使我们从未插入任何内容

let mut dictArray: Felt252Dict<<Array<u8>>> = Default::default();
// dictArray 的所有可能的键都没有值 0

Copy

我们之前提到过,字典在创建时会自动将所有键初始化为“零值”,通过 zero_default 方法。但是,这种行为不支持复杂或复合类型,例如数组结构体(包括像 u256 这样的类型)。这是因为 zero_default 要求该类型具有零值,当键尚未显式设置时可以返回。由于复杂类型通常不实现此trait,因此 Cairo 要求你在将它们存储在字典中时手动处理初始化和存在性检查。

为了解决这个限制,Nullable<T> 指针类型可以在字典中使用,以表示一个值或缺少一个值 (null)。字典存储指向堆分配值的指针,并且你显式检查读取时的 null。

下面的代码演示了如何通过将它们包装在 Nullable<Array<felt252>> 中来将数组存储在 Felt252Dict 中。这允许我们将动态数据(例如序列化值)与字典中基于 felt 的键相关联。

use core::dict::Felt252Dict;

##[executable]
fn main() {
    // 创建一个 felt252 值的数组
    let data = array![42, 13, 88, 5];

    // 初始化一个将 felt252 键映射到可为空的字节数组的字典
    let mut storage: Felt252Dict<Nullable<Array<u8>>> = Default::default();

    // 将我们的数据转换为字节并存储在字典中
    let byte_data = array![0x2a, 0x0d, 0x58, 0x05]; // 十六进制表示
    storage.insert(1, NullableTrait::new(byte_data));

    // 存储另一个条目
    let more_data = array![0xff, 0x00, 0xaa];
    storage.insert(2, NullableTrait::new(more_data));
}

Copy

此示例展示了如何使用唯一的 felt 键将数组插入到字典中,其中 Nullable 提供了一个安全的包装器,可以表示值或空状态。

结论

Cairo 是一种类似 Rust 的语言,具有熟悉的控制结构。

  • felt252 数据类型是数值类型的默认类型。许多数据类型在幕后转换为 felt252
  • 由于溢出保护,使用有符号和无符号类型优于 felt252 类型。
  • 变量默认是不可变的,如果它们的值将来会发生变化,则必须声明为 mut
  • Cairo 支持数组和字典来将数据组合在一起。

本文是 Starknet 上的 Cairo 编程 教程系列的一部分

  • 原文链接: rareskills.io/post/cairo...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
RareSkills
RareSkills
https://www.rareskills.io/