控制结构

函数

函数是合约中可执行的代码单元。函数只能在合约的 模块作用域 中声明。

@external
def bid():
    ...

函数可以根据它们的 可见性 在内部或外部调用。函数可以接受输入参数并返回变量,以便在它们之间传递值。

可见性

所有函数必须包含一个可见性装饰器。

外部函数

外部函数(用 @external 装饰器标记)是合约接口的一部分,只能通过交易或从其他合约调用。

@external
def add_seven(a: int128) -> int128:
    return a + 7

@external
def add_seven_with_overloading(a: uint256, b: uint256 = 3):
    return a + b

Vyper 合约不能直接在两个外部函数之间调用。如果必须这样做,可以使用 接口

注意

对于具有默认参数的外部函数,例如 def my_function(x: uint256, b: uint256 = 1),Vyper 编译器将基于 N 个默认参数生成 N+1 个重载函数选择器。

内部函数

内部函数(用 @internal 装饰器标记)只能从同一个合约中的其他函数访问。它们通过 self 对象调用

@internal
def _times_two(amount: uint256, two: uint256 = 2) -> uint256:
    return amount * two

@external
def calculate(amount: uint256) -> uint256:
    return self._times_two(amount)

注意

由于调用 internal 函数是通过跳转到其入口标签实现的,内部函数调度程序确保跳转的正确性。请注意,对于使用多个默认参数的 internal 函数,强烈建议使用 Vyper 版本 >=0.3.8,因为安全建议 GHSA-ph9x-4vc9-m39g

可变性

可以使用 装饰器 可选地声明函数的可变性。有四个可变性级别

  • :不从合约状态或任何环境变量读取。

  • 视图:可以从合约状态读取,但不更改它。

  • 非支付:可以从合约状态读取和写入,但不能接收以太坊。

  • 可支付:可以从合约状态读取和写入,并且可以接收以太坊。

@view
@external
def readonly():
    # this function cannot write to state
    ...

@payable
@external
def send_me_money():
    # this function can receive ether
    ...

当未使用可变性装饰器时,函数默认为 nonpayable

@view 标记的函数不能调用可变函数(payablenonpayable)。任何外部调用都是使用特殊的 STATICCALL 操作码进行的,这在 EVM 级别阻止状态更改。

@pure 标记的函数不能调用非 pure 函数。

重入锁

@nonreentrant(<key>) 装饰器对函数以及所有具有相同 <key> 值的函数进行锁定。外部合约尝试回调用这些函数中的任何一个会导致交易回滚。

@external
@nonreentrant("lock")
def make_a_call(_addr: address):
    # this function is protected from re-entrancy
    ...

你可以在 __default__ 函数上放置 @nonreentrant(<key>) 装饰器,但我们建议不要这样做,因为在大多数情况下它不会以有意义的方式工作。

非重入锁通过在函数入口处将一个专门分配的存储槽设置为 <locked> 值,并在函数退出时将其设置为 <unlocked> 值来工作。在函数入口处,如果检测到存储槽是 <locked> 值,则执行将回滚。

你不能在 pure 函数上放置 @nonreentrant 装饰器。你可以在 view 函数上放置它,但它只检查函数是否不在回调中(存储槽不在 <locked> 状态),因为 view 函数只能读取状态,不能更改它。

注意

可变函数可以防止可变函数被回调用(例如,如果一个 view 函数在可变函数期间返回不一致的状态),但 view 函数不能保护自己不被回调用。请注意,可变函数永远不能从 view 函数调用,因为从 view 函数发出的所有外部调用都受到 STATICCALL 操作码的使用保护。

注意

非重入锁的 <unlocked> 值为 3,<locked> 值为 2。非零值用于利用净气体计量 - 从柏林硬分叉开始,使用非重入锁的净成本为 2300 气体。在 v0.3.4 之前,<unlocked><locked> 值分别为 0 和 1。

__default__ 函数

合约还可以有一个默认函数,如果没有任何其他函数与给定的函数标识符匹配(或者根本没有提供任何函数标识符,例如通过有人向其发送以太坊),则在调用合约时执行该函数。它与 Solidity 中的回退函数 相同。

此函数始终命名为 __default__。它必须用 @external 注释。它不能期待任何输入参数。

如果函数被注释为 @payable,则每当合约被发送以太坊(没有数据)时,就会执行此函数。这就是为什么默认函数不能接受参数的原因 - 这是以太坊的设计决定,对发送以太坊到合约或用户地址没有区别。

event Payment:
    amount: uint256
    sender: indexed(address)

@external
@payable
def __default__():
    log Payment(msg.value, msg.sender)

注意事项

与 Solidity 中一样,Vyper 在找不到默认函数的情况下会生成一个默认函数,形式为 REVERT 调用。请注意,这仍然会 生成异常,因此不会成功接收资金。

以太坊规定,如果合约在执行过程中耗尽气体,则操作将回滚。对合约的 send 调用附带 2300 气体的免费津贴,这除了基本的日志记录外,没有留下太多空间来执行其他操作。但是,如果发送者通过 call 而不是 send 包含更高的气体量,那么就可以运行更复杂的功能。

确保你的可支付默认函数与该津贴兼容被认为是一种最佳实践。以下操作将消耗超过 2300 气体

  • 写入存储

  • 创建合约

  • 调用消耗大量气体的外部函数

  • 发送以太坊

最后,尽管默认函数不接受任何参数,但它仍然可以访问 msg 对象,包括

  • 与合约交互者的地址(msg.sender

  • 发送的以太坊金额(msg.value

  • 提供的 gas(msg.gas)。

__init__ 函数

__init__ 是一个特殊的初始化函数,只能在部署合约时调用。它可以用来设置存储变量的初始值。一个常见的用例是使用创建者将合约设置为一个 owner 变量

owner: address

@external
def __init__():
    self.owner = msg.sender

你不能从初始化函数调用其他合约函数。

装饰器参考

所有函数必须包含一个 可见性 装饰器(@external@internal)。其余装饰器是可选的。

装饰器

描述

@external

函数只能从外部调用

@internal

函数只能在当前合约中调用

@pure

函数不读取合约状态或环境变量

@view

函数不改变合约状态

@payable

函数可以接收以太坊

@nonreentrant(<unique_key>)

在外部调用期间,无法回调函数

if 语句

if 语句是用于条件执行的控制流结构

if CONDITION:
    ...

CONDITION 是布尔值或布尔运算。布尔值从左到右逐个表达式进行评估,直到发现条件为真或假。如果为真,则执行 if 语句体内的逻辑。

请注意,与 Python 不同,Vyper 不允许在 if 语句的条件中从非布尔值类型隐式转换。 if 1: pass 将无法编译,并出现类型不匹配错误。

您还可以包含 elifelse 语句,以添加更多条件语句以及在条件为假时执行的语句体

if CONDITION:
    ...
elif OTHER_CONDITION:
    ...
else:
    ...

for 循环

for 语句是用于迭代值的控制流结构

for i in <ITERABLE>:
    ...

迭代的值可以是静态数组、动态数组或由内置的 range 函数生成。

数组迭代

您可以使用 for 来迭代任何数组变量的值

foo: int128[3] = [4, 23, 42]
for i in foo:
    ...

在上面的示例中,循环执行三次,i 分别分配 42342 的值。

您还可以迭代字面量数组,只要可以确定数组中每个项目的通用类型

for i in [4, 23, 42]:
    ...

一些限制

  • 您不能迭代多维数组。 i 必须始终是基本类型。

  • 您不能在迭代数组时修改数组中的值,也不能调用可能修改正在迭代的数组的函数。

范围迭代

范围使用 range 函数创建。以下示例是 range 的有效用法

for i in range(STOP):
    ...

STOP 是大于零的字面量整数。 i 从零开始,每次增加 1,直到等于 STOP

for i in range(stop, bound=N):
    ...

这里,stop 可以是具有整数类型的变量,大于零。 N 必须是编译时常量。 i 从零开始,每次增加 1,直到等于 stop。如果 stop 大于 N,则执行将在运行时回滚。在某些情况下,您可能无法保证 stop 小于 N,但仍希望避免运行时回滚的可能性。为了实现这一点,在 range 的参数中使用 bound= 关键字以及 min(stop, N),例如 range(min(stop, N), bound=N)。这对于跨多个交易对较大的数组进行操作时的分块操作等用例很有帮助。

范围的另一种用法可以与 STARTSTOP 边界一起使用。

for i in range(START, STOP):
    ...

这里,STARTSTOP 是字面量整数,其中 STOP 的值大于 STARTiSTART 开始,每次增加 1,直到等于 STOP

for i in range(a, a + N):
    ...

a 是具有整数类型的变量,N 是大于零的字面量整数。 ia 开始,每次增加 1,直到等于 a + N。如果 a + N 会溢出,则执行将回滚。