1.概念
在Solidity中,函数是智能合约的基本构建块,用于实现特定的业务逻辑。以下是Solidity函数的一些关键特性和详细解释:
函数定义;
函数由 function 关键字开始,后跟函数的名称、参数列表和返回值。函数可以是内部的(internal)或外部的(external),状态变更的(state-changing)或只读的(read-only)。
function functionName(parameterType param1, parameterType param2) public returns (returnType return1, returnType return2) {
// 函数体
}
函数类型
• 内部函数(Internal functions): 只能在当前合约或继承它的合约中调用。
• 外部函数(External functions): 可以被其他合约或事务调用。
内部函数:
内部函数只能在定义它们的合约内部以及继承它们的合约内部被调用。它们不能被外部合约直接调用。内部函数通常用于合约内部逻辑的实现,这些逻辑不希望被外部合约直接访问。
以下是内部函数的一些特点:
定义方式:默认情况下,如果没有指定可见性,函数会被视为内部函数。也可以显式地使用internal关键字来定义。
调用方式:可以直接通过函数名调用,不需要使用this关键字。
- 性能:内部函数的调用比外部函数更高效,因为它们不会进行跨合约调用。
- 用途:通常用于实现合约的辅助功能,如计算逻辑、数据验证等。
pragma solidity ^0.8.0;
contract InternalExample {
// 内部函数,默认为internal
function helper() internal pure returns (uint) {
return 42;
}
function doSomething() public {
// 直接调用内部函数
uint result = helper();
// 使用result进行其他操作
}
}
contract InheritedContract is InternalExample {
function callHelper() public pure returns (uint) {
// 继承的合约可以调用内部函数
return helper();
}
}
在上面的例子中,helper函数是内部函数,它只能在InternalExample合约内部或继承它的合约(如InheritedContract)内部被调用。
外部函数:
外部函数可以从其他合约或通过事务调用。它们通常用于合约之间的交互。
以下是外部函数的一些特点:
- 定义方式:使用external关键字来定义。
- 调用方式:从合约内部调用时,必须使用this关键字或者直接作为交易的一部分调用。从合约外部调用时,直接使用合约地址和函数签名。
- 性能:外部函数调用可能会更昂贵,因为它们可能涉及到跨合约调用。
- 用途:用于合约之间的通信和交互,例如,调用其他合约的函数。
pragma solidity ^0.8.0;
contract ExternalExample {
// 外部函数
function externalFunction() external pure returns (uint) {
return 42;
}
}
contract CallerContract {
ExternalExample externalExample;
constructor(address _externalExampleAddress) {
externalExample = ExternalExample(_externalExampleAddress);
}
function callExternalFunction() public view returns (uint) {
// 从合约内部调用外部函数
return externalExample.externalFunction();
}
}
在上面的例子中,externalFunction是一个外部函数,它可以在其他合约(如CallerContract)中被调用。在CallerContract中,我们首先创建一个ExternalExample合约的实例,然后通过这个实例调用externalFunction。
2.状态可见性
函数可以修改合约的状态,这通过以下关键字指定:
- view:函数不会修改状态
- pure:函数即不读取也不修改状态
- payable:函数可以接受以太币
2.1 view
view修饰符用于声明一个函数为“视图函数”。视图函数不会改变合约的状态,即它们不会修改任何状态变量,也不会发送以太币。它们仅用于检索数据。
view
函数不会消耗任何Gas(燃料)并且不会修改区块链状态
以下是一些关于view修饰符的要点:
- 视图函数可以读取合约的状态变量。
- 视图函数不能修改状态变量。
- 视图函数不能触发事件。
- 视图函数不能调用任何非视图或非纯函数。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SimpleStorage {
uint public x;
function getX() public view returns (uint){
return x;
}
}
它们只能调用其他view
或pure
函数,而不能调用普通函数。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SimpleStorage {
//状态变量
uint public storedData;
//构造函数,在合约创建的时候执行
constructor(uint initialValue){
storedData=initialValue;
}
//view函数,读取存储到数据
function retrieve() public view returns (uint){
return storedData;
}
//普通函数,修改存储到数据
function update(uint x)public {
storedData = x;
}
//另一个view函数,执行计算但是不修改状态
function calculateSquare() public view returns (uint){
return storedData *storedData;
}
}
注意,尽管view函数不会修改区块链的状态,但它们仍然需要执行计算,并且调用它们会消耗一定的Gas。不过,这个Gas成本比执行会改变状态的函数要低得多。此外,view函数不能直接调用非view或非pure函数,因为非view/pure函数可能会修改状态。
- 因为读取区块链的信息消耗了计算量和gas。
- 调用view函数是免费的,除非你在消耗gas的函数中调用它。
- 我们可以编译,删除,部署,调用store,输入678,执行,查看它的消耗,发现加了retrieve()这行代码后,store函数的gas消耗更多了。
2.2 pure
在Solidity中,`pure`函数是一种特殊的函数修饰符,它用于表明该函数不会读取或修改区块链的状态。这意味着`pure`函数在执行时不会对任何状态变量进行读取或写入操作,也不会产生任何外部可观察的效果,如发起交易或调用其他合约。
以下是关于`pure`函数的详细解释:
1. 纯函数的特点
- 不读取状态变量:函数体内不能包含对任何状态变量的读取操作。
- 不修改状态变量:函数体内不能包含对任何状态变量的写入操作。
- 不访问区块链数据:函数不能访问区块链的任何信息,如区块哈希、时间戳、交易发送者等。
不触发事件:函数不能触发事件,因为事件是写入区块链日志的一种方式。
不调用非`pure`函数:`pure`函数不能调用任何非`pure`函数,因为非`pure`函数可能会读取或修改状态。
pragma solidity ^0.8.0;
contract Calculator {
// 这是一个pure函数,用于计算两个整数的和
function add(uint x, uint y) public pure returns (uint) {
return x + y;
}
// 这是一个pure函数,用于返回一个常量值
function getMagicNumber() public pure returns (uint) {
return 42;
}
}
看图,状态为未读取
使用`pure`修饰符可以提高函数执行的效率,因为不需要读取状态,所以执行时不会消耗gas去访问存储。
如果一个函数实际上不满足`pure`的条件(例如,它读取了状态变量),但在声明时错误地使用了`pure`修饰符,那么在编译时或运行时可能会出现错误。
举个例子:
示例:错误使用 pure 修饰符
假设我们有一个智能合约,其中包含一个状态变量,并且我们试图创建一个计算这个状态变量值的函数。如果这个函数被错误地声明为pure,虽然它实际上读取了状态变量,那么将会出现问题
pragma solidity ^0.8.0;
contract IncorrectPureUsage {
uint public myNumber = 10; // 状态变量
// 错误地声明为pure
function readNumber() public pure returns (uint) {
return myNumber; // 这里读取了状态变量
}
}
在这个例子中,readNumber函数尝试读取状态变量myNumber的值并返回它。尽管这个函数没有修改任何状态,它读取了状态,因此不应该被声明为pure。正确的做法是将其声明为view,因为view函数允许读取状态但不修改状态。
如何修复
为了修复这个问题,你应该将pure修饰符改为view,因为函数确实需要读取状态变量:
pragma solidity ^0.8.0;
contract CorrectUsage {
uint public myNumber = 10; // 状态变量
// 正确地声明为view
function readNumber() public view returns (uint) {
return myNumber; // 这里读取了状态变量
}
在早期版本的Solidity中(如0.4.x),与`pure`类似的概念是`constant`,但在0.5.0版本之后,`constant`被弃用,并推荐使用`view`和`pure`。
总的来说,`pure`函数是Solidity中一种非常有用的工具,它用于声明完全独立于智能合约状态的函数,有助于确保函数的确定性和安全性。
3.可见度标识符
函数和变量有四种可见度标识符public、private、internal和external。
- public:public 是最高级别的可见度标识符,表示变量、函数或合约对内外部都可见。公共状态变量可以被任何人读取,并且公共函数可以被外部调用者调用。公共函数和状态变量的访问可以通过合约地址直接进行。
- private:private 是最低级别的可见度标识符,表示只有当前合约内的其他函数才能访问该变量或函数。私有状态变量只能在当前合约中访问,私有函数只能被当前合约的其他函数调用。私有状态变量和函数对外部调用者是不可见的。
- internal:internal 表示内部可见性,表示只有当前合约及其派生合约内的其他函数才能访问该变量或函数。内部状态变量和函数可以在当前合约及其派生合约中访问,但对外部调用者不可见。
- external:external 可以用于函数,表示该函数只能通过外部消息调用。外部函数只能被其他合约调用,而不能在当前合约内部直接调用。此外,外部函数不能访问合约的状态变量,只能通过参数和返回值进行数据交互。
- 如果没有显式指定变量的访问修饰符,则默认为internal,而internal关键字表示,它只对本合约和继承合约可见。
现在这个favoriteNumber变量被设置为了internal,所以我们看不到它。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;
contract SimpleStorage {
// This gets initialized to zero!
// <- This means that this section is a comment!
uint256 public favoriteNumber;
function store(uint256 _favoriteNumber) public {
favoriteNumber = _favoriteNumber;
}
}
然后进入部署区域,点击下面的X,把旧的合约删掉。当然只是从这个窗口删掉,但是没有从区块链删掉,因为区块链是不可更改的。当然因为这个是测试环境,所以只是某种程度上不可更改。编译,然后重新部署。
然后发现在新的合约中有两个按钮。
其中一个橘黄色按钮是函数,还有一个favoriteNumber的按钮,这个按钮代表favoriteNumber变量,就像一个显示变量值的函数(就比如Java里面的getter函数),实际上确实就是一个getter函数,此章节后面将会详细说明。
如果现在点击这个按钮,会显示什么?因为favoriteNumber初始化的默认值是0,点击一下,我们可以看到显示的是0。现在就是说这个uint256数据类型存储的数值是0。如果现在通过store函数把变量改为678,再点击favoriteNumber按钮,可以看到数值更新为678。
public关键字
当你在Solidity中使用public关键字修饰一个状态变量时,Solidity编译器会自动生成一个类似getter函数的方法来允许外部调用者读取该变量的值。
这个自动生成的getter函数会使用与状态变量同名的函数名,并且没有参数。它的返回值类型与状态变量的类型相匹配。通过调用这个getter函数,外部调用者可以获取到状态变量的当前值。
例如,当你声明一个public的状态变量uint256 public myNumber时,Solidity编译器将会自动生成一个名为myNumber()的函数,用于获取myNumber的值。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;
contract MyContract {
uint256 public myNumber;
}
外部调用者可以通过合约地址调用这个自动生成的getter函数来获取myNumber的值。
需要注意的是,public关键字只会生成一个getter函数用于读取状态变量,而不能直接修改状态变量的值。如果你希望外部调用者能够修改状态变量的值,你可以使用setter函数或将状态变量声明为可写入的。
4.作用域
在 Solidity 中,变量和函数可以拥有不同的作用域,作用域决定了这些变量和函数的可见性和访问权限。
以下是 Solidity 中常见的作用域:
- 全局作用域:在合约的整个范围内可见的变量和函数属于全局作用域。这些变量和函数可以被合约内的任何地方访问。
- 合约作用域:在合约内部声明的变量和函数具有合约作用域,它们只能在声明它们的合约内部访问。
- 函数作用域:在函数内部声明的变量具有函数作用域,只能在该函数内部访问。这些变量通常被称为局部变量。
- 事件作用域:在 Solidity 中,事件也有其特定的作用域。事件在声明它们的合约内部可见,可以被合约内的任何函数调用来触发。
4.1 全局作用域
全局变量和函数是在整个合约都是可见的
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract GlobalScopeExample {
uint256 public globalVariable; // 全局变量
// 全局函数
function globalFunction() public view returns (uint256) {
return globalVariable;
}
function updateGlobalVariable(uint256 _value) public {
globalVariable = _value;
}
}
4.2 合约作用域
合约作用域的变量和函数只能在声明它们的合约内部访问
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract ContractScopeExample {
uint256 public contractVariable; // 合约变量
// 合约函数
function contractFunction() public view returns (uint256) {
return contractVariable;
}
// 只能在合约内部访问
function updateContractVariable(uint256 _value) public {
contractVariable = _value;
}
}
剩下的例子跟其他语言的用法是差不多的,就不一一举例了。
5.gas的消耗
每次调用这个store函数,我们都会发送一个交易。因为每次在更改区块链状态的时候,我们都会发送交易,可以在右下角Remix的日志区域,查看交易细节。
你可以看到交易消耗了多少gas,可以看到这里的数字比发送交易所用到的21000 gas要多的,那是因为这里的操作会消耗更多的计算量。
实际上我们在这里存储了一个数字,现在如果我们在函数中做更多的操作会发生什么?除了在这里存储数据外,我们在存储变量后更新这个变量,让favoriteNumber加1。因为我们加了这样一个操作,这个函数将消耗更多的计算量。
然后我们现在重新编译删掉旧合约,然后重新部署,然后再次输入678,然后查看交易细节,我们可以发现执行交易的消耗的gas变得更多了。那是因为我们做的事情变多了,这个函数消耗的计算量变多了。
43730 gas -> 44127 gas
每个区块链计算gas的方式不同,但最简单的理解是,做越多的操作,消耗更多的gas。