Appearance
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 类型的缺失值。因此,当你声明一个 optional 或 map<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;
}
块
块语句用于分组零个或多个语句。块由一对大括号(“花括号”,{}
)界定,包含零个或多个语句和声明。
某些语句,如 let
或 return
,必须以分号 ;
结尾。然而,块中最后一个语句的分号是可选的,可以省略。
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 标识符,只要它们是当前作用域的新标识符。最常见的选项是:k
和 v
,或 key
和 value
。
在以下示例中,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,以及 Struct 或 Message 类型实例中的 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 的当前长度
}