Skip to content

整数 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。

序列化类型

名称包含范围占用空间
uint80 到 (28 - 1)8位 = 1字节
uint160 到 (216 - 1)16位 = 2字节
uint320 到 (232 - 1)32位 = 4字节
uint640 到 (264 - 1)64位 = 8字节
uint1280 到 (2128 - 1)128位 = 16字节
uint2560 到 (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位
coins0 到 (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,表明以下情况:整数超出预期范围。

因此,在使用序列化时,要非常小心数字,并始终仔细检查计算。