类型转换

本文详细介绍了 Cairo 中类型转换的概念、IntoTryInto 两个 trait 的区别和使用场景,以及 felt252uintAddressByteArrayStringbool 等类型之间的转换方法。Cairo 强调类型安全和显式转换,避免了隐式转换可能导致的数据丢失和错误,提高了智能合约的可靠性。

Cairo 中的类型转换是将值从一种数据类型转换为另一种数据类型的过程。

当使用 Cairo 严格的类型系统时,这变得很有必要,在 Cairo 中,函数调用、变量赋值、合约交互以及数据操作都需要显式的类型匹配。

Cairo 采用比 Solidity 更谨慎的方式来处理类型转换。它提供了清晰的方法来处理可能导致转换失败的情况,而不是允许可能静默地导致意外错误(如数据截断)的转换。

为了说明这两种语言在方法上的差异,请考虑将 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 用于保证成功的转换。这些都是 “安全” 的转换,其中目标类型总是可以表示源类型的任何值。

这是一个示例,展示了从小整数类型到大整数类型,以及从整数类型到 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();
}

这里很明显,类型为 u8small_number 或任何小于 u32 的类型都可以放入 larger_number 中,并且 felt252 可以包含除 u256 之外的所有 uint 类型的值(u256 大于 felt252)。

当我们使用 .into() 时,我们是在告诉 Cairo 编译器,“我们知道这种转换总是会成功,所以直接做就好。” 在上面的示例中,将 small_numberu8 转换为 u32,以及将 numu16 转换为 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 仅为安全转换而实现,其中目标类型可以容纳源类型的所有可能值。由于 u256u32 具有更大的值范围,因此即使值 100 可以放入 u32 中,也没有用于此转换的 Into 实现。

TryInto Trait:可能失败的类型转换

如果源值范围不适合目标类型,则 TryInto 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>
    // 尝试将 u32 转换为 u8 - 返回 Option<u8>
    let result: Option<u8> = num.try_into();

    // Use 'match' to handle both success and failure cases
    // 使用 'match' 来处理成功和失败的情况
    // 'match' is Cairo's pattern matching - like a switch statement that checks what's inside Option
    // 'match' 是 Cairo 的模式匹配 - 就像一个检查 Option 内部内容的 switch 语句

    match result {
        Option::Some(val) => {
           // Conversion succeeded - val contains the converted u8 value
           // 转换成功 - val 包含转换后的 u8 值
           println!("Successfully converted {} to u8: {}", num, val);
        },
        Option::None => {
           // Conversion failed - number was too large for u8 (which holds 0-255)
           // 转换失败 - 数字对于 u8 来说太大(u8 保存 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):

Visual diagram of an Option containing a None

由于转换返回 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 的有效范围内:

Visual diagram of an Option containing a Some

由于转换成功,因此 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 到地址类型

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(),因为它是从 felt252ContractAddress 的唯一可用转换方法。.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
    // 第一步:将 u256 转换为 felt252 - 如果值超过 felt252 范围,则会 panic
    let address_felt: felt252 = value.try_into().unwrap();
    // Second step: convert felt252 to ContractAddress - will panic if outside valid address range
    // 第二步:将 felt252 转换为 ContractAddress - 如果超出有效地址范围,则会 panic
    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)
    // 第一步:尝试将 u256 转换为 felt252(如果值太大可能会失败)
    match value.try_into() {
        Option::Some(felt_val) => {
            // u256 → felt252 conversion succeeded
            // u256 → felt252 转换成功
            let address_felt: felt252 = felt_val;
            // Second step: try to convert felt252 to ContractAddress
            // 第二步:尝试将 felt252 转换为 ContractAddress
            // This can also fail if the felt252 value is outside valid address range
            // 如果 felt252 值超出有效地址范围,这也会失败
            address_felt.try_into()
        }
        Option::None => {
            // u256 → felt252 conversion failed (value too large for felt252)
            // u256 → felt252 转换失败(值对于 felt252 来说太大)
            Option::None
        }
    }
}

safe_convert_u256_to_address 不会在转换失败时 panic,而是为超出 ContractAddress 范围的输入返回 None,从而允许调用方优雅地处理过大的输入。

ByteArray 和 String Casting

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 值。

Screenshot of terminal output showing equivalency between underlying string and felt values

没有发生任何转换;'Hello' 及其十六进制等效项 0x48656c6c6f 是相同的 felt252 值。

短字符串到 uints

这是一个将字符(存储为 felt252)转换为其 ASCII 值的 u8 的示例:

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 中的各个字符用作数值时,可以通过索引提取它们并将其转换为其他数值类型,如 u32felt252。请考虑以下示例:

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",其中使用 text[0] 访问了第一个字符 'C',该字符返回一个 u8 值。然后使用 .into() 将其转换为 u32,保证会成功,因为 u32 可以容纳任何 u8 值。同样,位于 text[2] 的第三个字符 'i' 也被转换为 felt252

这样,我们就可以在字节级别处理 ByteArray 内容,并将各个字符转换为所需的数值类型。

在大多数情况下,如果我们有一个 ByteArray,我们可能希望将其保留为字符串数据,因为 ByteArray 经过优化,可以存储和操作文本,而不是用于数值计算。

bool casting

Cairo 通过设计将布尔值 (true/false) 与数字类型分开,但允许使用 .into() 将布尔值转换为 felt252,其中 true 变为 1,false 变为 0。但是,不能使用自动强制转换方法将布尔值直接转换为整数类型,如 u32u64 等:

fn main() {
    let flag: bool = true;

    // This works, bool to felt252:
    // 这有效,bool 到 felt252:
    let as_felt: felt252 = flag.into(); // Works: true becomes 1, false becomes 0
    // 有效:true 变为 1,false 变为 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];
}

因此,关键点是:boolfelt252 自动工作,但 bool → 整数类型需要手动转换,并且没有自动强制转换在相反方向上有效。

在大多数情况下,布尔值应保持为布尔值,以获得更好的类型安全性和代码清晰度。

结论

Cairo 中的类型转换强调安全性和显式性,而不是自动转换。它使用 Into 进行有保证的转换,并使用 TryInto 进行可能失败的转换,从而强制人们显式处理潜在的错误。

这种方法可以防止其他语言中常见的静默数据丢失,并在错误到达生产环境之前捕获它们。Cairo 的显式转换意味着你的智能合约会以可预测的方式运行,即使在处理意外数据时也是如此。

与 Solidity 不同,Solidity 需要手动安全检查类型转换,Cairo 通过 Option 类型将错误处理直接构建到其强制转换系统中。

虽然 Cairo 的转换要求比隐式强制转换需要更多的代码,但这种显式性可以构建更可靠的智能合约。

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

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

0 条评论

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