Appearance
整数 Int
在 TON 上的智能合约中进行的算术运算总是使用整数,而不使用浮点数,因为浮点数的结果是不可预测的。因此,重点放在整数及其处理上。
在 Tact 中,唯一的原始数字类型是 Int,用于表示 257 位有符号整数。 它能够存储的整数范围是从 −2256 到 2256-1
符号表示
Tact 支持以多种方式编写 Int 的原始值作为整数字面量。
大多数符号表示允许在数字之间添加下划线(_),但以下情况除外:
- 在字符串中的表示,如在 nano-tons 案例中所见。
- 以前导零(0)开头的十进制数字。通常不推荐使用这种表示,具体见下文。
连续多个下划线,如在 4__2 中,或尾随下划线,如在 42_ 中,都是不允许的。
十进制
使用十进制数系统表示数字是最常见且最广泛使用的方式: 123456789 你可以使用下划线(_)来提高可读性: 123_456_789 等于 123456789
另外,你可以在数字前加一个零(0),这种方式禁止使用下划线并且只允许十进制数字:0123=123。请注意,由于在 TypeScript 中使用前导零可能会与八进制整数字面量混淆,后者常与 Tact 一起用于开发和测试合约,因此强烈不推荐使用这种带前导零的表示法。
十六进制
使用十六进制数系统表示数字,通过前缀 0x(或 0X)来标识: 0xFFFFFFFFF。 使用下划线(_)来提高可读性: 0xFFF_FFF_FFF 等于 0xFFFFFFFFF。
八进制
使用八进制数系统表示数字,通过前缀 0o(或 0O)来标识: 0o777777777。 使用下划线(_)来提高可读性: 0o777_777_777 等于 0o777777777。
二进制
使用二进制数系统表示数字,通过前缀 0b(或 0B)来标识: 0b111111111。 使用下划线(_)来提高可读性: 0b111_111_111 等于 0b111111111。
币值单位 (Nano-tons)
例如,与美元相关的算术运算需要在小数点后保留两位小数 —— 这些小数用于表示美分。但如果我们只能使用整数,应该如何表示数字 $1.25 呢?解决方案是直接使用美分进行计算。这样,$1.25 就变成了 125 美分。我们只需记住最右边的两位数字代表小数点后的数值。
类似地,处理Ton币时需要九个小数位,而不是两个。因此,在 Tact 中可以表示为 ton("1.25") 的 1.25 TON 实际上是数字 1250000000。我们将这样的数字称为 nano-tons (或者 nanoToncoins) ,而不是美分。
序列化
在将整数值编码到持久状态(合约和特质的字段)时,通常最好使用比 257 位更小的表示形式以减少存储成本。由于这些表示形式代表了 TON 区块链操作的原生 TL-B 类型,使用这些表示形式也被称为“序列化”。
每次声明状态变量后,都会使用 as 关键词指定持久状态的大小:
solidity
contract SerializationExample {
// 持久状态变量
oneByte: Int as int8 = 0; // 范围从 -128 到 127(占用 8 位 = 1 字节)
twoBytes: Int as int16; // 范围从 -32,768 到 32,767(占用 16 位 = 2 字节)
init() {
// 因为没有默认值,需要在 init() 中初始化
self.twoBytes = 55*55;
}
}
整数序列化也适用于结构体和消息的字段,以及映射的键/值类型:
solidity
struct StSerialization {
martin: Int as int8;
}
message MsgSerialization {
seamus: Int as int8;
mcFly: map<Int as int8, Int as int8>;
}
动机非常简单:
- 在状态中存储 1000 个 257 位整数每年大约花费 0.184 TON。
- 相比之下,存储 1000 个 32 位整数每年只需 0.023 TON。
序列化类型
名称 | 包含范围 | 占用空间 |
---|---|---|
uint8 | 0 到 (28 - 1) | 8位 = 1字节 |
uint16 | 0 到 (216 - 1) | 16位 = 2字节 |
uint32 | 0 到 (232 - 1) | 32位 = 4字节 |
uint64 | 0 到 (264 - 1) | 64位 = 8字节 |
uint128 | 0 到 (2128 - 1) | 128位 = 16字节 |
uint256 | 0 到 (2256 - 1) | 256位 = 32字节 |
int8 | (-27) 到 (27 - 1) | 8位 = 1字节 |
int16 | (-215) 到 (215 - 1) | 16位 = 2字节 |
int32 | (-231}) 到 (231 - 1) | 32位 = 4字节 |
int64 | (-263) 到 (263 - 1) | 64位 = 8字节 |
int128 | (-2127) 到 (2127 - 1) | 128位 = 16字节 |
int256 | (-2255) 到 (2255 - 1) | 256位 = 32字节 |
int257 | (-2256) 到 (2256 - 1) | 257位 = 32字节+1位 |
coins | 0 到 (2120 - 1) | 120位 = 15字节 |
操作
所有运行时的数字计算都以 257 位进行,因此溢出非常罕见。然而,如果任何数学运算溢出,将会抛出异常,并且交易将失败。可以说,Tact 的数学运算默认是安全的。
请注意,在同一计算中混合使用不同状态大小的变量没有问题。在运行时,无论如何它们都是相同的类型 —— 257位有符号整数,所以那时不会发生溢出。
然而,这仍然可能在交易的计算阶段导致错误。考虑以下示例:
solidity
import "@stdlib/deploy";
contract ComputeErrorsOhNo with Deployable {
oneByte: Int as uint8; // 持久状态变量,最大值为255
init() {
self.oneByte = 255; // 初始值为255,一切正常
}
receive("lets break it") {
let tmp: Int = self.oneByte * 256; // 运行时无溢出
self.oneByte = tmp; // 哎呀,tmp值超出了oneByte的预期范围
}
}
这里,oneByte
被序列化为一个 uint8
,它只占用一个字节,范围从 0 到 (28 - 1),即 255。在运行时计算时不会发生溢出,一切都作为 257 位有符号整数计算。但是,当我们决定将 tmp
的值存储回 oneByte
时,我们会得到一个错误,退出代码为 5,表明以下情况:整数超出预期范围。
因此,在使用序列化时,要非常小心数字,并始终仔细检查计算。