Skip to content

Statements

以下语句可以出现在 函数 体的任何位置。

let 语句 [#let]

let 语句允许局部和 作用域变量声明。

在 Tact 中,声明局部变量总是需要一个初始值。然而,可以省略类型说明,Tact 将尝试从初始值推断类型:

solidity
let value: Int = 123; // 完整声明,包含类型和值
let vInferred = 123;  // 推断类型为 Int

let vExplicitCtx: Context = context(); // 明确类型为 Context,这是一个内置的 Struct
let vCtx = context();                  // 推断类型为 Context

注意,null 的初始值可以表示任意类型的空 map<K, V>optional 类型的缺失值。因此,当你声明一个 optionalmap<K, V> 时,需要明确指定类型,因为它无法被推断:

solidity
let vOptional: Int? = null; // 明确类型为 Int 或 null
let vOptInt = 42;           // 隐式类型为 Int
vOptInt = null;             // 编译错误!

let vMap: map<Int, Int> = emptyMap(); // 明确类型为 map<Int, Int>
let vMapWithSerialization: map<Int as uint8, Int as uint8> = emptyMap();

使用下划线 _ 命名局部变量表示其值未使用并被丢弃。这在你不需要某些带有副作用函数的返回值时非常有用,并且希望明确标记变量未被使用。注意,这样的通配符变量名 _ 不能被访问:

solidity
let _ = someFunctionWithSideEffects(); // 使用类型推断
let _: map<Int, Int> = emptyMap();     // 明确类型

dump(_); // 编译错误!不能访问 _

return 语句 [#return]

return 语句结束 函数 执行并指定一个返回值给 函数 调用者。

solidity
// 简单的包装器,调用 stdlib 函数 now()
fun getTimeFromNow(offset: Int): Int {
    return now() + offset;
}

块语句用于分组零个或多个语句。块由一对大括号(“花括号”,{})界定,包含零个或多个语句和声明。

某些语句,如 letreturn,必须以分号 ; 结尾。然而,块中最后一个语句的分号是可选的,可以省略。

solidity
{ // <- 块的开始
    // 任意语句:
    let value: Int = 2 + 2;
    dump(value);
} // <- 块的结束

{ dump(2 + 2) } // 只有一个语句的块,
                // 省略了最后的唯一一个分号

{
    let nah = 3 * 3 * 3; // 有两个语句的块,
    let yay = nah + 42   // 但没有最后的分号
}

表达式

表达式语句是在需要语句的地方使用的表达式。表达式被计算,其结果被丢弃——因此,它仅对有副作用的表达式有意义,如执行函数或更新变量。

solidity
dump(2 + 2); // stdlib 函数

赋值

赋值语句使用 赋值运算符 (=) 或 增强赋值运算符(赋值与操作相结合):

solidity
let value: Int; // 声明
value = 5;      // 赋值
value += 5;     // 增强赋值(许多之一,见下文)

分支

控制代码的流程。

if...else [#if-else]

执行 if...else 语句时,首先评估指定的条件。如果结果值为 true,则执行随后的语句块。否则,如果条件评估为 false,则执行可选的 else 块。如果缺少 else 块,则什么也不会发生,执行继续。

常规的 if 语句:

solidity
// 条件
// ↓
if (true) { // 当条件为真时的后果
    dump(2 + 2);
}

带有 else 块:

solidity
// 条件
// ↓
if (2 + 2 == 4) {
    // 当条件为真时的后果
    dump(true);
} else {
    // 当条件为假时的替代
    dump(false);
}

带有嵌套的 if...else

solidity
// 条件
// ↓
if (2 + 2 == 3) {
    // 当条件为真时的后果
    dump("3?");
//        条件2
//        ↓
} else if (2 + 2 == 4) {
    // 当条件2为真时的另一个后果
    dump(true);
} else {
    // 当条件和条件2都为假时的替代
    dump(false);
}

try...catch [#try-catch]

try...catch 语句由一个 try 块和一个可选的 catch 块组成,catch 块接收一个 Int 退出码 作为其唯一参数。首先执行 try 块中的代码,如果失败,则执行 catch 块中的代码,并且尽可能回滚 try 块中所做的更改。

常规的 try 语句:

solidity
fun braveAndTrue() {
    // 尝试做一些错误的事情
    try {
        nativeThrow(42); // 抛出退出码 42
    }

    // 由于上面的错误代码被包装在 try 块中,以下代码将被执行
    dump(42);
}

带有 catch (e) 块:

solidity
fun niceCatch() {
    // 尝试做一些错误的事情
    try {
        nativeThrow(42); // 抛出退出码 42
    } catch (err) {
        dump(err);       // 这将转储捕获的退出码,即 42
    }
}

带有嵌套的 try...catch

solidity
try {
    // 准备一个 x 等于 0,以一种 Tact 编译器尚未意识到的方式
    let xs: Slice = beginCell().storeUint(0, 1).endCell().beginParse();
    let x: Int = xs.loadUint(1); // 0

    try {
        throw(101);     // 1. 抛出退出码 101
    } catch (err) {     // 2. 捕获错误并将其退出码(101)捕获为 err
        return err / x; // 3. 将 err 除以 x,即 0,抛出退出码 4
    }

} catch (err) {         // 4. 捕获新错误并将其退出码(4)捕获为 err
    //   ^^^ 这可以正常工作,因为前一个 err
    //       有不同的作用域,只在前一个 catch 块中可见

    dump(err);          // 5. 转储最后捕获的退出码(4)
}

注意,类似于 let 语句,在 catch () 子句中捕获的 退出码 可以通过指定下划线 _ 来丢弃:

solidity
try {
    throw(42);
} catch (_) {
    dump("我不知道退出码了");
}

循环

有条件地多次重复某些代码块。

repeat [#repeat-loop]

repeat 循环执行指定次数的代码块。重复次数必须是非负的 $32$ 位 Int。否则,将抛出 退出码 5 的错误,整数超出预期范围

在以下示例中,循环内的代码将执行 $10$ 次:

solidity
let twoPow: Int = 1;
repeat (10) {  // 精确重复 10 次
    twoPow *= 2;
}

while [#while-loop]

while 循环在给定条件为 true 时继续执行代码块。

在以下示例中,每次迭代 x 的值减 $1$,因此循环将运行 $10$ 次:

solidity
let x: Int = 10;
while (x > 0) {
    x -= 1;
}

do...until [#do-until-loop]

do...until 循环是一个后测试循环,至少执行一次代码块,然后继续执行直到给定条件变为 true

在以下示例中,每次迭代 x 的值减 $1$,因此循环将运行 $10$ 次:

solidity
let x: Int = 10;
do {
    x -= 1;  // 至少执行一次此代码块
} until (x <= 0);

foreach [#foreach-loop]

foreach 循环按顺序操作 map<K, V> 类型的键值对(条目):从 map 的最小键到最大键。

此循环为给定 map 中的每个条目执行一个代码块,在每次迭代中捕获键和值。当你事先不知道 map 中有多少项或不想显式查找每个条目时,这非常方便。

注意,每次迭代中捕获的键和值对的名称是任意的,可以是任何有效的 Tact 标识符,只要它们是当前作用域的新标识符。最常见的选项是:kv,或 keyvalue

在以下示例中,map cells 有 $4$ 个条目,因此循环将运行 $4$ 次:

solidity
// 空 map
let cells: map<Int, Cell> = emptyMap();

// 设置四个条目
cells.set(1, beginCell().storeUint(100, 16).endCell());
cells.set(2, beginCell().storeUint(200, 16).endCell());
cells.set(3, beginCell().storeUint(300, 16).endCell());
cells.set(4, beginCell().storeUint(400, 16).endCell());

// 用于求和值的变量
let sum: Int = 0;

// 对 cells map 中的每个键值对执行:
foreach (key, value in cells) { // 或者只是 k, v
    let s: Slice = value.beginParse(); // 将 Cell 转换为 Slice
    sum += s.loadUint(16);             // 求和 Slice 的值
}
dump(sum); // 1000

还可以迭代合约存储中的 map,以及 StructMessage 类型实例中的 map 成员:

solidity
import "@stdlib/deploy";

struct Fizz { oh_my: map<Int, Int> }
message Buzz { oh_my: map<Int, Int> }

contract Iterated {
    oh_my: map<Int, Int>;

    receive("call to iterate!") {
        let oh_my: map<Int, Int> = emptyMap();
        oh_my.set(0, 42);
        oh_my.set(1, 27);

        self.oh_my = oh_my; // 将局部 map 赋值给存储中的 map
        let fizz = Fizz{ oh_my }; // 字段拼接
        let buzz = Buzz{ oh_my }; // 字段拼接

        // 迭代合约存储中的 map
        foreach (key, value in self.oh_my) {
            // ...
        }

        // 迭代 Struct Fizz 实例中的 map 成员
        foreach (key, value in fizz.oh_my) {
            // ...
        }

        // 迭代 Message Buzz 实例中的 map 成员
        foreach (key, value in buzz.oh_my) {
            // ...
        }
    }
}

注意,类似于 let 语句,捕获的键或值(或两者)可以通过指定下划线 _ 来丢弃:

solidity
// 空 map
let quartiles: map<Int, Int> = emptyMap();

// 设置一些条目
quartiles.set(1, 25);
quartiles.set(2, 50);
quartiles.set(3, 75);

// 丢弃捕获的键
// 不修改它们在 map 中
foreach (_, value in quartiles) {}

// 丢弃捕获的值
// 不修改它们在 map 中
foreach (key, _ in quartiles) {}

// 丢弃键和值
// 不修改它们在 map 中
foreach (_, _ in quartiles) {
    // 不能通过 _ 访问,但可以执行所需的操作
    // n 次,其中 n 是 map 的当前长度
}