Cairo 中的类型转换是指将值从一种数据类型转换为另一种数据类型的过程。
在使用 Cairo 严格的类型系统时,这变得尤为必要,因为在函数调用、变量赋值、合约交互和数据操作中都需要显式的类型匹配。
与 Solidity 相比,Cairo 在处理类型转换时采取了更为谨慎的方法。它提供了清晰的方式来处理转换可能失败的情况,而不是允许那些可能由于数据截断等原因悄悄导致意外错误的转换。
为了说明这两种语言在处理方式上的差异,考虑将 uint32 转换为 uint8 类型:
Solidity 示例:
contract Example {
function castingExample() public pure returns (uint8) {
uint32 largeNumber = 1000;
uint8 smallNumber = uint8(largeNumber);
return smallNumber;
}
}
在 Solidity 中,转换会使用模运算(1000 % 2**8 = 232)静默地将 largeNumber (1000) 截断为 smallNumber (232)。castingExample() 函数成功执行并返回 232,而不会有任何关于发生数据丢失的警告。
Cairo 示例:
fn casting_example() -> u8 {
let large_number: u32 = 1000;
let small_number: u8 = large_number.try_into().unwrap();
println!("Small number is : {}", small_number);
small_number
}
fn main(){
casting_example();
}
在运行时,try_into() 会检查 large_number (1000) 是否能容纳在 u8(最大值 255)中。当转换失败时,它会返回 None,随后调用的 unwrap() 会因 "Option::unwrap failed." 而引发 panic,立即停止执行而不是允许数据损坏。
下一节将详细介绍 try_into().unwrap() 语法。
有保证的转换与可能失败的转换之间的区别
Cairo 通过两个主要 trait 来处理类型转换:用于安全转换的 Into 和用于可能失败的转换的 TryInto。
Into Trait:绝对安全的类型转换
Into trait 用于保证能成功的转换。这些是“安全”的转换,即目标类型总是能够表示源类型的任何值。此 trait 在每个 Cairo 程序中自动可用,无需导入语句。
下面的示例展示了从较小整数类型到较大整数类型的转换,以及从整数到 Cairo 原生 felt252 类型的转换:
fn main() {
let small_number: u8 = 100;
let large_number: u32 = small_number.into();
let num: u16 = 500;
let as_felt: felt252 = num.into();
}
很明显,类型为 u8 或任何小于 u32 类型的 small_number 都能容纳进 large_number 中,并且 felt252 可以包含除 u256(它比 felt252 大)之外所有 uint 类型的任何值。
当我们使用 .into() 时,我们是在告诉 Cairo 编译器:“我们知道这个转换总是会成功,所以直接执行即可。”在上面的例子中,将 small_number 从 u8 转换为 u32,以及将 num 从 u16 转换为 felt252 都是保证能成功的,因为目标类型可以容纳源类型的所有可能值。
但是,如果我们尝试使用 .into() 将较大的类型转换为较小的类型,Cairo 编译器将抛出错误:
fn main() {
let large_number: u256 = 100;
let small_number: u32 = large_number.into(); // ERROR: Trait has no implementation in context: core::traits::Into::<core::integer::u256, core::integer::u32>
}
发生此错误是因为 Cairo 的 Into trait 仅针对安全的转换实现,即目标类型可以容纳源类型的所有可能值。由于 u256 的取值范围比 u32 大得多,因此没有针对此转换的 Into 实现,即使值 100 能够容纳进 u32 中也是如此。
TryInto Trait:可能失败的类型转换
如果源值范围无法容纳进目标类型导致转换可能失败,那么 TryInto trait 则是正确的选择方法。与 Into 一样,此 trait 也是自动可用的,无需导入语句。
TryInto 返回一个 Option 枚举,如果转换成功,则为 Some(converted_value);如果失败,则为 None。
在前面 1000(u32)无法安全转换为 u8 的示例基础上,下面的 try_convert_to_u8 函数展示了 Cairo 处理具有潜在不安全类型转换的方法。此函数接受一个 u32 值并尝试将其转换为 u8。代码展示了成功的转换(当值合适时)和失败的转换(当值对于 u8 的 0-255 范围来说太大时):
fn try_convert_to_u8(num: u32) {
// Attempt to convert u32 to u8 (returns Option<u8>)
let result: Option<u8> = num.try_into();
// Use 'match' to handle both success and failure cases
// 'match' is Cairo's pattern matching (like a switch statement that checks what's inside Option)
match result {
Option::Some(val) => {
// Conversion succeeded (val contains the converted u8 value)
println!("Successfully converted {} to u8: {}", num, val);
},
Option::None => {
// Conversion failed (number was too large for u8 (which holds 0-255))
println!("Conversion failed! {} is too big for u8 (max: 255)", num);
}
}
}
fn main() {
try_convert_to_u8(1000); // Will fail (too large for u8)
try_convert_to_u8(100); // Will succeed (fits in u8)
try_convert_to_u8(255); // Will succeed (maximum u8 value)
try_convert_to_u8(256); // Will fail (exceeds u8 maximum by 1)
}
在 try_convert_to_u8 函数内部,我们在输入值 num 上调用 try_into(),这将返回一个 Option 类型,并将其存储在 result 中。
match 语句是 Cairo 的模式匹配功能,类似于其他语言中的 switch 语句。它检查 Option 内部的内容,并根据结果是包含一个值还是为空来执行特定的代码块。如果转换成功,我们会得到包含转换后值的 Some(val),并打印成功消息。如果转换因数值过大而失败,我们会得到 None,并打印解释失败原因的错误消息。
在 main() 函数中,我们测试了四种不同的场景,以演示转换如何处理各种输入值。
当我们调用 try_convert_to_u8(1000) 时,下图展示了转换如何在 Option<u8> 中返回 None,因为 1000 超出了 u8 的最大值(255):

由于转换返回了 None,match 语句检测到它为空,并执行 Option::None 分支,打印出错误消息 "Conversion failed! 1000 is too big for u8 (max: 255).”
随后,当运行 try_convert_to_u8(100) 时,下图展示了转换如何在 Option<u8> 中返回 Some(100),因为 100 处于 0-255 的有效范围内:

由于转换成功,match 语句执行 Option::Some(val) 分支,打印出 “Successfully converted 100 to u8: 100.”
何时使用 into() 或 try_into()
将 into() 用于:
- 将较小的类型转换为较大的类型(例如,
u32转换为u64) - 任何不可能发生数据丢失的转换
into()明确禁止从较大类型转换为较小类型,即使特定的值能够容纳在目标范围内。
将 try_into() 用于:
- 将较大的类型转换为较小的类型(
u32转换为u8) - 任何值可能无法容纳的转换
- 当你想优雅地处理转换失败而不是引发 panic 时,将
try_into()与match一起使用
了解了 Cairo 的转换机制后,我们现在可以深入探讨这些 trait 如何应用于特定的转换场景。
felt252 转换为 uints
将 felt252 转换为无符号整数类型在 Cairo 中是一项常见操作,因为 felt252 是原生类型。转换方法取决于我们是要转换为更大还是更小的整数类型。
下面的示例将一个 felt252 值(felt_value)转换为 u256(as_u256):
fn main() {
let felt_value: felt252 = 42615;
let as_u256: u256 = felt_value.into();
}
由于 u256 可以容纳任何 felt252 值,因此这种转换保证会成功,这也是我们可以使用 .into() 而不是 .try_into() 的原因。
将 felt252 转换为较小的整数类型需要使用 try_into(),因为 felt252 的值可能会超出目标类型的范围。
下面的函数尝试将 felt252 转换为 u8,但如果值大于 255 将发生 panic:
fn convert_felt_to_small_uint(felt_value: felt252) -> u8 {
felt_value.try_into().unwrap()
}
convert_felt_to_small_uint 函数接受一个 felt252 值,尝试将其转换为 u8,如果失败则发生 panic。它使用 .unwrap() 从 try_into() 返回的 Option 中提取 u8 值。如果 felt252 值超过 255,转换将返回 None,而 .unwrap() 会导致程序引发 panic。
更安全的方法是显式处理潜在的转换失败,而不引发 panic。下面的代码通过创建一个返回 Option<u8> 而不是引发 panic 的函数展示了适当的错误处理方法,然后测试了成功的转换 (100) 和失败的转换 (1000),并使用 match 语句适当地处理每个结果:
fn safe_convert_felt_to_u8(felt_value: felt252) -> Option<u8> {
felt_value.try_into()
}
fn main() {
let small_felt: felt252 = 100;
let large_felt: felt252 = 1000;
let small_as_u8 = safe_convert_felt_to_u8(small_felt); // Returns Some(100)
println!("Small conversion result: {:?}", small_as_u8);
let large_as_u8 = safe_convert_felt_to_u8(large_felt); // Returns None
// handle the successful conversion
match small_as_u8 {
Option::Some(val) => println!("Successfully converted 100 to u8: {}", val),
Option::None => println!("Small conversion failed"),
}
// handle the failed conversion
match large_as_u8 {
Option::Some(val) => println!("Converted: {}", val),
Option::None => println!("Conversion failed: 1000 is too large for u8"),
}
}
safe_convert_felt_to_u8 接受一个 felt252 值并返回 Option<u8>。请注意它没有使用 .unwrap();它直接返回来自 try_into() 的 Option,让调用者决定如何处理潜在的失败。
在 main 函数中,我们测试了两种场景:
- 将 100 转换为
u8:此操作会成功,因为 100 适合u8的范围(0-255),因此small_as_u8包含Some(100) - 将 1000 转换为
u8:此操作会失败,因为 1000 超出了u8的最大值 255,因此large_as_u8包含None
第一个 match 语句处理成功的转换。由于 small_as_u8 包含 Some(100),它匹配到了 Option::Some(val) 分支,并使用转换后的值打印成功消息。
第二个 match 语句处理失败的转换。由于 large_as_u8 包含 None,它匹配到了 Option::None 分支,并打印一条解释转换失败原因的错误消息。
这展示了如何优雅地处理成功和失败的转换而不会引发 panic,从而使我们能够完全控制程序中的错误处理逻辑。
uints 转换为 Address 类型
Cairo 不允许直接从整数类型转换为地址类型。相反,必须通过 felt252 作为中间类型进行转换。在处理用户 ID、数字标识符或在智能合约中从整数计算推导地址时,将 uints 转换为地址就变得非常必要。
转换过程包括两个步骤:首先将整数转换为 felt252,然后将 felt252 转换为 ContractAddress:
use starknet::ContractAddress;
fn user_address(user_id: u64) -> ContractAddress {
let address_felt: felt252 = user_id.into();
address_felt.try_into().unwrap()
}
user_address 函数接受一个 u64 类型的参数 user_id。它首先使用 .into() 将 u64 值转换为 felt252,这总是会成功,因为 felt252 可以容纳任何 u64 值,并将其存储在 address_felt 中。然后,它使用 .try_into().unwrap() 将 felt252 类型的 address_felt 转换为 ContractAddress。
我们使用 .try_into(),因为这是从 felt252 转换为 ContractAddress 的唯一可用方法。.unwrap() 提取结果,但在转换失败时将引发 panic。
在上面的代码示例中,try_into() 方法返回 Option<ContractAddress>;如果转换成功则返回 Some(address),如果失败则返回 None。.unwrap() 方法从 Some() 中提取实际的 ContractAddress 值,但如果转换失败并返回 None,则会引发 panic。
转换 u256 时需要格外小心,因为 u256 的值可能会超过 felt252 的范围,而且 ContractAddress 的有效范围更小,为 [0, 2**251)。这意味着两个转换步骤都有可能失败。
对于我们确信值可以合适地容纳的情况(例如,当值在 felt252 范围内时),我们可以使用直接的方法:
fn convert_u256_to_address(value: u256) -> ContractAddress {
// First step: convert u256 to felt252 (will panic if value exceeds felt252 range)
let address_felt: felt252 = value.try_into().unwrap();
// Second step: convert felt252 to ContractAddress (will panic if outside valid address range)
address_felt.try_into().unwrap()
}
在处理可能超出有效范围的任意 u256 值时,最好显式地处理潜在的失败:
fn safe_convert_u256_to_address(value: u256) -> Option<ContractAddress> {
// First step: try to convert u256 to felt252 (might fail if value is too large)
match value.try_into() {
Option::Some(felt_val) => {
// u256 to felt252 conversion succeeded
let address_felt: felt252 = felt_val;
// Second step: try to convert felt252 to ContractAddress
// This can also fail if the felt252 value is outside valid address range
address_felt.try_into()
}
Option::None => {
// u256 to felt252 conversion failed (value too large for felt252)
Option::None
}
}
}
safe_convert_u256_to_address 在转换失败时不会发生 panic,而是对于超出 ContractAddress 范围的输入返回 None,允许调用者优雅地处理超大的输入值。
ByteArray 和 String 转换
在 Cairo 中,字符串表示法和数字类型之间的转换涉及到处理 Cairo 的字符串类型:短字符串(即 felt252)和用于较长字符串的 ByteArray。
短字符串
fn main() {
let string_as_felt: felt252 = 'Hello';
let hex_representation: felt252 = 0x48656c6c6f;
println!("String: {}", string_as_felt);
println!("Hex form: {}", hex_representation);
}
运行此代码时,您会看到 Cairo 中的短字符串是直接作为 felt252 值存储的。

这里并没有发生转换;'Hello' 及其对应的十六进制值 0x48656c6c6f 是同一个 felt252 值。
短字符串转换为 uints
下面的示例将一个字符(存储为 felt252)转换为其对应的作为 u8 的 ASCII 值:
fn main() {
let char_a: felt252 = 'A';
let char_as_u8: u8 = char_a.try_into().unwrap();
println!("Character 'A' as u8: {}", char_as_u8);
}
运行此代码时,终端中会显示 “Character ‘A’ as u8: 65”,因为该字符的值能够容纳进目标类型中。如果值不能容纳,程序将会引发 panic,因此处理无法确定转换是否成功的情况非常重要。
ByteArray 操作
要转换 ByteArray 数据,需要通过索引访问单个字节,并将每个字节分别进行转换。
ByteArray 类型主要用于处理长度超过 31 字节的字符串,而不是用于数字类型的转换。
请注意,在 ByteArray 中访问单个字节返回的是 u8。当需要将 ByteArray 中的单个字符作为数字值处理时,可以通过索引将它们提取出来,并转换为其他数字类型,如 u32 或 felt252。思考下面的示例:
fn main() {
let text: ByteArray = "Cairo";
let first_byte = text[0];
let third_byte = text[2];
let byte_as_u32: u32 = first_byte.into();
let byte_as_felt: felt252 = third_byte.into();
println!("First byte 'C' as u32: {}", byte_as_u32);
println!("Third byte 'i' as felt252: {}", byte_as_felt);
}
我们访问了 ByteArray “Cairo”,其中第一个字符 ‘C’ 是使用 text[0] 访问的,这会返回一个 u8 值。然后使用 .into() 将其转换为 u32,这肯定会成功,因为 u32 可以容纳任何 u8 值。类似地,位于 text[2] 的第三个字符 ‘i’ 被转换为 felt252。
通过这种方式,我们在字节层面上处理 ByteArray 内容,并将单个字符转换为所需的数字类型。
大多数时候,如果我们有一个 ByteArray,我们可能会希望将其保留为字符串数据,因为 ByteArray 是专门为存储和操作文本进行优化的,而不是用于数字计算。
bool 转换
在设计上,Cairo 将布尔值(true/false)与数字类型分开,但允许使用 .into() 将布尔值转换为 felt252,其中 true 变为 1,false 变为 0。然而,不能使用自动转换方法将布尔值直接转换为 u32、u64 等整数类型:
fn main() {
let flag: bool = true;
// This works, bool to felt252:
let as_felt: felt252 = flag.into(); // Works: true becomes 1, false becomes 0
// This will cause a compilation error:
// let as_u32: u32 = flag.into(); // ERROR: Trait has no implementation in context: core::traits::Into::<core::bool, core::integer::u32>.
// Manual conversion for integers:
let as_u32: u32 = if flag {
1
} else {
0
};
}
数字不能自动转换回布尔值。需要进行显式的比较,如下所示:
fn main() {
let number: u32 = 1;
let felt_num: felt252 = 1;
// These will cause compilation errors:
// let back_to_bool: bool = number.into(); // ERROR: Trait has no implementation in context: core::traits::Into::<core::integer::u32, core::bool>.
// let felt_to_bool: bool = felt_num.into(); // ERROR: Trait has no implementation in context: core::traits::Into::<core::felt252, core::bool>.
// Manual conversion:
let back_to_bool: bool = number != 0;
let felt_to_bool: bool = felt_num != 0;
}
布尔值也不能直接用作数组索引或参与算术运算:
fn main() {
let my_array = array![10, 20];
let index: bool = true;
// These will cause compilation errors:
// let value = my_array[index];
// let result = index + 1;
// Conversions work:
let numeric_index: u32 = if index {
1
} else {
0
};
let value = my_array[numeric_index];
}
因此,关键点在于:bool 到 felt252 的转换会自动进行,但 bool 到整数类型的转换需要手动完成,且没有向相反方向自动进行的类型转换。
在大多数情况下,布尔值应保留为布尔值,以实现更好的类型安全性和代码清晰度。
结论
Cairo 中的类型转换强调安全性和显式性,而不是自动转换。它使用 Into 处理保证能成功的转换,使用 TryInto 处理可能失败的转换,迫使开发者显式地处理潜在的错误。
这种方法防止了其他语言中常见的无声数据丢失现象,并在错误进入生产环境之前捕获它们。Cairo 的显式转换意味着,即使在处理意外数据时,智能合约的行为也是可预测的。
与 Solidity 需要对类型转换进行手动安全检查不同,Cairo 通过 Option 类型将错误处理机制直接内置到了其转换系统中。
虽然 Cairo 的转换要求比隐式转换需要编写更多的代码,但这种显式性能够构建出更可靠的智能合约。
本文是 Starknet 上的 Cairo 编程 教程系列的一部分。