文章目录
前言
在Solidity语言中,合约是构建以太坊应用的基本单元,它类似于面向对象语言中的类,包含了持久化的状态变量和操作这些变量的函数。本文将深入探讨Solidity合约的各个方面,包括创建、可见性、函数修饰器、继承等重要概念。
合约的创建与构造函数
创建合约可以通过以太坊交易从外部进行,也可以在Solidity合约内部完成。集成开发环境如Remix提供了用户友好的界面来简化创建过程,而通过web3.js的web3.eth.Contract函数则可以以编程方式创建合约。
当合约被创建时,其构造函数会被执行一次。构造函数使用constructor关键字声明,是可选的,且每个合约只能有一个构造函数,不支持重写。构造函数执行完毕后,合约的最终代码会被存储在区块链上,该代码包含所有公开和外部函数,以及所有可通过函数调用到达的函数,但不包括构造函数代码或仅从构造函数调用的内部函数。
以下是一个创建合约的示例:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >= 0.4.22 < 0.9.0 ;
contract OwnedToken {
TokenCreator creator ;
address owner ;
bytes32 name ;
constructor ( bytes32 name_ ) {
owner = msg.sender ;
creator = TokenCreator ( msg.sender );
name = name_ ;
}
// 其他函数...
}
contract TokenCreator {
function createToken ( bytes32 name )
public
returns ( OwnedToken tokenAddress )
{
return new OwnedToken ( name );
}
// 其他函数...
}
可见性与Getter函数
状态变量的可见性
- public:公开状态变量与内部变量的不同之处在于,编译器会自动为它们生成getter函数,从而允许其他合约读取它们的值。当在同一个合约中使用时,外部访问(例如
this.x)会调用getter,而内部访问(例如x)会直接从存储中获取变量值。Setter函数没有被生成,所以其他合约不能直接修改其值。 - internal:内部状态变量只能从它们所定义的合约和派生合约中访问,不能被外部访问,这是状态变量的默认可见性。
- private:私有状态变量就像内部变量一样,但它们在派生合约中是不可见的。
需要注意的是,标记变量为private或internal只能防止其他合约读取或修改信息,但它仍然会被区块链之外的整个世界看到。
函数的可见性
Solidity有两种函数调用:创建实际EVM消息调用的外部函数和不创建EVM消息调用的内部函数。此外,派生合约可能无法访问内部函数,这就产生了四种类型的函数可见性:
- external:外部函数作为合约接口的一部分,可以从其他合约和交易中调用。一个外部函数
f不能从内部调用(即f()不起作用,但this.f()可以)。 - public:公开函数是合约接口的一部分,可以在内部或通过消息调用。
- internal:内部函数只能从当前的合约或从它派生出来的合约中访问,不能被外部访问。由于它们没有通过合约的ABI暴露在外部,它们可以接受内部类型的参数,如映射或存储引用。
- private:私有函数和内部函数一样,但它们在派生合约中是不可见的。
Getter函数
编译器会自动为所有公开状态变量创建getter函数。对于数组类型的public状态变量,只能通过生成的getter函数检索数组的单个元素,若想返回整个数组,需要自定义函数。
以下是getter函数的示例:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >= 0.4.16 < 0.9.0 ;
contract C {
uint public data = 42 ;
}
contract Caller {
C c = new C ();
function f () public view returns ( uint ) {
return c . data ();
}
}
函数修饰器
函数修饰器可以用来以声明的方式改变函数的行为,例如在执行函数之前自动检查一个条件。修饰器是合约的可继承属性,可以被派生合约重写,但只有当它们被标记为virtual时才能被重写。
在修饰器中,使用_;占位符来表示被修饰函数的主体应该插入的位置。修饰器可以接受参数,并且可以在同一个函数上应用多个修饰器,它们会按照指定的顺序执行。
以下是一个使用修饰器的示例:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >= 0.7.1 < 0.9.0 ;
contract owned {
constructor () { owner = payable ( msg.sender ); }
address payable owner ;
modifier onlyOwner {
require (
msg.sender == owner ,
"Only owner can call this function."
);
_ ;
}
}
contract destructible is owned {
function destroy () public onlyOwner {
selfdestruct ( owner );
}
}
常量和不可变状态变量
状态变量可以被声明为constant或immutable,在这两种情况下,变量在合约构建完成后不能被修改。
- constant:常量变量的值必须在编译时固定,编译器不会为其预留存储,每次访问都会被替换为相应的常量表达式,燃料成本较低。
- immutable:不可变变量可以在构造时赋值,值在部署前可以更改,之后成为永久值。不可变变量在构造时被评估一次,其值被复制到代码中所有被访问的地方。
以下是常量和不可变变量的示例:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^ 0.8.21 ;
uint constant X = 32 ** 22 + 8 ;
contract C {
string constant TEXT = "abc" ;
bytes32 constant MY_HASH = keccak256 ( "abc" );
uint immutable decimals = 18 ;
uint immutable maxBalance ;
address immutable owner = msg.sender ;
constructor ( uint decimals_ , address ref ) {
if ( decimals_ != 0 )
decimals = decimals_ ;
maxBalance = ref . balance ;
}
// 其他函数...
}
特殊函数
接收以太的函数
一个合约最多可以有一个receive函数,使用receive() external payable { ... }声明。这个函数不能有参数,不能返回任何东西,必须具有external的可见性和payable的状态可变性。当合约接收到空的calldata时,receive函数会被执行,例如在纯以太传输时。
如果没有receive函数,但存在一个payable类型的fallback函数,那么fallback函数将在纯以太传输时被调用。如果两者都没有,合约将无法接收以太币。
Fallback函数
一个合约最多可以有一个fallback函数,使用fallback () external [payable]或fallback (bytes calldata input) external [payable] returns (bytes memory output)声明。当其他函数都不符合给定的函数签名,或者根本没有提供数据,并且没有receive函数时,fallback函数将被执行。
fallback函数总是接收数据,但为了同时接收以太,它必须被标记为payable。如果使用带参数的版本,input将包含发送给合约的全部数据,output中可以返回数据。
继承
Solidity支持多重继承,包括多态性。多态性意味着函数调用总是执行继承层次结构中最新继承的合约中的同名函数,但必须使用virtual和override关键字在层次结构中的每个函数上明确启用。
通过使用ContractName.functionName()可以明确指定合约来调用继承层次结构中更高的函数,或者使用super.functionName()在扁平化的继承层次中调用高一级的函数。
以下是继承的示例:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >= 0.7.0 < 0.9.0 ;
contract Owned {
constructor () { owner = payable ( msg.sender ); }
address payable owner ;
}
contract Destructible is Owned {
function destroy () virtual public {
if ( msg.sender == owner ) selfdestruct ( owner );
}
}
contract Named is Owned , Destructible {
constructor ( bytes32 name ) {
// 构造函数逻辑
}
function destroy () public virtual override {
if ( msg.sender == owner ) {
// 重写的逻辑
Destructible . destroy ();
}
}
}
事件
Solidity事件在EVM的日志功能之上提供了一个抽象,应用程序可以通过Ethereum客户端的RPC接口订阅和监听这些事件。事件是合约的可继承成员,调用事件时会将参数存储在交易的日志中。
可以最多给三个参数添加indexed属性,将它们添加到“topics”中,以便于搜索。所有没有indexed属性的参数会被ABI编码到日志的数据部分。
以下是事件的示例:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >= 0.4.21 < 0.9.0 ;
contract ClientReceipt {
event Deposit (
address indexed from ,
bytes32 indexed id ,
uint value
);
function deposit ( bytes32 id ) public payable {
emit Deposit ( msg.sender , id , msg.value );
}
}
错误和恢复语句
Solidity中的错误提供了一种方便且省燃料的方式来向用户解释操作失败的原因。错误可以定义在合约内部和外部,必须与恢复语句一起使用,导致当前调用中的所有变化被恢复,并将错误数据传回给调用者。
错误使用error关键字定义,例如:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^ 0.8.4 ;
error InsufficientBalance ( uint256 available , uint256 required );
contract TestToken {
mapping ( address => uint ) balance ;
function transfer ( address to , uint256 amount ) public {
if ( amount > balance [ msg.sender ])
revert InsufficientBalance ({
available : balance [ msg.sender ],
required : amount
});
balance [ msg.sender ] -= amount ;
balance [ to ] += amount ;
}
}
接口和库合约
接口合约
接口合约类似于抽象合约,但不能实现任何函数,且有更多限制:不能继承其他合约(但可以继承其他接口合约)、所有函数必须是external类型、不能声明构造函数、状态变量和修饰器。
接口合约由interface关键字声明,例如:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >= 0.6.2 < 0.9.0 ;
interface Token {
enum TokenType { Fungible , NonFungible }
struct Coin { string obverse ; string reverse ; }
function transfer ( address recipient , uint amount ) external ;
}
库合约
库合约与普通合约类似,但只需在特定地址部署一次,其代码可以通过EVM的DELEGATECALL特性重用。库函数在调用合约的上下文中执行,this指向调用合约,可访问调用合约的存储。
库合约不能有状态变量、不能继承或被继承、不能接收以太、不能被销毁。
以下是库合约的示例:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >= 0.6.0 < 0.9.0 ;
struct Data {
mapping ( uint => bool ) flags ;
}
library Set {
function insert ( Data storage self , uint value )
public
returns ( bool )
{
if ( self . flags [ value ])
return false ;
self . flags [ value ] = true ;
return true ;
}
// 其他函数...
}
本文涵盖了合约的基本概念和重要特性,更多详细内容请访问Solidity官方文档。

2万+

被折叠的 条评论
为什么被折叠?



