以太坊智能合约开发(三):Solidity编程基础(二)
1 修饰符
1.1 修饰符
(1)internal修饰符 这样声明的函数和状态变量只能通过内部访问。如在当前合约中调用,或在继承的合约内调用。注意,不能加前缀this,前缀this是表示通过外部方式访问。 (2)external修饰符 外部函数是合约接口的一部分,可以从其他合约或通过交易来发起调用。 一个外部函数f不能通过内部的方式来发起调用,但是可以通过this.f()发起调用。 外部函数在接收大的数组数据时更加有效。 (3)public修饰符 公开函数是合约接口的一部分,可以通过内部消息和外部消息来进行调用。 对于public类型的状态变量,会自动创建一个访问器。 (4)private修饰符 私有函数和状态变量只能在当前合约中可以访问,在继承的合约内不可访问。私有函数不是合约接口的一部分。 上面的四种修饰符internal,external,public,private对函数或者控制变量进行修饰可以达到可见性控制的作用。 对函数和状态变量的可见性进行限定和约束能够有效划分函数的职能,提高合约的安全性。 (5)constant修饰符 带有constant修饰符的函数没有能力改变区块链上的状态变量,可以读取状态变量并返回给调用者。 (6)view修饰符 view声明的函数不能修改状态变量,等同于constant函数。 (7)pure修饰符 pure给函数的能力设置了很多限制,pure函数不能读写状态变量。pure声明的函数自身不能访问当前的状态和交易变量。 (8)payable修饰符 payable声明的函数可以从调用者那里接受ether。如果发送方没有提供ether,则调用可能会失败。一个函数如果声明为ether,它只能收取ether。
1.2 修饰符区别
external和public的区别 external和public基本一样,当一个合约中这两种函数随着合约被部署到链上,这个函数就可以被其他合约通过调用或者交易的方式调用。 两者最主要的区别是合约调用函数的方式和输入参数的传递方式。在合约中,从一个函数直接调用一个public函数,代码的执行会通过JUMP指令,像private和internal函数,而external函数必须通过call指令。另外external函数并不从只读的calldate里复制输入参数到内存或者栈上。 总结就是,public函数调用需要更多的gas,因为public函数不知道调用者是external还是internal,public函数会像internal函数那样将参数复制到memory(内存)。如果可以确信函数只能被外部调用,就使用external修饰符。 internal调用永远消耗燃料最少。
2 事件
事件是以太坊虚拟机(EVM)日志基础设施提供的一个便利接口,该机制可以使得开发者可以方便的使用EVM的日志系统(一种以太坊区块链中特殊的数据结构)。 开发者可以在Dapp的用户界面中监听事件,EVM的日志系统可以反过来“调用”,用来监听事件的JavaScript回调函数。事件在智能合约中可以被继承,当事件被调用时,会将参数存储到交易的日志中,这些日志与地址相关联,被存入区块链中,只要区块可以访问就一直存在。日志和事件在智能合约内不可直接被访问。
pragma solidity ^0.4.0;
contract SimpleAuction{
event aNewHigherBid(address bidder, uint amount);
function bid(uint bidValue)external {
aNewHigherBid(msg.sender, msg.value);
}
}
记录在区块链中的日志消息可以被外部检索到,最多可以有三个参数被设置为indexed,用来设置是否被索引。设置索引后,可以通过这个参数来查找日志,甚至可以通过特定的值来过滤。 事件和日志主要有三个用途:
- 智能合约返回值给用户接口:一个事件最简单的用法是从智能合约返回值给app的前端。
- 异步的带数据的触发器:如果一个合约需要触发前端,合约会发送一个事件。前端在监听事件时,就会采取情动,如显示一个消息等。
- 一种比较便宜的存储:花费的gas要远小于合约storage的花费。
3 继承
3.1 基类构造函数(单继承)
和其他面向编程的语言类似,solidity中的构造函数是由constructor关键字声明的可选函数,该函数在智能合约被创建时执行。构造函数可以是public的,也可以是internal的,如果没有定义构造函数,则编译器会生成默认的构造函数constructor()public{ }。
pragma solidity ^0.4.22;
contract A {
uint public a;
constructor(uint _a) internal {
a = _a;
}
}
contract B is A(1) {
constructor () public{ }
}
当一个构造函数被声明成internal时,该智能合约将被自动标记为抽象合约。
pragma solidity ^0.4.11;
contract A {
uint public a;
function A(uint _a) internal {
a = _a;
}
}
contract B is A(1) {
function B() public{ }
}
派生智能合约需要提供基类构造函数需要的所有参数,这可以通过两种方式来完成。
pragma solidity ^0.4.0;
contract Base {
uint a;
function Base(uint _a) public{
a = _a;
}
}
contract Derived1 is Base(7){
constructor (uint _y)public {}
}
contract Derived2 is Base{
constructor (uint _y)Base (_y * _y)public {}
}
从上面的例子可以看出,一种是直接在继承列表中调用基类构造函数(is Base(7)),适用于构造函数参数是常量的情况并且定义或描述了智能合约的行为,例如第一个实例中的常数为‘7’;另一种方法是像修饰器的使用方法一样,作为派生智能合约构造函数定义头的一部分(Base (_y * _y)),适用于构造的参数值由派生合约指定的情况即基类构造函数的参数依赖于派生智能合约的参数。(基类相当于父类,派生相当于子类)
3.2 多重继承
通过复制包括多态的代码,solidity可以支持多重继承。所有的函数调用都是虚拟的,这意味着最远的派生函数会被调用,除非给出明确的智能合约名称。当一个智能合约从多个智能合约继承时,在区块链上只有一个智能合约被创建,所有基类智能合约的代码被复制到创建的智能合约中。
3.3 线性化
编程语言实现多重继承需要解决几个问题,其中一个问题是钻石问题。solidity借鉴python的方式并且使用“C3线性化”强制一个由基类构成的DAG(有向无环图),保持一个特定的顺序。这种方法可以使我们得到所期望的唯一结果,但也使某些继承方式变得无效。**尤其需要注意的是,is在基类后面的顺序很重要。**下面的代码中,solidity会给出“Linearization of inheritance graph impossible ”(遗传图的线性化是不可能的)。
pragma solidity ^0.4.0;
contract X{}
contract A is X{}
contract C is A,X{}
4 函数
4.1 函数修饰器
函数修饰器的关键字是modifier,使用修饰器可以轻松改变函数的行为。 例如,他们可以在执行函数之前自动检查某个条件。修饰器是智能合约可继承属性,并且可以被派生智能合约所覆盖。
pragma solidity ^0.4.11;
contract owned {
address owner;
function owned () public {
owner = msg.sender;
}
modifier onlyOwner{
require (msg.sender == owner);
_;
}
}
contract mortal is owned{
function close()public onlyOwner {
selfdestruct(owner);
}
}
contract priced{
modifier costs(uint price){
if (msg.value >= price){
_;
}
}
}
contract Register is priced,owned{
mapping (address =>bool) registeredAddresses;
uint price;
function Register(uint initalPrice) public {
price = initalPrice;
}
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);
locked = true;
_;
locked = false;
}
function f() public noReentrancy returns (uint){
require (msg.sender.call());
return 7;
}
}
如果同一个函数有多个修改器,他们之间以空格隔开,修饰器会被依次检查执行。修饰器或函数体中显示的return语句仅仅跳出当前的修饰器和函数体。返回变量会被赋值,但整个执行逻辑会从前一个修饰器中定义的“_;”之后继续执行。 修饰器的参数可以是任意表达式,再次上下文中,所有在函数中可见的符号,在修饰器中均可见,但在修饰器中引入的符号在函数中不可见(可能被重载改变)。
4.2 getter函数
对于所有的public变量,solidity编译器提供了自动为状态变量生成对应的getter(访问器)的特性。 例如,**编译器会生成一个名为date的函数,该函数不会接收任何参数并返回一个uint,即状态变量date的值。**可以在声明时完成状态变量的初始化。
pragma solidity ^0.4.0;
contract C {
uint public date = 5;
}
contract Caller {
C c = new C();
function f() public {
uint local = c.date();
}
}
getter函数的可见性是external。如果从内部访问getter(即没有this.),它相当于一个状态状态变量。如果它是从外部访问的(即用this.),它被认为是一个函数。
pragma solidity ^0.4.0;
contract C {
uint public date;
function x() public {
date = 3;
uint val = this.date();
}
}
对于数组型的状态变量,只能通过编译器自动生成的getter函数访问数组中的单个元素。可以使用参数定制索要访问的单个元素位置,例如date(0)。这种机制存在的原因是为了防止当返回整个数组时gas的消耗过高。如果想要一次得到整个数组,那么需要编写一个函数,例如,
pragma solidity ^0.4.0;
contract arrayExample{
uint[] public myArray;
function myArray(uint i) returns (uint){
return myArray[i];
}
function getArray() returns (uint[] memory){
return myArray;
}
}
4.3 view函数和pure函数
在Solidity0.4.17版本以后,函数已经不能被声明为constant(状态常量),取而代之的是关键字view和pure。之前的版本,被标记为constant的函数不会改动智能合约的状态变量。也就是说调用constant函数不会发生写入操作,执行的结果也不会在网络中被验证,仅仅是读取区块链中的数据,因此不会花费gas。 view关键字的作用与constant作用完全一样,将函数声明为view类型,表明这种情况只能读取状态变量,但不能修改状态变量。同时也不能发送和接收以太币。调用其他函数时只能调用view函数和pure函数。 下面的语句被认为修改了状态:
- 修改状态变量。
- 产生事件。
- 使用selfdestruct。
- 通过调用发送以太币。
- 调用任何没有标记位view或者pure函数。
- 使用低级调用。
- 使用包含特定操作码的内联汇编。
**声明为pure的函数表示即不读取,也不修改状态变量。**调用其他函数时只能调用pure函数。 下面的语句被认为是从状态中读取的: - 读取状态变量。
- 访问this.balance或者< address >.balance.
- 访问block,msg,tx中的任意成员(除msg.sig和msg.date)。
- 调用任何未标记为pure的函数。使用包含某些操作码的内联汇编。
4.4 fallback函数
每个智能合约都可以有且仅有一个未命名的函数,称为fallback函数。这个函数不能有参数,也不能有返回值。 如果一个智能合约的调用中,没有其他函数与给定的函数标识符匹配(或没有提供调用数据),那么fallback函数就会被执行。 除此之外,每当智能合约收到以太币(没有任何数据)时,fallback函数也会被执行。为了接收以太币,fallback必须标记为payable。如果不存在这样的函数,则智能合约不能通过常规交易接收以太币。 虽然fallback函数不能有参数,但任然可以使用msg.date 来获取随调用提供的任何有效数据。注意,一个没有定义fallback函数的智能合约直接接收以太币(没有函数调用,即使用send和transfer)会抛出一个异常,并返还以太币。所以想让用户的智能合约接收以太币,就必须实现fallback函数。 实现fallback函数的例子如下:
pragma solidity ^0.4.0;
contract Test{
uint x ;
function() public {
x = 1;
}
}
contract Sink{
function()public payable{ }
}
contract Caller{
function CallTest(Test test) public{
test.call(0xabcdef01);
}
}
4.5 函数重载
智能合约可以具有多个不同参数的同名函数,这也适用于继承函数。
pragma solidity ^0.4.16;
contract A{
function f(uint _in) public pure returns(uint out){
out = 1;
}
function f(uint _in,bytes32 _key) public pure returns(uint out){
out = 2;
}
}
5 抽象智能合约
当一个智能合约中至少有一个函数缺省实现时,这个智能合约就可以当做抽象智能合约来使用。如下所示,函数的声明头由分号(;)结尾:
pragma solidity ^0.4.0;
contract X{
function utterance()public returns(bytes32);
}
抽象智能合约无法成功编译(即使它们除了未实现的函数,还包含其他已经实现了的函数),但它们可以用作基类智能合约。 如果一个智能合约继承自抽象智能合约,但没有通过重写来实现所有未实现的函数,那么它本身也是一个抽象智能合约。 需要注意的是,缺省实现的函数虽然跟function类型的变量在语法上看起来很像,但两者完全不同。例如,缺省实现的函数:function foo(address) external returns (address); ,function类型的变量在声明时:function (address) external returns(address) foo; 。 抽象智能合约将智能合约的定义部分与实现部分分离,从而提供了更好的可拓展性和易读性,减少代码冗余,并且使得智能合约的编写更加模板化。
6 接口
接口类似于抽象智能合约,但是接口内部不实现任何函数。除此之外还进一步限制:
- 无法继承其他智能合约和接口。
- 无法定义构造函数。
- 无法定义变量。
- 无法定义结构体。
- 无法定义枚举。
接口仅限于智能合约ABI可以表示的内容,并且ABI和接口之间的装换应该不会丢失任何信息。像继承其他智能合约一样,智能合约也可以继承接口。 接口由属于他们自己的关键字“interface”表示,例如
pragma solidity ^0.4.0;
interface Token{
function totalSupple() public view returns (uint256);
function balanceOf(address who) public view returns(uint256);
function transfer(address to,uint256 value)public returns (bool);
}
什么时候使用抽象智能合约,什么时候使用接口? 在实际编写DApp的过程中,如果想使用“模板方法”,那么最好使用抽象智能合约。这样能够更加便捷地搭建DApp的骨架,在快速实现原型的同时保持很高的拓展性。此外使用抽象智能合约有助于调试,在编译器发现DApp模式中的不一致的情况时,就会及时报告错误。当编写大规模DApp时,使用接口会更加有用。接口使得DApp易于扩展,而且不会增加复杂性。但由于接口的限制较多,在实际编程过程中还要注意是否能够使用接口。
7 库
库与智能合约类似,它们只需要在特定的地址部署一次,并且它们的代码可以通过EVM的DELEGATECALL特性进行重用。库可以看做是使用它们的智能合约的隐式的基类智能合约。对所有使用库的智能合约,库的internal函数都是可见的。 库和合约的区别在于库不能有Fallback函数以及payable关键字,也不能定义storage变量。但是库可以修改和他们相链接的合约的storage变量。另外库不能有日志(event),但是库可以分发事件。库函数触发的事件会被记录在调用合约的Event日志中。 下面的例子说明如何使用库:
pragma solidity ^0.4.16;
library Set{
struct Date {
mapping(uint => bool) flags;
}
function insert(Date storage self,uint value) public returns (bool){
if (self.flags[value])
return false;
self.flags[value] = true;
return true;
}
function remove(Date storage self,uint value) public returns (bool){
if (!self.flags[value])
return false;
self.flags[value] = false;
return true;
}
function contains(Date storage self,uint value) public view returns (bool){
return self.flags[value];
}
}
contract C{
Set.Date knowValues;
function register(uint value) public {
require(Set.insert(knowValues,value));
}
}
库也可以在不定义数据结构类型的情况下使用。函数也不需要任何存储引用参数,库可以出现在任何位置并且可以有多个存储引用参数。调用Set.contains,Set.insert,Set.remove都被编译为外部调用。如果使用库,请注意实际执行的是外部函数调用。msg.sender,msg.value,this调用中将保留他们的值。 下面的例子展示了如何在库中使用内存类型和内部函数来实现自定义类型,而无需支付外部函数调用的开销:
pragma solidity ^0.4.16;
library BigInt{
struct bigint{
uint[] limbs;
}
function fromUint(uint x) internal pure returns(bigint r){
r.limbs = new uint[](1);
r.limbs[0] = x;
}
function add(bigint _a,bigint _b) internal pure returns (bigint r){
r.limbs = new uint[] (max(_a.limbs.length, _b.limbs.length));
uint carry = 0;
for (uint i=0;i < r.limbs.length;++i){
uint a = limb(_a,i);
uint b = limb(_b,i);
r.limbs[i] = a+b+carry;
if (a+b<a||(a+b==uint(-1)&&carry>0))
carry = 1;
else
carry = 0;
}
if (carry > 0){
uint[] memory newLimbs = new uint[](r.limbs.length + 1);
for(i = 0;i < r.limbs.length;++i)
newLimbs[i] = r.limbs[i];
newLimbs[i] = carry;
r.limbs = newLimbs;
}
}
function max(uint a,uint b) private pure returns(uint){
return a > b ? a:b;
}
function limb(bigint _a,uint _limb) internal pure returns(uint){
return _limb < _a.limbs.length ? _a.limbs[_limb] : 0;
}
}
contract C{
using BigInt for BigInt.bigint;
function f() public pure{
var x = BigInt.fromUint(7);
var y = BigInt.fromUint(uint(-1));
var z = x.add(y);
}
}
由于来凝结器无法知道库的部署位置,我们需要通过连接器将这些地址填入最终的字节码中。相比智能合约,库在使用上还存在一些限制。
- 没有状态变量。
- 不能够继承或者被继承。
- 不能接收以太币。
8 using for的用法
指令“using A for B”可用于附加库函数(从库A)到任何类型B。这些函数接收到调用它们的对象作为它们的第一参数。
|