数据类型
在 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中内建的整数类型。我们可以使用这些变体中的任何一种来声明一个整数值的类型。
长度 | 无符号 |
---|---|
8-bit | u8 |
16-bit | u16 |
32-bit | u32 |
64-bit | u64 |
128-bit | u128 |
256-bit | u256 |
32-bit | usize |
每个变量都有一个明确的大小。注意,现在,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
这样指定类型。
数字字面值 | 例子 |
---|---|
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 中的布尔类型有两个可能的值:true
和 false
。布尔型的大小为一个 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 中,你可以使用 TryInto
和 Into
特性提供的 try_into
和 into
方法在标量类型之间进行显式类型转换。
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
分成了三个不同的变量,
x
、y
和 z
。这叫做解构(destructuring),
因为它将一个元组拆成了三个部分。最后,程序打印出了 y
的值,
也就是 6
。
我们也可以同时用 value 和 name 来声明元组。 比如说:
fn main() { let (x, y): (felt252, felt252) = (2, 3); }
unit类型 ()
一个 unit 类型 是一个只有一个值 ()
的类型。
它由一个没有元素的元组来表示。
它的大小总是为零,并且它在编译后的代码中一定不存在。