Cairo 是一种专为可证明、可验证计算而设计的编程语言,尤其是在零知识证明系统(如以太坊上的 Layer 2 (L2) 网络 Starknet)的背景下。
Cairo 的专门设计是为了实现基于 STARK 的程序执行证明。这使得计算可以在链下被高效验证,随后通过简洁的、无需信任的证明在链上进行证明。
尽管该语言是为区块链用例而创建的,但 Cairo 在其领域内非常灵活,因为它支持具有密码学完整性的链下可验证计算。与 Solidity 不同,Cairo 可以在智能合约的上下文之外运行。在这个意义上,Cairo 的行为几乎像一种被约束于智能合约和可证明链下计算的通用语言。
本文概述了该语言的工作原理。我们将涵盖主要的数据类型、控制流机制以及常用的数据结构。
Cairo 在 Starknet 中的角色
Starknet 使用 STARKs(可扩展的透明知识论证)来实现复杂计算的链下执行,同时保留了以太坊的安全性和去中心化特性。
所有 Starknet 智能合约都是用 Cairo 编写的。这些合约会被编译成名为 Sierra 的中间表示形式,随后再编译成 Casm(Cairo 汇编),这是 CairoVM 能够理解的低级语言。CairoVM 会确定性地执行 Casm 指令,生成执行轨迹,并确保程序遵循 STARK 证明生成所需的约束条件。
本文介绍了 Cairo 编程语言的基础知识,并展示了如何将其作为通用语言在智能合约上下文之外使用。在进入下一节之前,请按照以下步骤设置开发环境。
设置开发环境
-
创建一个空目录并进入该目录。
该目录可以命名为任何名称,在本例中,它被称为
cairo_playground:mkdir cairo_playground && cd cairo_playground -
在
cairo_playground目录中创建一个 source 文件夹:mkdir src -
在
src文件夹中,创建两个文件:playground.cairo(名称可以变化)和lib.cairo:touch src/playground.cairo && touch src/lib.cairo -
将以下内容添加到新文件中。
playground.cairo:#[executable] fn main() { // Print message to terminal. println!("Hello from Rareskills!!!"); }lib.cairo:mod playground; -
在项目根目录(
cairo_playground)中创建一个Scarb.toml文件:touch Scarb.toml添加以下内容:
[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"#[executable]注解将在后面的小节中进行解释。
完成设置后,该目录应具有类似于以下的结构:

最后,要测试 Cairo 程序(playgroung.cairo),请运行以下命令:
scarb execute
Cairo 语言基础语法与数据类型
Cairo 的语法受到 Rust 的启发,但为可证明、可验证的计算进行了优化。在我们探讨数据类型和逻辑之前,必须先了解其基础构建块,例如 Cairo 中的变量和函数声明。
声明变量:let、mut 和 const
Cairo 是静态类型的,所有变量的类型必须在编译时声明。在声明变量时使用 let 关键字,随后紧跟变量名、冒号、类型,然后是具体的值:
// let <NAME>: <dataType> = <value>;
let count: u8 = 42;
let name: felt252 = 'bob';
let active: bool = true;
变量的可变性
Cairo 中的变量默认是不可变的。它们在赋值后无法被修改。要启用修改功能,需使用 mut 关键字,如下所示。
// let mut <NAME>: <Type> = <value>;
let mut total: u128 = 0;
total = total + 10;
声明常量
在 Cairo 中,const 关键字用于定义在编译期已知且在运行时无法更改的固定值。常量被硬编码到程序源代码中,这意味着它们不占用内存,且访问它们在运行时是零成本的。
以下是声明常量的方法:
// const <NAME>: <Type> = <value>;
const DECIMALS: u8 = 18;
在 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);
}
在函数中,我们可以使用 return 关键字显式地返回值。但是,正如上面 multiply 函数中看到的那样,也可以隐式地返回值。当函数体中的最后一个表达式没有以分号结尾时,它的结果将自动被返回。
解释 #[executable] 属性
#[executable] 属性将函数标记为一个可以直接被 Cairo runner 调用的入口点。Cairo runner 是负责执行已编译 Cairo 代码的程序,它会寻找带有 #[executable] 标记的函数作为起点。
如果没有这个属性,该函数将不会作为顶级入口点暴露出来,也就无法独立执行。
在上面的例子中,multiply 函数是一个普通函数:它可以被程序中的其他函数调用,但不能直接独立执行。相反,main 函数带有 #[executable] 属性标记,这将其指定为可以由 Cairo runner 直接运行的入口点。
要执行上面的 main,请在终端中输入 scarb execute 命令。输出结果如下所示:

注意: #[executable] 属性不适用于智能合约。
实际上,上面的例子并不是一个智能合约,而是一个常规的 Cairo 程序。这是可能的,因为与特定于领域的 Solidity 不同,Cairo 可以在智能合约的上下文之外执行。使用 Cairo runner,你可以编写并运行独立的程序,而无需将它们部署到 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);
}
Cairo 中的数据类型
既然我们已经了解了如何在 Cairo 中声明变量,接下来让我们探讨一下 Cairo 中的主要数据类型。
1. felt252:核心数字类型
在 Cairo 中,最基本的数据类型是由 felt252 表示的域元素。它是该语言中默认的数字类型,代表 Cairo VM 使用的素数域中的一个元素。该素数域如下所示:
p = 2^{251} + 17*2^{192} + 1
这意味着 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);
}
终端输出:

由于这种回绕行为,如果不小心处理,可能会发生由意外回绕引起的算术错误(即溢出)。
为了解决这个问题,Cairo 还提供了固定宽度的整数类型 u8..u256 和有符号整数 i8..i256,它们会在运行时检查溢出/下溢。如果某项操作试图超过有效范围,程序将会触发 panic(即因错误而中止)。
在需要进行极限优化的场景下使用 felt252,因为所有其他类型在底层最终都是以 felt252 的形式表示的。为了满足常规计算和安全性,建议使用整数类型,因为它们提供了内置的防溢出保护。
felt252 中的除法
Cairo 的 felt252 类型中的域元素遵循有限域算术原理运行,这意味着它们不支持余数,也不支持像固定宽度整数那样的传统整数除法。相反,除法 a / b 的计算结果为 a × b^(-1) mod P,其中 b 是非零值。
b⁻¹ 被称为 b 模 P 的模乘法逆元。
如果 a = 1 且 b=2,我们将得到 1 × 2⁻¹。
既然,
在下面的代码块中,我们将展示上述证明是如何成立的,并观察有无余数时 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));
}
终端输出:

如上面的测试所示,当没有余数时,Cairo 域中的除法工作原理类似于整数除法。
然而,当除法存在余数时则有所不同。例如,如果我们将 4 除以 3,我们并不是在问“三可以被四除几次”,而是在问“在这个域中,什么值乘以三等于四?”
在域算术中,答案是 4 与 3 的模逆元的乘积。这确保了该结果在乘以三时,在对该域素数取模后产生四。
当变量未声明类型时会发生什么?
在 Cairo 中,当你分配一个数字字面量而没有指定类型时(如下所示),编译器会自动假设该值的类型为 felt252。
let count = 42;
// count's is of type felt252
这是因为 felt252 是 Cairo 的默认数字类型,类似于某些其他语言中默认使用 int。
2. 无符号整数:u8..u256
在 Cairo 中,固定宽度的整数(如 u8、u16、u32、u64 和 u128)都是更大的 felt252 类型的子集,这意味着它们的值可以完全容纳在 felt252 中;它们可以安全地表示为域元素,因为它们的最大值小于 felt252 的最大值。
表 1:无符号整数范围
| 类型 | 大小(位) | 范围 |
|---|---|---|
u8 |
8-bit | 0 to 255 |
u64 |
64-bit | 0 to 2⁶⁴ - 1 |
u128 |
128-bit | 0 to 2¹²⁸ - 1 |
u256 |
256-bit | 0 to 2²⁵⁶ - 1 (组合的) |
正如表 1 所示,u256 超出了 felt252 的最大值,因此无法被容纳在单个域元素中。在底层,Cairo 将 u256 表示为一个由两个 u128 值组成的结构体:
struct u256 {
low: u128, // Least significant 128 bits
high: u128, // Most significant 128 bits
}
例如,类型为 u256 的值 7 被像这样对半拆分:
let value: u256 = 7;
// __________________________256-bit_____________________________
// | |
// 0x0000000000000000000000000000000000000000000000000000000000000007
// ________high 128-bit__________ __________low 128-bit_________
// | | | |
// 0x00000000000000000000000000000000 00000000000000000000000000000007
3. 有符号整数:i8、i16、i32、i64、i128
Cairo 中的有符号整数使用小写字母 i 加上位宽来书写,例如 i8、i16、i32、i64 或 i128。每个有符号类型可以表示一个以零为中心的范围内的值,其计算公式为:
to
例如,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').
}
如上所示,所有超过 u256 最大值的算术运算都会导致 panic 错误。
4. bool:true 或 false
bool 用于表示逻辑值:true 或 false。在内部,bool 被编码为一个值为 0 (false) 或 1 (true) 的 felt252。
Cairo 复合类型
复合类型将多个值组合在一起,从而在 Cairo 中实现结构化和富有表现力的数据表示。
元组
元组容纳具有不同类型的固定值集合。它们对于从函数返回多个值或临时对相关数据进行分组非常有用。
let pair: (felt252, bool) = (42, true);
// Accessing tuple elements by destructuring
let (first_element, first_element) = pair;
结构体
结构体是具有命名自定义字段的数据类型。
// Define the struct
struct Point {
x: felt252,
y: felt252,
}
#[executable]
fn main() {
let p = Point { x: 3, y: 4 };
// Accessing struct fields
let x_coordinate = p.x;
let y_coordinate = p.y;
println!("The x coordinate of point p is: {}", x_coordinate);
println!("The y coordinate of point p is: {}", y_coordinate);
}
枚举
枚举是具有多个命名变体的类型,其中每个变体都可以有选择地保存数据。它们非常适合表示可以是几种不同类型之一的值。
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 */ },
}
字符串、短字符串、bytearray
与高级语言相比,Cairo 中的文本处理更为底层。该语言没有像 Rust 或 JavaScript 那样传统的 String 类型,但它提供了两个用于处理文本数据的核心原语:
- 短字符串(Short strings):编码为
felt252(或bytes31)的字符串字面量,限制为 31 个字节。 ByteArray:一种用于动态大小的 ASCII 字符和字节序列的内置类型。
让我们详细了解这些字符串类型的具体内容。
短字符串: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: felt252 = 'hello world'; // Fits within 31 ASCII characters
// OR
let greeting: bytes31 = 'hello world'.try_into().unwrap(); // Fits within 31 ASCII characters
// ' h e l l o w o r l d '
// → ASCII bytes: 68 65 6C 6C 6F 20 77 6F 72 6C 64
// → Hex: 0x68656c6c6f20776f726c64
如果我们将 'hello world' 示例中的每个字符映射到其 ASCII 码,并将这些字节打包成单个十六进制值,从左到右我们将得到:0x68656c6c6f20776f726c64。
Byte Arrays 字符串
Cairo 中的 ByteArray 类型旨在处理超过单个 felt252 的 31 字节限制的 ASCII 字符和任意字节序列。这使其成为管理动态长度数据时不可或缺的一部分。
// 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.";
在内部,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
}
data字段保存作为bytes31存储的完整的 31 字节数据块。pending_word字段保存无法组成完整数据块的剩余字节。pending_word_len追踪存储在pending_word中的确切字节数。
pending_word 最多可以存储 30 个字节,而不是 31 个。如果你刚好有 31 个可用字节,它们将作为完整的数据块存储在 data 中。对于总长短于 31 字节的 byte arrays,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
}
Cairo 中的控制流
Cairo 支持标准的控制流结构,例如条件语句和循环,这使得开发者能够编写具有分支的程序。
if、else if 和 else
就像在 Rust 或其他主流语言中一样,Cairo 使用 if、else if 和 else 块来实现分支逻辑。
这是一个展示如何在 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);
}
请注意,如果在上面的例子中我们将 x 定义为 felt252,程序将会在编译时失败。这是因为 felt252 没有实现 PartialOrd trait,而这是使用 <、>、<= 和 >= 等比较运算符所必需的。这一限制是 Cairo 中故意设定的设计选择,目的是防止依赖域元素数值排序而可能引起的密码学错误。
循环(loop、while 和 for)
Cairo 支持三种主要形式的循环:loop、while 和 for,每种形式都有特定的用例和约束。
loops:loop 关键字创建一个无限循环,类似于其他语言中的 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
}
在这个例子中,loop 会无限期地继续下去,直到 i == limit,此时 break 会退出循环。
while:只要给定条件评估为 true,Cairo 中的 while 循环就会执行。当结束条件是在运行时进行评估时,它最适合用于条件迭代。循环条件必须是确定性的,并且基于执行期间已知的值。
let mut i = 0;
while i < 5 {
// Do something
i += 1;
}
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;
}
}
Cairo 中的数组与字典
Cairo 中的数组是相同类型值的有序集合。由于 Cairo 的不可变内存模型,现有元素一旦被添加就无法被修改。可以使用 append() 将元素追加到末尾,并使用 pop_front() 从前端移除元素,后者会返回一个 Option<T> 并将逻辑起始位置向前推进。这种类似于队列的行为允许进行 FIFO(先进先出)操作。
数组是通过 Array<T> 类型与 array::ArrayTrait 提供的方法来实现的。因此,新的数组是使用 ArrayTrait::new() 调用来创建的。
下面的代码展示了如何创建一个新数组。
use array::ArrayTrait;
let mut numbers = ArrayTrait::<felt252>::new();
本地(内存)数组默认是不可变的。因此我们使用 let mut 使其变为可变,如下所示。
之后,我们可以通过调用 .append(value) 将项添加到数组中。
numbers.append(10); // the element 10 is appended to index 0
numbers.append(20); // the element 10 is appended to index 1
或者,我们可以使用 array! 在编译时按顺序追加项:
let arr = array![1, 2, 3, 4, 5];
数组方法
每个数组都由内置方法支持,这些方法通过 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 中包含多种数据类型的数组
你不能直接将多种不同的数据类型存储在一个数组中,因为数组是同构的(要求所有元素具有相同的类型)。
然而,你可以通过使用**自定义枚举(custom enum)或结构体(struct)**将不同的类型包装成一个统一的类型,从而绕过这一限制。下面的示例展示了如何使用枚举来解决此问题。
use core::array::ArrayTrait;
//NOTE:
// The Drop trait allows automatic cleanup when this type goes out of scope.
// Basic types like felt252, u8, bool, etc. have automatic Drop implementations, But
// Custom types like enums and structs typically need to derive Drop explicitly.
// Felt252Dict or other non-droppable types cannot implement 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));
}
字典(Felt252Dict<T> 数据类型)
类似于 Solidity 中的 mapping,Felt252Dict<T> 是一种类似字典的数据类型,代表键值对的集合,其中每个键都是唯一的,并与一个对应的值 T 相关联。它的功能或方法是在核心库的 Felt252DictTrait trait 中实现的。
键的数据类型被限制为 felt252,而它们的值数据类型是被指定的。在内部,Felt252Dict<T> 表现为一个条目列表,其中与每个键关联的值都会初始化为零。一旦设置了新条目,零值将被设为前一个条目。因此,如果输入了一个不存在的键,则将调用 Felt252DictTrait 下的 zero_default 方法以返回 0,而不是报错或返回未定义的值。然而,这个 trait 对于复杂类型是不可用的(原因在下一个小节中说明)。
这是一个关于如何处理 Felt252Dict<T> 键值对的简单示例。
use core::dict::Felt252Dict;
#[executable]
fn main() {
// Create the dictionary
let mut balances: Felt252Dict<u64> = Default::default();
// Insert only 'clark'
balances.insert('clark', 50);
// Get balance for 'clark'
let clark_balance = balances.get('clark');
println!("This is clark_balance: {}", clark_balance);
assert!(clark_balance == 100, "clark_balance is not 100");
// Try to get 'jane' — not inserted, returns 0
let jane_balance = balances.get('jane');
println!("This is jane_balance: {}", jane_balance);
// Demonstrate that jane was not inserted by checking if the returned value is 0
assert!(jane_balance == 25, "jane_balance should be 0 since she was never added");
}
当我们运行上面的代码时,第一个断言将会失败,因为键 'clark' 插入的值为 50,因此条件 clark_balance == 100 的计算结果为 false。
如果我们注释掉第一个断言以允许第二个断言运行,程序将继续去检索 'jane' 的余额,而 'jane' 从未被插入字典中。在 Cairo 中,对尚未被显式插入的键调用 .get('jane') 会返回值类型的默认值,在当前例子中为 0。

字典中的复合类型
let mut dict: Felt252Dict<u64> = Default::default();
// ALL possible keys now have value 0 (the zero value for u64)
let value = dict.get(999); // Returns 0, even though we never inserted anything
let mut dictArray: Felt252Dict<<Array<u8>>> = Default::default();
// ALL possible keys of dictArray do not have value 0
我们之前提到过,字典在被创建时会自动通过 zero_default 方法将所有键初始化为“零值”。然而,这种行为不支持复杂类型或复合类型,例如数组和结构体(包括像 u256 这样的类型)。这是因为 zero_default 要求该类型必须存在一个零值,以便在尚未被显式设置键时进行返回。由于复杂类型通常不会实现此 trait,因此 Cairo 要求你在将它们存储进字典时,手动处理初始化和存在性检查。
为了解决这一限制,可以在字典中使用 Nullable<T> 指针类型来表示要么有值,要么没有值(null)。字典存储指向堆分配值的指针,在读取时你需要显式地检查它是否为 null。
下面的代码演示了如何通过将数组包装在 Nullable<Array<felt252>> 中,从而将其存储在 Felt252Dict 内。这使我们能够将动态数据(如序列化的值)与字典中基于 felt 的键相关联。
use core::dict::Felt252Dict;
#[executable]
fn main() {
// Create an array of felt252 values
let data = array![42, 13, 88, 5];
// Initialize a dictionary that maps felt252 keys to nullable byte arrays
let mut storage: Felt252Dict<Nullable<Array<u8>>> = Default::default();
// Convert our data to bytes and store in dictionary
let byte_data = array![0x2a, 0x0d, 0x58, 0x05]; // hex representation
storage.insert(1, NullableTrait::new(byte_data));
// Store another entry
let more_data = array![0xff, 0x00, 0xaa];
storage.insert(2, NullableTrait::new(more_data));
}
此示例展示了如何使用唯一的 felt 键将数组插入到字典中,同时 Nullable 提供了一个安全的包装器,可以表示存在的值或者空状态。
结论
Cairo 是一门类似于 Rust 的语言,拥有我们熟悉的控制结构。
felt252数据类型是默认的数字类型。在幕后,许多数据类型会被转换为felt252。- 相比于
felt252类型,由于具有溢出保护机制,更推荐使用有符号和无符号类型。 - 变量默认是不可变的,如果未来它们的值会改变,则必须使用
mut进行声明。 - Cairo 支持使用数组和字典将数据组合在一起。
本文是Starknet 上的 Cairo 编程教程系列的一部分