Skip to content

合约 contract

在 Tact 中,合约类似于流行的面向对象语言中的类,除了它们的实例部署在区块链上,并且不能像结构体(Structs)和消息(Messages)那样传递。

自引用

合约和特征(traits)有一个内置的标识符 self,用于引用它们的字段(持久状态变量和常量)和方法(内部函数):

solidity
contract Example {
    // 持久状态变量
    foo: Int;
 
    init() {
        self.foo = 42; // <- 通过 self 引用变量 foo。
    }
}

结构

每个合约可以包含:

  • 持久状态变量
  • 构造函数 init()
  • 合约常量
  • 获取器函数
  • 接收器函数
  • 内部函数

此外,合约可以继承来自特征的所有声明,并覆盖一些默认行为。

持久状态变量

合约可以定义在合约调用之间持久存在的状态变量。TON 中的合约需要按照它们消耗的持久空间比例支付租金,因此鼓励通过序列化使用紧凑的表示形式。

solidity
contract Example {
    // 持久状态变量
    val: Int;              // Int
    val32: Int as uint32;  // 序列化为 32 位无符号的 Int
    mapVal: map<Int, Int>; // Int 键对 Int 值
    optVal: Int?;          // Int 或 null
}

状态变量必须在 init() 函数中有默认值或初始化,该函数在合约部署时运行。唯一的例外是类型为 map<k, v> 的持久状态变量,因为它们默认初始化为空。

合约常量

与变量不同,常量不能改变。它们的值在编译时计算,并且在执行期间不能改变。

合约内部和外部定义的常量之间没有太大区别。在项目中的其他合约可以使用外部定义的常量。

常量初始化必须相对简单,只能依赖于编译期间已知的值。例如,如果你加两个数字,编译器将在构建期间计算结果并将结果放入你的编译代码中。

你可以在接收器和获取器中读取常量。

与合约变量不同,合约常量不消耗持久状态空间。它们的值直接存储在合约的代码单元(Cell)中。

solidity
// 全局常量在编译时计算,不能改变
const GlobalConst1: Int = 1000 + ton("42") + pow(10, 9);
 
contract Example {
    // 合约常量也在编译时计算,不能改变
    const ContractConst1: Int = 2000 + ton("43") + pow(10, 9);
 
    // 合约常量可以是枚举的简单替代品
    const StateUnpaid: Int = 0;
    const StatePaid: Int = 1;
    const StateDelivered: Int = 2;
    const StateDisputed: Int = 3;
 
    // 初始化常量不需要 init 函数
    init() {}
 
    get fun sum(): Int {
        // 从任何地方访问常量
        return GlobalConst1 + self.ContractConst1 + self.StatePaid;
    }
}

构造函数 init()

在合约部署时运行构造函数 init()

如果合约有任何没有指定默认值的持久状态变量,它必须在此函数中初始化它们。

solidity
contract Example {
    // 持久状态变量
    var1: Int = 0; // 使用默认值 0 初始化
    var2: Int;     // 必须在 init() 函数中初始化
 
    // 构造函数
    init() {
        self.var2 = 42;
    }
}

要在内部函数、接收器或获取器中获取目标合约的初始状态,请使用 initOf 表达式。

获取器函数

获取器函数无法从其他合约访问,只导出到链下世界。

此外,获取器不能修改合约的状态变量,只能读取它们的值并在表达式中使用它们。

solidity
contract HelloWorld {
    foo: Int;
 
    init() {
        self.foo = 0;
    }
 
    // 带返回类型 Int 的获取器函数
    get fun foo(): Int {
        return self.foo; // 这里不能改变 self.foo
    }
}

接收器函数

Tact 中的接收器函数可以是以下三种之一:

  • receive(),接收来自其他合约的内部消息。
  • bounced(),当这个合约发出的消息反弹回来时被调用。
  • external(),没有发送者,任何人都可以发送的消息。
solidity
message CanBounce {
    counter: Int;
}
 
contract HelloWorld {
    counter: Int;
 
    init() {
        self.counter = 0;
    }
 
    get fun counter(): Int {
        return self.counter;
    }
 
    // 内部消息接收器,响应字符串消息 "increment"
    receive("increment") {
        self.counter += 1;
 
        // 将消息发送回发送者
        send(SendParameters{
            to: sender(),
            value: 0,
            mode: SendRemainingValue | SendIgnoreErrors,
            body: CanBounce{counter: self.counter}.toCell()
        });
    }
 
    // 反弹消息接收器,当消息反弹回此合约时调用
    bounced(src: bounced<MsBounced>) {
        self.counter = 0; // 如果消息反弹,则重置计数器
    }
 
    // 外部消息接收器,响应链下消息 "hello, it's me"
    external("hello, it's me") {
        // 因为没有发送者,所以无法回复!
        self.counter = 0;
    }
}

内部函数

这些函数的行为类似于流行的面向对象语言中的私有方法——它们是合约内部的,可以通过前缀 self 来调用。这就是为什么有时内部函数也被称为“合约方法”。

内部函数可以访问合约的持久状态变量和常量。

它们只能从接收器、获取器和其他内部函数调用,但不能从其他合约或 init() 调用。

solidity
contract Functions {
    val: Int = 0;
    init() {}
 
    // 这个合约方法只能从这个合约内部调用并访问其变量
    fun onlyZeros() {
        require(self.val == 0, "Only zeros are permitted!");
    }
 
    // 接收器函数,调用内部函数 onlyZeros
    receive("only zeros") {
        self.onlyZeros();
    }
}

请注意,Tact 还支持其他类型的函数,详见:函数。