合约
Solidity 中的合约相似于面向对象语言中的类。 它们蕴含状态变量中的持久数据,以及能够批改这些变量的函数。 在不同的合约(实例)上调用函数将执行 EVM 函数调用,从而切换上下文,使得调用合约中的状态变量不可拜访。 须要调用合约及其性能能力产生任何事件。 以太坊中没有“cron”概念能够在特定事件时主动调用函数。
创立合约
能够通过以太坊交易“从内部”或从 Solidity 合约外部创立合约。
一些集成开发环境,例如 Remix, 通过应用一些UI用户界面使创立合约的过程更加顺畅。 在以太坊上通过编程创立合约最好应用 JavaScript API web3.js。 当初,咱们曾经有了一个叫做 web3.eth.Contract 的办法可能更容易的创立合约。
创立合约时, 合约的 构造函数 (一个用关键字 constructor
申明的函数)会执行一次。 构造函数是可选的。只容许有一个构造函数,这意味着不反对重载。
构造函数执行结束后,合约的最终代码将部署到区块链上。此代码包含所有公共和内部函数以及所有能够通过函数调用拜访的函数。 部署的代码没有 包含构造函数代码或结构函数调用的外部函数。
在外部,结构函数参数在合约代码之后通过 ABI 编码 传递,然而如果你应用 web3.js
则不用关怀这个问题。
如果一个合约想要创立另一个合约,那么创建者必须通晓被创立合约的源代码(和二进制代码)。 这意味着不可能循环创立依赖项。
// SPDX-License-Identifier: GPL-3.0pragma solidity >=0.4.22 <0.9.0;contract OwnedToken { // TokenCreator 是前面定义的合约类型. // 不创立新合约的话,也能够援用它。 TokenCreator creator; address owner; bytes32 name; // 这是注册 creator 和设置名称的构造函数。 constructor(bytes32 name_) { // 状态变量通过其名称拜访,而不是通过例如 this.owner 的形式拜访。 // 这也实用于函数,特地是在构造函数中,你只能像这样(“外部地”)调用它们, // 因为合约自身还不存在。 owner = msg.sender; // 从 `address` 到 `TokenCreator` ,是做显式的类型转换 // 并且假设调用合约的类型是 TokenCreator,没有真正的办法来查看这一点。 creator = TokenCreator(msg.sender); name = name_; } function changeName(bytes32 newName) public { // 只有 creator (即创立以后合约的合约)可能更改名称 —— 因为合约是隐式转换为地址的, // 所以这里的比拟是可行的。 if (msg.sender == address(creator)) name = newName; } function transfer(address newOwner) public { // 只有以后所有者能力发送 token。 if (msg.sender != owner) return; // 咱们也想询问 creator 是否能够发送。 // 请留神,这里调用了一个上面定义的合约中的函数。 // 如果调用失败(比方,因为 gas 有余),会立刻进行执行。 if (creator.isTokenTransferOK(owner, newOwner)) owner = newOwner; }}contract TokenCreator { function createToken(bytes32 name) public returns (OwnedToken tokenAddress) { // 创立一个新的 Token 合约并且返回它的地址。 // 从 JavaScript 方面来说,返回类型是简略的 `address` 类型,因为 // 这是在 ABI 中可用的最靠近的类型。 return new OwnedToken(name); } function changeName(OwnedToken tokenAddress, bytes32 name) public { // 同样,`tokenAddress` 的内部类型也是 `address` 。 tokenAddress.changeName(name); } function isTokenTransferOK(address currentOwner, address newOwner) public view returns (bool ok) { // 查看一些任意的状况。 address tokenAddress = msg.sender; return (keccak256(newOwner) & 0xff) == (bytes20(tokenAddress) & 0xff); }}
可见性和 getter 函数
状态变量可见性
public
对于 public 状态变量会主动生成一个getter函数(见上面)。 以便其余的合约读取他们的值。 当在用一个合约里应用是,内部形式拜访 (如: this.x
) 会调用getter 函数,而外部形式拜访 (如: x
) 会间接从存储中获取值。 Setter函数则不会被生成,所以其余合约不能间接批改其值。
internal
外部可见性状态变量只能在它们所定义的合约和派生合同中拜访。 它们不能被内部拜访。 这是状态变量的默认可见性。
private
公有状态变量就像外部变量一样,但它们在派生合约中是不可见的。
<aside>
设置为 private
和internal
,只能避免其余合约读取或批改信息,但它依然能够在链外查看到。
</aside>
函数可见性
因为 Solidity 有两种函数调用:内部调用则会产生一个 EVM 调用,而外部调用不会, 更进一步, 函数能够确定器被外部及派生合约的可拜访性,这里有 4 种可见性:
external
内部可见性函数作为合约接口的一部分,意味着咱们能够从其余合约和交易中调用。 一个内部函数 f
不能从外部调用(即 f
不起作用,但 this.f()
能够)。
public
public 函数是合约接口的一部分,能够在外部或通过音讯调用。
internal
外部可见性函数拜访能够在以后合约或派生的合约拜访,不能够内部拜访。 因为它们没有通过合约的ABI向内部公开,它们能够承受外部可见性类型的参数:比方映射或存储援用。
private
private 函数和状态变量仅在以后定义它们的合约中应用,并且不能被派生合约应用。
可见性标识符的定义地位,对于状态变量来说是在类型前面,对于函数是在参数列表和返回关键字两头。
pragma solidity >=0.4.16 <0.9.0;contract C { function f(uint a) private pure returns (uint b) { return a + 1; } function setData(uint a) internal { data = a; } uint public data;}
在上面的例子中,D
能够调用 c.getData()
来获取状态存储中 data
的值,但不能调用 f
。 合约 E
继承自 C
,因而能够调用 compute
。
pragma solidity >=0.4.16 <0.9.0;contract C { uint private data; function f(uint a) private returns(uint b) { return a + 1; } function setData(uint a) public { data = a; } function getData() public returns(uint) { return data; } function compute(uint a, uint b) internal returns (uint) { return a+b; }}// 上面代码编译谬误contract D { function readData() public { C c = new C(); uint local = c.f(7); // 谬误:成员 `f` 不可见 c.setData(3); local = c.getData(); local = c.compute(3, 5); // 谬误:成员 `compute` 不可见 }}contract E is C { function g() public { C c = new C(); uint val = compute(3, 5); // 拜访外部成员(从继承合约拜访父合约成员) }}
Getter 函数
编译器主动为所有 public状态变量创立 getter 函数。对于上面给出的合约,编译器会生成一个名为 data
的函数, 该函数没有参数,返回值是一个 uint
类型,即状态变量 data
的值。 状态变量的初始化能够在申明时实现。
pragma solidity >=0.4.16 <0.9.0;contract C { uint public data = 42;}contract Caller { C c = new C(); function f() public { uint local = c.data(); }}
getter 函数具备内部(external)可见性。如果在外部拜访 getter(即没有 this.
),它被认为一个状态变量。 如果应用内部拜访(即用 this.
),它被认作为一个函数。
pragma solidity >=0.4.16 <0.9.0;contract C { uint public data; function x() public { data = 3; // 外部拜访 uint val = this.data(); // 内部拜访 }}
如果你有一个数组类型的 public
状态变量,那么你只能通过生成的 getter 函数拜访数组的单个元素。 这个机制以防止返回整个数组时的高老本gas。 能够应用如 myArray(0)
用于指定参数要返回的单个元素。 如果要在一次调用中返回整个数组,则须要写一个函数,例如:
pragma solidity >=0.4.0 <0.9.0;contract arrayExample { // public state variable uint[] public myArray; // 指定生成的Getter 函数 /* function myArray(uint i) public view returns (uint) { return myArray[i]; } */ // 返回整个数组 function getArray() public view returns (uint[] memory) { return myArray; }}
当初能够应用 getArray()
取得整个数组,而 myArray(i)
是返回单个元素。
下一个例子略微简单一些:
pragma solidity ^0.4.0 <0.9.0;contract Complex { struct Data { uint a; bytes3 b; mapping (uint => uint) map; uint[3] c; uint[] d; bytes e; } mapping (uint => mapping(bool => Data[])) public data;}
这将会生成以下模式的函数,在构造体内的映射和数组(byte 数组除外)被省略了,因为没有好方法为单个构造成员或为映射提供一个键。
function data(uint arg1, bool arg2, uint arg3) public returns (uint a, bytes3 b, bytes memory e){ a = data[arg1][arg2][arg3].a; b = data[arg1][arg2][arg3].b; e = data[arg1][arg2][arg3].e;}
函数 修改器modifier
应用 修改器modifier能够轻松扭转函数的行为。 例如,它们能够在执行函数之前主动查看某个条件。 修改器modifier是合约的可继承属性,并可能被派生合约笼罩 , 但前提是它们被标记为 virtual
。 无关详细信息,请参见 Modifier 重载
// SPDX-License-Identifier: GPL-3.0pragma solidity >=0.7.1 <0.9.0;contract owned { constructor() { owner = payable(msg.sender); } address owner; // 这个合约只定义一个修改器,但并未应用: 它将会在派生合约中用到。 // 修改器所润饰的函数领会被插入到特殊符号 _; 的地位。 // 这意味着如果是 owner 调用这个函数,则函数会被执行,否则会抛出异样。 modifier onlyOwner { require( msg.sender == owner, "Only owner can call this function." ); _; }}contract destructible is owned { // 这个合约从 `owned` 继承了 `onlyOwner` 修饰符,并将其利用于 `destroy` 函数, // 只有在合约里保留的 owner 调用 `destroy` 函数,才会失效。 function destroy() public onlyOwner { selfdestruct(owner); }}contract priced { // 修改器能够接管参数: modifier costs(uint price) { if (msg.value >= price) { _; } }}contract Register is priced, destructible { mapping (address => bool) registeredAddresses; uint price; constructor(uint initialPrice) { price = initialPrice; } // 在这里也应用关键字 `payable` 十分重要,否则函数会主动回绝所有发送给它的以太币。 function register() public payable costs(price) { registeredAddresses[msg.sender] = true; } function changePrice(uint price_) public onlyOwner { price = price_; }}contract Mutex { bool locked; modifier noReentrancy() { require( !locked, "Reentrant call." ); locked = true; _; locked = false; } // 这个函数受互斥量爱护,这意味着 `msg.sender.call` 中的重入调用不能再次调用 `f`。 // `return 7` 语句指定返回值为 7,但修改器中的语句 `locked = false` 仍会执行。 function f() public noReentrancy returns (uint) { (bool success,) = msg.sender.call(""); require(success); return 7; }}
如果你想拜访定义在合约 C
的 修改器modifier m
, 能够应用 C.m
去援用它,而不须要应用虚构表查找。
只能应用在以后合约或在基类合约中定义的 修改器modifier , 修改器modifier 也能够定义在库外面,然而他们被限定在库函数应用。
如果同一个函数有多个 修改器modifier,它们之间以空格隔开,修改器modifier 会顺次查看执行。
修改器不能隐式地拜访或扭转它们所润饰的函数的参数和返回值。 这些值只能在调用时明确地以参数传递。
在函数修改器中,指定何时运行被修改器利用的函数是有必要。占位符语句(用单个下划线字符 _
示意)用于示意被批改的函数的主体应该被插入的地位。 请留神,占位符运算符与在变量名称中应用下划线作为前导或尾随字符不同,后者是一种格调上的抉择。
修改器modifier 或函数体中显式的 return 语句仅仅跳出以后的 修改器modifier 和函数体。 返回变量会被赋值,但整个执行逻辑会从前一个 修改器modifier 中的定义的 _
之后继续执行。
Constant 和 Immutable 状态变量
状态变量能够申明为常量constant
或不可变量immutable
的。 在这两种状况下,变量都不能在合约构建后批改。 对于constant
,其值必须在编译时固定,而对于immutable
,它依然能够在结构时赋值。
也能够在文件级别定义常量变量。
编译器不会为这些变量保留一个存储槽,每次呈现都会被相应的值替换。
与惯例状态变量相比,常量和不可变变量的gas老本要低得多。 对于常量变量,调配给它的表达式被复制到所有拜访它的中央,并且每次都从新计算。 这容许局部优化。 不可变变量在结构时被评估一次,并且它们的值被复制到代码中拜访它们的所有地位。 对于这些值,保留了 32 个字节,即便它们适宜更少的字节也是如此。 因而,常量值有时可能比不可变值便宜。
目前并非所有常量和不可变类型都已实现。 惟一反对的类型是字符串(仅实用于常量)和值类型。
// SPDX-License-Identifier: GPL-3.0pragma solidity >0.7.4;uint constant X = 32**22 + 8;contract C { string constant TEXT = "abc"; bytes32 constant MY_HASH = keccak256("abc"); uint immutable decimals; uint immutable maxBalance; address immutable owner = msg.sender; constructor(uint decimals_, address ref) { decimals = decimals_; // Assignments to immutables can even access the environment. maxBalance = ref.balance; } function isBalanceTooHigh(address _other) public view returns (bool) { return _other.balance > maxBalance; }}
Constant
对于constant
常量变量,该值在编译时必须是常量,并且必须在申明变量的中央赋值。 任何拜访存储、区块链数据(例如 block.timestamp
、address(this).balance
或 block.number
)或执行数据(msg.value
或 gasleft()
)或调用内部合约的表达式都是不容许的。 容许应用可能对内存调配产生副作用的表达式,但不容许应用可能对其余内存对象产生副作用的表达式。 内置函数 keccak256
、sha256
、ripemd160
、ecrecover
、addmod
和 mulmod
是容许的(只管除了 keccak256
,它们的确调用内部合约)。
Immutable
申明为不可变immutable
的变量比申明为常量constant
的变量受到的限度要少一些:能够在合约的构造函数中或在申明时为不可变变量调配任意值。 它们只能调配一次,并且从那时起,即便在构建期间也能够读取。
编译器生成的合约创立代码将在合约返回之前批改合约的运行时代码,办法是将所有对不可变对象的援用替换为调配给它们的值。 如果您要将编译器生成的运行时代码与理论存储在区块链中的代码进行比拟,这一点很重要。
<aside>
不可变量能够在申明时赋值,不过只有在合约的构造函数执行时才被视为视为初始化。 这意味着,你不能用一个依赖于不可变量的值在行内初始化另一个不可变量。 不过,你能够在合约的构造函数中这样做。
这是为了避免对状态变量初始化和构造函数程序的不同解释,特地是继承时,呈现问题。
</aside>
函数
能够在合约外部和内部定义函数。
合约之外的函数(也称为“自在函数”)始终具备隐式的 internal
可见性。 它们的代码蕴含在所有调用它们合约中,相似于外部库函数。
// SPDX-License-Identifier: GPL-3.0pragma solidity >=0.7.1 <0.9.0;function sum(uint[] memory arr) pure returns (uint s) { for (uint i = 0; i < arr.length; i++) s += arr[i];}contract ArrayExample { bool found; function f(uint[] memory arr) public { // This calls the free function internally. // The compiler will add its code to the contract. uint s = sum(arr); require(s >= 10); found = true; }}
在合约之外定义的函数依然总是在合约的上下文中执行。 他们依然能够调用其余合约,向它们发送 Ether 并销毁调用它们的合约等。 与合约内定义的函数的次要区别在于,自在函数不能间接拜访变量 this
、存储变量和不在其范畴内的函数。
函数参数及返回值
与 Javascript 一样,函数可能须要参数作为输出; 而与 Javascript 和 C 不同的是,它们可能返回任意数量的参数作为输入。
函数参数(输出参数)
函数参数的申明形式与变量雷同。不过未应用的参数能够省略参数名。
例如,如果咱们心愿合约承受有两个整数形参的函数的内部调用,能够像上面这样写:
pragma solidity >=0.4.16 <0.9.0;contract Simple { uint sum; function taker(uint a, uint b) public { sum = a + b; }}
返回变量
函数返回变量的申明形式在关键词 returns
之后,与参数的申明形式雷同。
例如,如果咱们须要返回两个后果:两个给定整数的和与积,咱们应该写作:
pragma solidity >=0.4.16 <0.9.0;contract Simple { function arithmetic(uint a, uint b) public pure returns (uint sum, uint product) { sum = a + b; product = a * b; }}
返回变量的名称能够省略。 返回变量能够用作任何其余局部变量,它们应用默认值初始化并具备该值,直到它们被(从新)调配。
您能够显式调配给返回变量,而后像下面那样保留函数,或者您能够间接应用 return 语句提供返回值(单个或多个):
contract Simple { function arithmetic(uint a, uint b) public pure returns (uint sum, uint product) { return (a + b, a * b); }}
您不能从非外部函数返回某些类型。 这包含上面列出的类型以及递归蕴含它们的任何复合类型:
- 映射,
- 外部函数类型,
- 地位设置为存储的援用类型,
- 多维数组(仅实用于 ABI 编码器 v1),
- 构造(仅实用于 ABI 编码器 v1)。
此限度不适用于库函数,因为它们具备不同的外部 ABI。
状态可变性
view
能够将函数申明为 view
类型,这种状况下要保障不批改状态。
上面的语句被认为是批改状态:
- 批改状态变量。
- 产生事件。
- 创立其它合约。
- 应用
selfdestruct
。 - 通过调用发送以太币。
- 调用任何没有标记为
view
或者pure
的函数。 - 应用低级调用。
- 应用蕴含特定操作码的内联汇编。
// SPDX-License-Identifier: GPL-3.0pragma solidity >=0.5.0 <0.9.0;contract C { function f(uint a, uint b) public view returns (uint) { return a * (b + 42) + block.timestamp; }}
Pure
函数能够申明为pure
函数,在这种状况下它们承诺不读取或批改状态。 特地是,应该能够在编译时仅给定输出和 msg.data
来评估pure
,而无需理解以后的区块链状态。 这意味着从不可变变量中读取能够是一个非纯操作。
除了下面解释的状态批改语句列表之外,以下被认为是读取状态:
- 读取状态变量。
- 拜访
address(this).balance
或者<address>.balance
。 - 拜访
block
,tx
,msg
中任意成员 (除msg.sig
和msg.data
之外)。 - 调用任何未标记为
pure
的函数。 - 应用蕴含某些操作码的内联汇编。
pragma solidity >=0.5.0 <0.9.0;contract C { function f(uint a, uint b) public pure returns (uint) { return a * (b + 42); }}
纯函数可能应用 revert()
和 require()
在 产生谬误 时去回滚潜在状态更改。
还原状态更改不被视为 “状态批改”, 因为它只还原以前在没有view
或 pure
限度的代码中所做的状态更改, 并且代码能够抉择捕捉 revert
并不传递还原。
这种行为也合乎 STATICCALL
操作码。
非凡的函数
receive
一个合约最多有一个 receive
函数, 申明函数为: receive() external payable { ... }
不须要 function
关键字,也没有参数和返回值并且必须是 external
可见性和 payable
润饰. 它能够是 virtual
的,能够被重载也能够有 修改器modifier 。
在对合约没有任何附加数据调用(通常是对合约转账)是会执行 receive
函数.例如 通过 .send()
or .transfer()
如果 receive
函数不存在,然而有payable 的 fallback 回退函数
那么在进行纯以太转账时,fallback 函数会调用.
如果两个函数都没有,这个合约就没法通过惯例的转账交易接管以太(会抛出异样).
更糟的是,receive
函数可能只有 2300 gas 能够应用(如,当应用 send
或 transfer
时), 除了根底的日志输入之外,进行其余操作的余地很小。上面的操作耗费会操作 2300 gas :
- 写入存储
- 创立合约
- 调用耗费大量 gas 的内部函数
- 发送以太币
<aside>
一个没有定义 fallback 函数或 receive 函数的合约,间接接管以太币(没有函数调用,即应用 send
或 transfer
)会抛出一个异样, 并返还以太币(在 Solidity v0.4.0 之前行为会有所不同)。 所以如果你想让你的合约接管以太币,必须实现receive函数(应用 payable fallback 函数不再举荐,因为payable fallback性能被调用,不会因为发送方的接口凌乱而失败)。
</aside>
<aside>
一个没有receive函数的合约,能够作为 coinbase 交易 (又名 矿工区块回报 )的接收者或者作为 selfdestruct
的指标来接管以太币。
一个合约不能对这种以太币转移做出反馈,因而也不能回绝它们。这是 EVM 在设计时就决定好的,而且 Solidity 无奈绕过这个问题。
这也意味着 address(this).balance
能够高于合约中实现的一些手工记帐的总和(例如在receive 函数中更新的累加器记帐)。
</aside>
上面你能够看到一个应用函数 receive 的 Sink 合约的例子。
// SPDX-License-Identifier: GPL-3.0pragma solidity >=0.6.0 <0.9.0;// This contract keeps all Ether sent to it with no way// to get it back.contract Sink { event Received(address, uint); receive() external payable { emit Received(msg.sender, msg.value); }}
Fallback
合约能够最多有一个回退函数。函数申明为: fallback () external [payable]
或 fallback (bytes calldata input) external [payable] returns (bytes memory output)
没有 function
关键字。 必须是 external
可见性,它能够是 virtual
的,能够被重载也能够有 修改器modifier 。
如果在一个对合约调用中,没有其余函数与给定的函数标识符匹配fallback会被调用. 或者在没有 receive 函数 时,而没有提供附加数据对合约调用,那么fallback 函数会被执行。
fallback 函数始终会接收数据,但为了同时接管以太时,必须标记为 payable
。
如果应用了带参数的版本, input
将蕴含发送到合约的残缺数据(等于 msg.data
),并且通过 output
返回数据。 返回数据不是 ABI 编码过的数据,相同,它返回不通过批改的数据。
更糟的是,如果回退函数在接管以太时调用,可能只有 2300 gas 能够应用,参考 receive接管函数
与任何其余函数一样,只有有足够的 gas 传递给它,回退函数就能够执行简单的操作
<aside>payable
的fallback函数也能够在纯以太转账的时候执行, 如果没有承受ether的函数,
举荐总是定义一个receive函数,而不是定义一个payable的fallback函数,
</aside>
函数重载
合约能够具备多个不同参数的同名函数,称为“重载”(overloading),这也实用于继承函数。以下示例展现了合约 A
中的重载函数 f
。
// SPDX-License-Identifier: GPL-3.0pragma solidity >=0.4.16 <0.9.0;contract A { function f(uint value) public pure returns (uint out) { out = value; } function f(uint value, bool really) public pure returns (uint out) { if (really) out = value; }}
重载函数也存在于内部接口中。 如果两个内部可见函数的 Solidity 类型不同而不是内部类型不同,这是一个谬误。
// SPDX-License-Identifier: GPL-3.0pragma solidity >=0.4.16 <0.9.0;// 以下代码无奈编译contract A { function f(B value) public pure returns (B out) { out = value; } function f(address value) public pure returns (address out) { out = value; }}contract B {}
以上两个 f
函数重载都承受了 ABI 的地址类型,尽管它们在 Solidity 中被认为是不同的。
重载解析和参数匹配
通过将以后范畴内的函数申明与函数调用中提供的参数相匹配,能够抉择重载函数。 如果所有参数都能够隐式地转换为预期类型,则选择函数作为重载候选项。如果一个候选都没有,解析失败。
// SPDX-License-Identifier: GPL-3.0pragma solidity >=0.4.16 <0.9.0;contract A { function f(uint8 val) public pure returns (uint8 out) { out = val; } function f(uint256 val) public pure returns (uint256 out) { out = val; }}
调用 f(50)
会导致类型谬误,因为 50
既能够被隐式转换为 uint8
也能够被隐式转换为 uint256
。 另一方面,调用 f(256)
则会解析为 f(uint256)
重载,因为 256
不能隐式转换为 uint8
。
本文由mdnice多平台公布