数据类型

在 Cairo 中,每一个值都属于某一个数据类型(data type), 这告诉 Cairo 它被指定为何种数据,以便明确数据处理方式。我们将看到两类数据类型子集:标量(scalar)和复合(compound)。

记住,Cairo 是静态类型(statically typed)语言,也就是说在编译时就必须知道所有变量的类型。根据值及其使用方式,编译器通常可以推断出我们想要用的类型。 在可能存在多种类型的情况下,我们可以使用一种称为“类型转换”的方法,在其中指定所需的输出类型。

fn main() {
    let x: felt252 = 3;
    let y: u32 = x.try_into().unwrap();
}

你会看到其它数据类型的各种类型注解。

标量类型

一个 标量 (scalar)类型表示一个单一的值。Cairo 有三种主要的标量类型: felts、整数(integers)和布尔值(booleans)。你可能在其他语言中见过它们。 让我们深入了解它们在 Cairo 中是如何工作的。

Felt 类型

在 Cairo 中,如果你没有指定一个变量或参数的类型,它的类型默认为一个字段元素,由关键字 felt252 表示。在 Cairo 中,当我们说 "一个字段元素 "时,我们指的是范围为 0 <= x < P 的整数、 其中 P 是一个非常大的素数,目前为 P = 2^{251} + 17 * 2^{192}+1。当加减乘时,如果结果超出了素数的指定范围,就会发生溢出,然后再加上或减去 P 的适当倍数,使结果回到范围内(也就是说,结果是以 P 为模数计算的)。

整数和字段元素之间最重要的区别是除法:字段元素的除法(以及 Cairo 的除法)与普通 CPU 的除法不同,其中整数除法 x / y 被定义为[x/y], 其中商的整数部分被返回(所以你得到7 / 3 = 2),它可能满足或不满足方程式 (x / y) * y == x,这取决于 x 是否能被 y 除。

在 Cairo,x/y 的结果被定义为总是满足方程式 (x / y) * y == x。如果 y 作为整数可以整除 x ,你将得到 Cairo 的预期结果(例如�����6 / 2 确实会得到3)。 但是当 y 不能整除 x 时,你可能会得到一个令人惊讶的结果: 例如,由于 2 * ((P+1)/2) = P+1 ≡ 1 mod[P],在 Cairo 中 1 / 2 的值是 (P+1)/2(而不是0或0.5),因为它满足上述公式。

整数类型

所谓的felt252 类型是一个基本类型,是创建核心库中所有类型的基础。 然而,我们强烈建议程序员尽可能使用整数类型而不是 felt252 类型,因为 integer 类型带有额外的安全功能, 对代码中的潜在漏洞提供额外保护,如溢出检查。通过使用这些整数类型,程序员可以确保他们的程序更加安全,不容易受到攻击或其他安全威胁。 一个 integer 是一个没有小数部分的数字。这个类型声明指出了该类型可以用来存储整数的比特位。 表3-1显示了Cairo中内建的整数类型。我们可以使用这些变体中的任何一种来声明一个整数值的类型。

表格3-1: Cairo 的整数类型

长度无符号
8-bitu8
16-bitu16
32-bitu32
64-bitu64
128-bitu128
256-bitu256
32-bitusize

每个变量都有一个明确的大小。注意,现在,usize 类型只是 u32 的别名;然而,当将来 Cairo 可以被编译为 MLIR 时,它可能会很有用。 由于变量是无符号的,它们不能包含一个负数。这段代码会引起程序出现错误:

fn sub_u8s(x: u8, y: u8) -> u8 {
    x - y
}

fn main() {
    sub_u8s(1, 3);
}

前面提到的所有整数类型都适合felt252,但u256除外,它需要存储 4 个以上的位。在内部原理中,u256基本上是一个包含 2 个字段的结构体:u256 {low: u128, high: u128}

你可以用表 3-2 中的任何一种形式编写数字字面值。 请注意可以是多种数字类型的数字字面值允许使用类型后缀, 例如像 57_u8 这样指定类型。

表3-2:Cairo 的整数类型字面值

数字字面值例子
Decimal(十进制)98222
Hex (十六进制)0xff
Octal (八进制)0o04321
Binary (二进制)0b01

那么,你如何知道要使用哪种类型的整数?试着估计你用的 int 的最大值,然后选择合适的大小。 usize 的主要是用在为某种集合做索引时。

数值运算

Cairo 支持所有整数类型的基本数学运算: 加法、减法、乘法、除法和取余。 整数除法将向最接近0的整数截断。以下代码展示了如何在 let 语句中使用它们:

fn main() {
    // addition
    let sum = 5_u128 + 10_u128;

    // subtraction
    let difference = 95_u128 - 4_u128;

    // multiplication
    let product = 4_u128 * 30_u128;

    // division
    let quotient = 56_u128 / 32_u128; //result is 1
    let quotient = 64_u128 / 32_u128; //result is 2

    // remainder
    let remainder = 43_u128 % 5_u128; // result is 3
}

这些语句中的每个表达式使用了一个数学运算符并计算出了一个值,然后绑定给一个变量。

附录 B 包含了一个Cairo中所有操作符的列表。

布尔类型

正如其他大部分编程语言一样,Cairo 中的布尔类型有两个可能的值:truefalse。布尔型的大小为一个 felt252。 布尔类型在 Cairo 中是用 bool 来指定的。例如:

fn main() {
    let t = true;

    let f: bool = false; // with explicit type annotation
}

使用布尔值的主要方式是通过条件语句,如 if 表达式。 我们将在"控制流”部分介绍 if 表达式在 Cairo 中的工作原理。

短字符串类型

Cairo 没有字符串(String)的原生类型,但你可以在 felt252 中存储短字符,形成我们所说的 "短字符串"。一个短字符串的最大长度为 31 个字符。这是为了确保它能装入一个 felt (一个 felt 是252位,一个 ASCII 字符是 8 位)。 可以通过把值放在单引号之间来声明短字符串,下面是一些例子:

fn main() {
    let my_first_char = 'C';
    let my_first_string = 'Hello world';
}

类型转换

在 Cairo 中,你可以使用 TryIntoInto 特性提供的 try_intointo 方法在标量类型之间进行显式类型转换。

try_into 方法允许在目标类型可能不适合源值时进行安全的类型转换。请记住,try_into 会返回一个 Option<T> 类型,你需要解开(unwrap)这个类型来访问新的值。

另一方面,当转换必然成功时,如源类型小于目标类型时,into 方法可用于类型转换。

为了进行转换,在源值上调用 var.into()var.try_into() 来将其转换为另一种类型。新变量的类型必须被明确定义,如下面的例子所示。

fn main() {
    let my_felt252 = 10;
    // Since a felt252 might not fit in a u8, we need to unwrap the Option<T> type
    let my_u8: u8 = my_felt252.try_into().unwrap();
    let my_u16: u16 = my_u8.into();
    let my_u32: u32 = my_u16.into();
    let my_u64: u64 = my_u32.into();
    let my_u128: u128 = my_u64.into();
    // As a felt252 is smaller than a u256, we can use the into() method
    let my_u256: u256 = my_felt252.into();
    let my_usize: usize = my_felt252.try_into().unwrap();
    let my_other_felt252: felt252 = my_u8.into();
    let my_third_felt252: felt252 = my_u16.into();
}

元组类型

元组 是一个将多个其他类型的值组合进一个复合类型的主要方式。 元组长度固定:一旦声明,其长度不会增大或缩小。

我们使用包含在圆括号中的逗号分隔的值列表来创建一个元组。 元组中的每一个位置都有一个类型,而且这些不同值的类型也不必是相同的。 这个例子中使用了可选的类型注解:

fn main() {
    let tup: (u32, u64, bool) = (10, 20, true);
}

tup 变量绑定到整个元组上,因为元组是一个单独的复合元素。 为了从元组中获取单个值,可以使用模式匹配(pattern matching) 来解构(destructure)元组值,像这样:

use debug::PrintTrait;
fn main() {
    let tup = (500, 6, true);

    let (x, y, z) = tup;

    if y == 6 {
        'y is six!'.print();
    }
}

程序首先创建了一个元组并绑定到 tup 变量上。 接着使用了 let 和一个模式将 tup 分成了三个不同的变量, xyz。这叫做解构(destructuring), 因为它将一个元组拆成了三个部分。最后,程序打印出了 y 的值, 也就是 6

我们也可以同时用 value 和 name 来声明元组。 比如说:

fn main() {
    let (x, y): (felt252, felt252) = (2, 3);
}

unit类型 ()

一个 unit 类型 是一个只有一个值 () 的类型。 它由一个没有元素的元组来表示。 它的大小总是为零,并且它在编译后的代码中一定不存在。

Last change: 2023-09-28, commit: f04c9d6