对于 public 状态变量会自动生成一个,与状态变量同名的 public
修饰的函数。 以便其他的合约读取他们的值。 当在用一个合约里使用是,外部方式访问 (如: this.x
) 会调用该自动生成的同名函数,而内部方式访问 (如: x
) 会直接从存储中获取值。 Setter函数则不会被生成,所以其他合约
状态变量可见性
在这之前的文章里,给出的例子中,声明的状态变量
都修饰为public
,因为我们将状态变量
声明为public
后,Solidity 编译器自动会为我们生成一个与状态变量
同名的、且函数可见性
为public
的函数!
在 Solidity 中,除了可以将状态变量
修饰为public
,还可以修饰为另外两种:internal
、private
。
public
对于 public 状态变量会自动生成一个,与状态变量同名的
public
修饰的函数。 以便其他的合约读取他们的值。 当在用一个合约里使用是,外部方式访问 (如:this.x
) 会调用该自动生成的同名函数,而内部方式访问 (如:x
) 会直接从存储中获取值。 Setter函数则不会被生成,所以其他合约不能直接修改其值。internal
内部可见性状态变量只能在它们所定义的合约和派生合同中访问, 它们不能被外部访问。 这是状态变量的默认可见性。
private
私有状态变量就像内部变量一样,但它们在派生合约中是不可见的。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract stateVarsVisible {
uint public num;
function showNum() public returns(uint){
num += 1;
return num;
}
}
contract outsideCall {
function myCall() public returns(uint){
//实例化合约
stateVarsVisible sv = new stateVarsVisible();
//调用 getter 函数
return sv.num();
}
}
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract stateVarsVisible {
uint internal num;
function showNum() public returns(uint){
num += 1;
return num;
}
function fn() external returns(uint){
return num;
}
}
contract sub is stateVarsVisible {
function myNum() public returns(uint){
return stateVarsVisible.num;
}
}
contract outsideCall {
function myCall() public returns(uint){
//实例化合约
stateVarsVisible sv = new stateVarsVisible();
//外部合约 不能访问
//return sv.num();
}
}
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract stateVarsVisible {
uint private num;
function showNum() public returns(uint){
num += 1;
return num;
}
function fn() external returns(uint){
return num;
}
}
contract sub is stateVarsVisible {
function myNum() public returns(uint){
//派生合约 无法访问 基合约 的状态变量
return stateVarsVisible.num;
}
}
函数可见性
前面的文章,我们多多少少有见到在函数参数列表后的一些关键字,那便是函数可见性
修饰符。对于函数可见性
这一概念,有过现代编程语言的经历大都知晓,诸如,public(公开的)
、private(私有的)
、protected(受保护的)
用来修饰函数的可见性,Java
、PHP`等便是使用这些关键字来修饰函数的可见性。
当然咯,Solidity 函数对外可访问也做了修饰,分为以下 4 种可见性:
- external
外部可见性函数作为合约接口的一部分,意味着我们可以从其他合约和交易中调用。 一个外部函数 f
不能从内部调用(即 f
不起作用,但 this.f()
可以)。
- public
public 函数是合约接口的一部分,可以在内部或通过消息调用。
- internal
内部可见性函数访问可以在当前合约或派生的合约访问,外部不可以访问。 由于它们没有通过合约的ABI向外部公开,它们可以接受内部可见性类型的参数:比如映射或存储引用。
- private
private 函数和状态变量仅在当前定义它们的合约中使用,并且不能被派生合约使用。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract FunctionVisible {
uint private num;
function privateFn(uint x) private returns(uint y){ y = x + 5; }
function setNum(uint x) public { num = x;}
function getNum() public returns(uint){ return num; }
function sum(uint x,uint y) internal returns(uint) { return x + y; }
function showPri(uint x) external returns(uint){ x += num; return privateFn(x); }
}
contract Outside {
function myCall() public {
FunctionVisible fv = new FunctionVisible();
uint res = fv.privateFn(7); // 错误:privateFn 函数是私有的
fv.setNum(4);
res = fv.getNum();
res = fv.sum(3,4); // 错误:sum 函数是 内部的
}
}
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract FunctionVisible {
uint private num;
function privateFn(uint x) private view returns(uint y){ y = x + num; }
function setNum(uint x) public { num = x;}
function getNum() public view returns(uint){ return num; }
function sum(uint x,uint y) internal pure returns(uint) { return x + y; }
function showPri(uint x) external view returns(uint){ x += num; return privateFn(x); }
}
contract Sub is FunctionVisible {
function myTest() public pure returns(uint) {
uint val = sum(3, 5); // 访问内部成员(从继承合约访问父合约成员)
val = privateFn(6); //privateFn函数是私有的,即便是派生合约也不能访问
return val;
}
}
getter 函数具有外部(external)可见性。如果在内部访问 getter(即没有 this.
),它被认为一个状态变量。 如果使用外部访问(即用 this.
),它被认作为一个函数。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract C {
uint public data;
function x() public returns(uint) {
data = 3; // 内部访问
uint val = this.data(); // 外部访问
return val;
}
}
如果你有一个数组类型的 public
状态变量,那么你只能通过生成的 getter 函数访问数组的单个元素。 这个机制以避免返回整个数组时的高成本gas。 可以使用如 myArray(0)
用于指定参数要返回的单个元素。 如果要在一次调用中返回整个数组,则需要写一个函数,例如:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract C {
uint[] public myArray;
// 自动生成的Getter 函数
/*
function myArray(uint i) public view returns (uint) {
return myArray[i];
}
*/
// 返回整个数组
function getArray() public view returns (uint[] memory) {
return myArray;
}
}
合约之外的函数
在 Solidity 0.7.0 版本之后,便可以将函数定义在合约之外,我们称这种函数为"自由函数"
,其函数可见性始终隐式地为internal
,它们的代码包含在所有调用它们的合约中,类似于后续会讲到的库
函数。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.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 {
//编译器会将 合约外函数的代码添加到这里
uint s = sum(arr);
require(s >= 10); //后续会讲到
found = true;
}
}
在合约之外定义的函数仍然在合约的上下文内执行。 它们仍然可以调用其他合约,将其发送以太币或销毁调用它们的合约等其他事情。 与在合约中定义的函数的主要区别为:自由函数不能直接访问存储变量 this
、存储和不在他们的作用域范围内函数。
函数参数与返回值
与其它编程语言一样,函数可能接受参数作为输入。但 Solidity 和 golang 一样,函数可以返回任意数量的值作为输出。
1、函数入参
函数的参数变量这一点倒是与声明变量是一样的,如果未能使用到的参数可以省略参数名。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract Simple {
uint sum;
function taker(uint a, uint b) public {
sum = a + b;
}
}
2、返回值
Solidity 函数返回值与 golang 函数返回很类似,只不过,Solidity 使用returns
关键字将返回参数声明在小括号内。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract Simple {
function arithmetic(uint a, uint b) public pure returns (uint sum, uint product)
{
sum = a + b;
product = a * b;
}
}
返回变量名可以被省略。 返回变量可以当作为函数中的局部变量,没有显式设置的话,会使用 :ref:默认值 <default-value>
返回变量可以显式给它附一个值(像上面),也可以使用 return
语句指定,使用 return
语句可以一个或多个值。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract Simple {
function arithmetic(uint a, uint b)
public
pure
returns (uint , uint )
{
return (a + b, a * b);
}
}
状态可变性
view
我们在先前的文章会看到,有些函数在修饰为public
后,有多了view
修饰的。而函数使用了view
修饰,说明这个函数不能修改状态变量(State Variable)
,只能获取状态变量的值,由于view
修饰的函数不能修改存储在区块链上的状态变量
,这种函数的gas fee
不会很高,毕竟调用函数也会消耗gas fee
。
而以下情况被认为是修改状态的:
- 修改状态变量。
- 产生事件。
- 创建其它合约。
- 使用
selfdestruct
。 - 通过调用发送以太币。
- 调用任何没有标记为
view
或者pure
的函数。 - 使用低级调用。
- 使用包含特定操作码的内联汇编。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract Simple {
function f(uint a, uint b) public view returns (uint) {
return a * (b + 42) + block.timestamp;
}
}
Solidity 0.5.0 移除了 view
的别名constant
。
Getter 方法自动被标记为 view
pure
若将函数声明为pure
的话,那么该函数是既不能读取也不能修改状态变量(State Variable)
。
除了上面解释的状态修改语句列表之外,以下被认为是读取状态:
- 读取状态变量。
- 访问
address(this).balance
或者<address>.balance
。 - 访问
block
,tx
,msg
中任意成员 (除msg.sig
和msg.data
之外)。 - 调用任何未标记为
pure
的函数。 - 使用包含某些操作码的内联汇编。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract Simple {
function f(uint a, uint b) public pure returns (uint) {
return a * (b + 42);
}
}
特别的函数
receive 接收以太函数
一个合约最多有一个 receive
函数, 声明函数为: receive() external payable { ... }
不需要 function
关键字,也没有参数和返回值并且必须是 external
可见性和 payable
修饰. 它可以是 virtual
的,可以被重载也可以有 后续会讲到的 修改器modifier 。
在对合约没有任何附加数据调用(通常是对合约转账)是会执行 receive
函数. 例如 通过 .send()
或 .transfer()
, 如果 receive
函数不存在, 但是有 payable 的 接下来会讲到的 fallback 回退函数
那么在进行纯以太转账时,fallback 函数
会调用.
如果两个函数都没有,这个合约就没法通过常规的转账交易接收以太(会抛出异常).
更糟的是,receive
函数可能只有 2300 gas 可以使用(如,当使用 send
或 transfer
时), 除了基础的日志输出之外,进行其他操作的余地很小。下面的操作消耗会操作 2300 gas :
- 写入存储
- 创建合约
- 调用消耗大量 gas 的外部函数
- 发送以太币
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract Simple {
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 可以使用。
与任何其他函数一样,只要有足够的 gas 传递给它,回退函数
就可以执行复杂的操作。
payable
的fallback函数
也可以在pure
·以太转账的时候执行, 如果没有 receive 以太函数 推荐总是定义一个receive函数,而不是定义一个payable
的fallback函数,
如果想要解码输入数据,那么前四个字节用作函数选择器,然后用 abi.decode
与数组切片语法一起使用来解码ABI编码的数据:
(c, d) = abi.decode(_input[4:], (uint256, uint256));
请注意,这仅应作为最后的手段,而应使用对应的函数。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract Test {
// 发送到这个合约的所有消息都会调用此函数(因为该合约没有其它函数)。
// 向这个合约发送以太币会导致异常,因为 fallback 函数没有 `payable` 修饰符
fallback() external { x = 1; }
uint x;
}
// 这个合约会保留所有发送给它的以太币,没有办法返还。
contract TestPayable {
uint x;
uint y;
// 除了纯转账外,所有的调用都会调用这个函数.
// (因为除了 receive 函数外,没有其他的函数).
// 任何对合约非空calldata 调用会执行回退函数(即使是调用函数附加以太).
fallback() external payable { x = 1; y = msg.value; }
// 纯转账调用这个函数,例如对每个空empty calldata的调用
receive() external payable { x = 2; y = msg.value; }
}
contract Caller {
function callTest(Test test) public returns (bool) {
(bool success,) = address(test).call(abi.encodeWithSignature("nonExistingFunction()"));
require(success);
// test.x 结果变成 == 1。
// address(test) 不允许直接调用 send , 因为 test 没有 payable 回退函数
// 转化为 address payable 类型 , 然后才可以调用 send
address payable testPayable = payable(address(test));
testPayable.transfer(2 ether);
// 以下将不会编译,但如果有人向该合约发送以太币,交易将失败并拒绝以太币。
// test.send(2 ether);
return true;
}
function callTestPayable(TestPayable test) public returns (bool) {
(bool success,) = address(test).call(abi.encodeWithSignature("nonExistingFunction()"));
require(success);
// 结果 test.x 为 1 test.y 为 0.
(success,) = address(test).call{value: 1}(abi.encodeWithSignature("nonExistingFunction()"));
require(success);
// 结果test.x 为1 而 test.y 为 1.
// 发送以太币, TestPayable 的 receive 函数被调用.
// 因为函数有存储写入, 会比简单的使用 send 或 transfer 消耗更多的 gas。
// 因此使用底层的call调用
(success,) = address(test).call{value: 2 ether}("");
require(success);
// 结果 test.x 为 2 而 test.y 为 2 ether.
return true;
}
}