智能合约基础语言:Solidity函数 | 四
一、目录
▪ 函数的定义
▪ 函数的调用方式
▪ 函数的可见性
▪ 函数修改器
▪ pure函数
▪ constant、view函数
▪ payable函数
▪ 回退函数
▪ 构造函数
▪ 函数参数
▪ 抽象函数
▪ 数学和加密函数
二、函数的定义
function关键字声明的,合约中的可执行单元,一个函数的完整定义如下:
function (funcName) () {public|external|internal|private} [constant|view|payable] [returns (<return types>)]
三、函数的调用方式
Solidity封装了两种函数的调用方式internal(内部调用)和external(外部调用)。
3.1 internal(内部调用方式)
internal调用,实现时转为简单的EVM跳转,所以它能直接使用上下文环境中的数据,对于引用传递时将会变得非常高效(不用拷贝数据)。
在当前的代码单元内,如对合约内函数,引入的库函数,以及父类合约中的函数直接使用即是以internal方式的调用。我们来看个简单的例子:
pragma solidity ^0.4.24; contract Test {
function
f
(){}
//以internal的方式调用 function
callInternally
(){ f(); } }
在上述代码中,callInternally()以internal的方式对f()函数进行了调用。
简而言之,internal(内部调用方式)就是直接使用函数名去调用函数。
3.2 external(外部调用方式)
external调用,实现为合约的外部消息调用。所以在合约初始化时不能external的方式调用自身函数,因为合约还未初始化完成。下面来看一个以external方式调用的例子:
pragma solidity ^0.4.24; contract A{
function
f
(){} } contract B{
//以external的方式调用另一合约中的函数 function
callExternal
(A a){ a.f(); } }
虽然当前合约A和B的代码放在一起,但部署到网络上后,它们是两个完全独立的合约,它们之间的方法调用是通过消息调用。上述代码中,在合约B中的callExternal()以external的方式调用了合约A的f()。
简而言之,external(外部调用方式)就是使用合约实例名.函数名的方式去调用函数。
3.3 this
我们可以在合约的调用函数前加this.来强制以external方式的调用。
pragma solidity ^0.4.24; contract A{
function f
() external
{}
function callExternally
(){
this.f(); } }
3.4 调用方式说明
上面所提到的internal和external指的函数调用方式,请不要与后面的函数可见性声明的external,public,internal,private弄混。声明只是意味着这个函数需要使用相对应的调用方式去调用。
四、函数的可见性
Solidity为函数提供了四种可见性,external,public,internal,private。
4.1 external(外部函数)
▪ 声明为external的函数可以从其它合约来进行调用,所以声明为external的函数是合约对外接口的一部分。
▪ 不能以internal的方式进行调用。
▪ 有时在接收大的数据数组时性能更好。
pragma solidity ^0.4.24; contract FuntionTest{
function externalFunc
() external
{}
function callFunc
(){
//以`internal`的方式调用函数报错 //Error: Undeclared identifier. //externalFunc(); //以`external`的方式调用函数 this.externalFunc(); } }
声明为external的externalFunc()只能以external的方式进行调用,以internal的方式调用会报Error: Undeclared identifier。
4.2 pulic(公有函数)
▪ 函数默认声明为public。
▪ public的函数既允许以internal的方式调用,也允许以external的方式调用。
▪ public的函数由于允许被外部合约访问,是合约对外接口的一部分。
pragma solidity ^0.4.24; contract FuntionTest{
//默认是public函数 function publicFunc(){}
function callFunc(){
//以`internal`的方式调用函数 publicFunc();
//以`external`的方式调用函数 this.publicFunc(); } }
我们可以看到声明为public的publicFunc()允许两种调用方式。
4.3 internal(内部函数)
在当前的合约或继承的合约中,只允许以internal的方式调用。
pragma solidity ^0.4.24; contract A{
function internalFunc() internal{}
function callFunc(){
//以`internal`的方式调用函数 internalFunc(); } } contract B is A{
//子合约中调用 function callFunc(){ internalFunc(); } }
上述例子中声明为internal的internalFunc()在定义合约,和子合约中均只能以internal的方式可以进行调用。
4.4 private(私有函数)
▪ 只能在当前合约中被访问(不可在被继承的合约中访问)。
▪ 即使声明为private,仍能被所有人查看到里面的数据,但是不能修改数据且不能被其它合约访问。
pragma solidity ^0.4.24; contract A{
function privateFunc() private{}
function callFunc(){
//以`internal`的方式调用函数 privateFunc(); } } contract B is A{
//不可调用`private` function callFunc(){
//privateFunc(); //这里无法调用合约A中的内部函数, //且在编译阶段就会报错 }
//但是间接调用private函数,但是需要这个private函数处在public中 //function callPrivateByPublicFunc(){ // callFunc(); //}
}
五、pure(纯函数)
既不从状态读取数据也不写入数据的函数可以被声明为纯函数 除了之前修改状态数据的情况外,我们认为一下情况属于从状态读取数据。
1. 读取状态变量
2. 调用this.balance或者address.balance
3. 调用block、tx、msg的成员
4. 调用任何非纯函数
5. 使用了包含某些操作码的内联汇编
pragma solidity ^0.4.24; contract C {
function f(uint a, uint b) public pure returns (uint) {
return a * (b + 42); } }
六、constant/view(只读函数)
不改变状态的函数可以被声明为只读函数一下几种情况被视为修改了状态:
1. 修改状态变量
2. 触发事件
3. 创建了其他合约的实例
4. 使用了selfdestruct自我销毁
5. 调用了向合约转账的函数
6. 调用了非只读函数或者纯函数
7. 使用了底层调用
8. 使用了包含某些操作码的内联汇编
注意:
constant是view的一个别名,会在0.5.0版本中遗弃,访问器(getter)方法默认被标记为view调用只读函数。
七、函数修改器
在实际情况中,我们经常需要对调用者进行一些限制。比如,只能是合约的所有者才能改变归属。我们一起来看看如何用函数修改器实现这一限制:
pragma solidity ^0.4.24; contract Ownable { address public owner = msg.sender;
/// 限制只有创建者才能访问
modifier onlyOwner {
if (msg.sender != owner) throw;
_; }
/// 改变合约的所有者 function changeOwner(address _newOwner) onlyOwner {
if(_newOwner == 0x0) throw; owner = _newOwner; } }
7.1 函数修改器支持参数
pragma solidity ^0.4.24; contract Parameter{
uint balance = 10; modifier lowerLimit(uint _balance, uint _withdraw){
if( _withdraw < 0 || _withdraw > _balance) throw; _; }
//含参数的函数修改器 function f
(uint withdraw) lowerLimit
(balance, withdraw) returns
(uint){
return balance; } }
在上面的例子中,f()函数,有一个函数修改器lowerLimit(),传入了状态变量参数balance,和入参withdraw,以lowerLimit(balance, withdraw)的方式进行调用。最后函数能否正确执行取决于输入的withdraw值大小。
7.2 函数修改器参数支持表达式
pragma solidity ^0.4.24; contract ParameterExpression{ modifier m(uint a){
if(a > 0) _; }
function add
(uint a, uint b) private returns
(uint){
return a + b; }
function f
() m(add(1, 1)) returns
(uint){
return 1; } }
八、payable(接收以太币函数)
是声明了该函数涉及接收以太币操作,如果函数没有声明为payable,并且在调用过程中有以太币通过被调用的函数转入合约,那么EVM虚拟机将会抛出异常,状态回退。
pragma solidity ^0.4.24; contract AddressExample {
function AddressExample
() payable
{}
function giveEthersTo
(address _toAccount,uint amount){
if (this.balance >=amount){ _toAccount.transfer(amount); } }
function getBalance() view returns
(uint){
return this.balance; }
//function() payable{}
}
九、回退函数
每一个合约有且仅有一个没有名字的函数。这个函数无参数,也无返回值。如果调用合约时,没有匹配上任何一个函数(或者没有传哪怕一点数据),就会调用默认的回退函数。
此外,当合约收到ether时(没有任何其它数据),这个函数也会被执行。在此时,一般仅有少量的gas剩余,用于执行这个函数(准确的说,还剩2300gas)。所以应该尽量保证回退函数使用少的gas。
下述提供给回退函数可执行的操作会比常规的花费得多一点。
写入到存储(storage) 创建一个合约 执行一个外部(external)函数调用,会花费非常多的gas 发送ether 请在部署合约到网络前,保证透彻的测试你的回退函数,来保证函数执行的花费控制在2300gas以内。
一个没有定义一个回退函数的合约。如果接收ether,会触发异常,并返还ether(solidity v0.4.0开始)。所以合约要接收ether,必须实现回退函数。下面来看个例子。下面来看个例子:
pragma solidity ^0.4.24; contract Test {
function() public payable{}
function getX
() view returns
(uint){
return x; }
function getBalance() view returns
(uint){
return this.balance; } } contract Caller {
function Caller()payable
{}
function callTest(Test test) public{ test.call(0xabcdef01); // test.transfer(2 ether); }
function getBalance() view returns(uint){
return this.balance; } }
如果涉及支付以太币,即回退函数被声明为payable类型,并且通过send或者transfer被调用,那么回退函数仅有你2300gas可以使用,如果回退函数中的代码执行消耗超过2300gas那么被转入的以太币将会退回,修改过的数据状态回退。
以下操作会消耗超过2300gas:
1. 修改状态变量
2. 创建新的合约实例
3. 调用了会消耗gas较多的外部函数
4. 发送以太币
用做接收以太币回退函数内部仅能进行触发事件操作。
十、构造函数
构造函数是一个用constructor关键字声明的可选函数,它在创建合约时执行。构造函数可以是public,也可以是internal。如果没有构造函数,则该合约将生成默认构造函数:contructor() public {}。
pragma solidity ^0.4.24; contract A {
uint public a; constructor(uint _a) internal { a = _a; } }
在版本0.4.22之前,构造函数被定义为与合同名称相同的特殊函数,有且只能有一个,不允许重载。这个函数将在合约创建时,执行一次,用于初始化一些配置。这个语法现在不推荐使用。
pragma solidity ^0.4.24; contract ContractConstructor{
uint public counter;
function ContractConstructor(){ counter++; } }
上述合约在创建成功后,counter的值将为1。说明合约在创建时,被调用了一次。
十一、函数的输入参数与输出参数
Solidity函数的输入参数的数量是可选的,也可以有任意数量的返回参数。
入参(Input Parameter)与变量的定义方式一致,稍微不同的是,不会用到的参数可以省略变量名称。一种可接受两个整型参数的函数如下:
pragma solidity ^0.4.0; contract Simple {
function taker(uint _a, uint) { // do something with _a. } }
出参(Output Paramets)在returns关键字后定义,语法类似变量的定义方式。返回结果的数量需要与定义的一致。如果给定了参数名,则函数可以不适用return关键字返回,如果没有给定参数名则需要函数体重使用return关键字按照顺序返回。
pragma solidity ^0.4.24; contract Simple {
//return sum and product function arithmetics(uint _a, uint _b) returns (uint o_sum, uint o_product) { o_sum = _a + _b; o_product = _a * _b; } }
pragma solidity ^0.4.24; contract Simple {
//return sum and product function arithmetics(uint _a, uint _b) pure returns (uint , uint ) {
return(_a + _b,_a * _b); } }
十二、访问函数
编译器为自动为所有的public的状态变量创建访问函数。下面的合约例子中,编译器会生成一个名叫data的无参,返回值是uint的类型的值data。状态变量的初始化可以在定义时完成。
pragma solidity ^0.4.0;
contract C{
uint public c = 10; }
contract D{
C c = new C();
function getDataUsingAccessor() returns (uint){
return c.c(); } }
访问函数有外部(external)可见性。如果通过内部(internal)的方式访问,比如直接访问,你可以直接把它当一个变量进行使用,但如果使用外部(external)的方式来访问,如通过this.,那么它必须通过函数的方式来调用。
pragma solidity ^0.4.0; contract C{ uint public c = 10;
function accessInternal() returns
(uint){
return c; }
function accessExternal() returns (uint){
return this.c(); } }
十三、抽象函数
是没有函数体的的函数。如下:
pragma solidity ^0.4.0; contract Feline {
function utterance() returns (bytes32); }
这样的合约不能通过编译,即使合约内也包含一些正常的函数。但它们可以做为基合约被继承。
pragma solidity ^0.4.0; contract Feline {
function utterance() returns (bytes32);
function getContractName() returns (string){
return "Feline"; } } contract Cat is Feline {
function utterance() returns (bytes32) { return "miaow"; } }
如果一个合约从一个抽象合约里继承,但却没实现所有函数,那么它也是一个抽象合约。
十四、数字和加密函数
以下函数式solidity自带的函数
asser(bool condition):
如果条件不满足,抛出异常。
addmod(uint x, uint y, uint k) returns (uint):
计算(x + y) % k。加法支持任意的精度。但不超过(wrap around?)2**256。
mulmod(uint x, uint y, uint k) returns (uint):
计算(x * y) % k。乘法支持任意精度,但不超过(wrap around?)2**256。
keccak256(...) returns (bytes32):
使用以太坊的(Keccak-256)计算HASH值。紧密打包。
sha3(...) returns (bytes32):
等同于keccak256()。紧密打包。
sha256(...) returns (bytes32):
使用SHA-256计算HASH值。紧密打包。
ripemd160(...) returns (bytes20):
使用RIPEMD-160计算HASH值。紧密打包。
ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address):
通过签名信息恢复非对称加密算法公匙地址。如果出错会返回0,例如:
function verify(bytes32 hash, uint8 v, bytes32 r, bytes32 s) constant returns(bool) {
bytes memory prefix = "/x19Ethereum Signed Message:/n32"; bytes32 prefixedHash = keccak256(prefix, hash); return ecrecover(prefixedHash, v, r, s) == (Your Address);
}
revert():
取消执行,并回撤状态变化。
需要注意的是参数是“紧密打包(tightly packed)”的,意思是说参数不会补位,就直接连接在一起的。下面来看一个例子:
keccak256
("ab", "c")
keccak256
("abc")
//hex
keccak256
(0x616263)
keccak256
(6382179)
//ascii
keccak256
(97, 98, 99)
上述例子中,三种表达方式都是一致的。
如果需要补位,需要明确的类型转换,如keccak256("/x00/x12")等同于keccak256(uint16(0x12))
需要注意的是字面量会用,尽可能小的空间来存储它们。比如,keccak256(0) == keccak256(uint8(0)),keccak256(0x12345678) == keccak256(uint32(0x12345678))
注意:
在私链(private blockchain)上运行sha256,ripemd160或ecrecover可能会出现Out-Of-Gas报错。因为它们实现了一种预编译的机制,但合约要在收到第一个消息后才会存在。向一个不存在的合约发送消息,非常昂贵,所以才会导致Out-Of-Gas的问题。一种解决办法是每个在你真正使用它们前,先发送1 wei到这些合约上来完成初始化。在官方和测试链上没有这个问题。
文章声明:本文为火星财经专栏作者作品,不代表火星财经观点,版权归作者所有,如需转载,请提前联系作者或注明出处。