Solidity 合约类似于面向对象编程语言中的类,包含状态变量中存储的持久化数据,以及用于修改这些数据的函数。在不同合约实例上调用函数会触发 EVM 函数调用,切换执行上下文,使调用合约的状态变量无法直接访问。所有合约和函数的变更必须通过显式调用触发,以太坊没有内置的“cron”机制来自动调用函数。
合约的创建与部署
合约可以通过以太坊交易“从外部”创建,或在其他 Solidity 合约内部创建。集成开发环境(IDE)如 Remix 通过用户界面简化了创建流程。在以太坊上以编程方式创建合约的一种方法是使用 JavaScript API web3.js,它提供了 web3.eth.Contract 函数来简化合约创建过程。
创建合约时,其构造函数(用 constructor 关键字声明的函数)会执行一次。构造函数是可选的,每个合约最多只能有一个构造函数,不支持重载。
构造函数执行完毕后,合约的最终代码将存储在区块链上。此代码包括所有公共和外部函数,以及可以通过函数调用访问的所有函数。部署的代码不包含构造函数代码或仅从构造函数调用的内部函数。
可见性与 Getter 函数
状态变量可见性
- public:公共状态变量与内部状态变量的区别在于编译器会自动为它们生成 getter 函数,允许其他合约读取其值。在同一合约内使用时,外部访问(如
this.x)会调用 getter,而内部访问(如x)则直接从存储中获取变量值。不会生成 setter 函数,因此其他合约无法直接修改它们的值。 - internal:内部状态变量只能在定义它们的合约及其派生合约中访问,无法被外部访问。这是状态变量的默认可见性级别。
- private:私有状态变量类似于内部变量,但在派生合约中不可见。
函数可见性
Solidity 有两种类型的函数调用:外部调用会创建实际的 EVM 消息调用,而内部调用则不会。此外,内部函数可以对派生合约不可访问,这产生了四种函数的可见性类型:
- external:外部函数是合约接口的一部分,可以从其他合约和通过交易调用。
- public:公共函数是合约接口的一部分,可以通过内部调用或消息调用。
- internal:内部函数只能在当前合约内或从其派生的合约中访问,无法被外部访问。
- private:私有函数类似于内部函数,但在派生合约中不可见。
Getter 函数
编译器会自动为所有 public 状态变量创建 getter 函数。对于数组类型的 public 状态变量,只能通过生成的 getter 函数检索单个元素。如果想在一次调用中返回整个数组,需要编写一个自定义函数。
函数修改器
修改器可以以声明方式改变函数的行为。例如,可以使用修改器在执行函数之前自动检查条件。修改器是合约的可继承属性,可以被派生合约重写,但只有在标记为 virtual 的情况下才可以。
修改器不能隐式访问或更改它们所修饰的函数的参数和返回值。在函数修改器中,必须使用占位符语句 _ 表示应插入被修饰函数的主体的位置。
常量和不可变状态变量
状态变量可以声明为 constant(常量)或 immutable(不可变量)。在这两种情况下,变量在合约构造后不能被修改。对于 constant 变量,值必须在编译时固定;而对于 immutable,它仍然可以在构造时赋值。
与常规状态变量相比,常量和不可变变量的 gas 成本要低得多。目前支持的类型包括字符串(仅适用于常量)和值类型。
函数详解
函数可以在合约内外定义。合约外的函数(自由函数)始终具有隐式的 internal 可见性,它们的代码包含在所有调用它们的合约中,类似于内部库函数。
函数参数和返回变量
函数接受类型化参数作为输入,并可以返回任意数量的值作为输出。函数参数的声明方式与变量相同,未使用参数的名称可以省略。返回变量在 returns 关键字后声明,可以像其他局部变量一样使用。
状态可变性
- view 函数:声明为
view的函数承诺不修改状态。 - pure 函数:声明为
pure的函数承诺不读取或修改状态。
特殊函数
- 接收以太币函数:合约最多可以有一个
receive函数,用于处理普通以太币转账。 - 回退函数:合约最多可以有一个
fallback函数,在没有其他函数与给定的函数签名匹配时执行。
事件与错误处理
Solidity 事件在 EVM 的日志功能之上提供了一个抽象层,应用程序可以通过以太坊客户端的 RPC 接口订阅和监听这些事件。事件允许将参数存储在交易的日志中,这是区块链中的一种特殊数据结构。
自定义错误提供了一种方便且节省 gas 的方式来向用户解释操作失败的原因。它们可以在合约内部和外部定义,必须与 revert 语句或 require 函数一起使用。
继承与抽象合约
Solidity 支持多重继承,包括多态。这意味着函数调用始终在继承层次结构中最派生的合约中执行同名函数。必须使用 virtual 和 override 关键字显式启用此功能。
抽象合约是至少有一个函数未实现或未为所有基类构造函数提供参数的合约。它们必须标记为 abstract,即使不是这种情况,当不打算直接创建该合约时,也可以标记为抽象。
接口与库
接口类似于抽象合约,但不能实现任何函数。它们有进一步的限制,如不能继承其他合约、所有函数必须是外部的、不能声明构造函数等。
库类似于合约,但目的是在特定地址上部署一次,并通过 EVM 的 DELEGATECALL 功能重用其代码。这意味着库函数在调用合约的上下文中执行,可以访问调用合约的存储。
Using For 指令
指令 using A for B 可用于将函数作为运算符附加到用户定义的值类型或作为任何类型的成员函数。这在文件级别或合约内部都有效,可以用于扩展内置类型或用户定义类型的功能。
常见问题
什么是 Solidity 合约?
Solidity 合约类似于面向对象语言中的类,包含状态变量和函数,用于在以太坊区块链上实现去中心化应用逻辑。
如何创建合约?
合约可以通过以太坊交易从外部创建,或在其他合约内部创建。使用 web3.js 等工具可以编程方式创建合约。
什么是函数修改器?
函数修改器可以以声明方式改变函数的行为,常用于在执行函数前自动检查条件。
常量和不可变变量有什么区别?
常量在编译时固定值,而不可变变量在构造时可以赋值,之后不能修改。
什么是接口?
接口类似于抽象合约,但不能实现任何函数,主要用于定义合约之间的交互标准。
如何使用库?
库通过 DELEGATECALL 在调用合约的上下文中执行代码,可以用于实现可重用的功能模块。