solidity语法4——合约(类似面向对象中的Class)
这部分主要介绍合约的完整写法和用法。前面1,2,3运用在4中。
Solidity 合约类似于面向对象语言中的类。合约中有用于数据持久化的状态变量,和可以修改状态变量的函数。
调用另一个合约实例的函数时,会执行一个 EVM 函数调用,这个操作会切换执行时的上下文,这样,前一个合约的状态变量就不能访问了。
创建合约
可以通过以太坊交易“从外部”或从 Solidity 合约内部创建合约。
一些集成开发环境,例如 Remix, 通过使用一些用户界面元素使创建过程更加流畅。 在以太坊上编程创建合约最好使用 JavaScript API web3.js。 现在,我们已经有了一个叫做 web3.eth.Contract 的方法能够更容易的创建合约。
创建合约时,会执行一次构造函数(与合约同名的函数)。构造函数是可选的。只允许有一个构造函数,这意味着不支持重载。
在内部,构造函数参数在合约代码之后通过 ABI 编码 传递,但是如果你使用 web3.js
则不必关心这个问题。
如果一个合约想要创建另一个合约,那么创建者必须知晓被创建合约的源代码(和二进制代码)。 这意味着不可能循环创建依赖项。
可见性和 getter 函数
由于 Solidity 有两种函数调用(内部调用不会产生实际的 EVM 调用或称为“消息调用”,而外部调用则会产生一个 EVM 调用), 函数和状态变量有四种可见性类型。
函数可以指定为 external
,public
,internal
或者 private
,默认情况下函数类型为 public
。
external
,默认是 internal
。
external
:- 外部函数作为合约接口的一部分,意味着我们可以从其他合约和交易中调用。 一个外部函数
f
不能从内部调用(即f
不起作用,但this.f()
可以)。 当收到大量数据的时候,外部函数有时候会更有效率。 public
:- public 函数是合约接口的一部分,可以在内部或通过消息调用。对于公共状态变量, 会自动生成一个 getter 函数(见下面)。
internal
:// 相当于文件闭包里的变量,多个类都可以访问- 这些函数和状态变量只能是内部访问(即从当前合约内部或从它派生的合约访问),不使用
this
调用。 private
: // 类的私有成员变量- private 函数和状态变量仅在当前定义它们的合约中使用,并且不能被派生合约使用。
private
类型只能阻止其他合约访问和修改这些信息, 但是对于区块链外的整个世界它仍然是可见的。在下面的例子中,D
可以调用 c.getData()
来获取状态存储中 data
的值,但不能调用 f
。 合约 E
继承自 C
,因此可以调用 compute
。
// 下面代码编译错误
pragma solidity ^0.4.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.0;
contract C {
uint public data = 42;
}
contract Caller {
C c = new C();
function f() public {
uint local = c.data();
}
}
getter 函数具有外部可见性。如果在内部访问 getter(即没有 this.
),它被认为一个状态变量。 如果它是外部访问的(即用 this.
),它被认为为一个函数。
pragma solidity ^0.4.0;
contract C {
uint public data;
function x() public {
data = 3; // 内部访问
uint val = this.data(); // 外部访问
}
}
下一个例子稍微复杂一些:
pragma solidity ^0.4.0;
contract Complex {
struct Data {
uint a;
bytes3 b;
mapping (uint => uint) map;
}
mapping (uint => mapping(bool => Data[])) public data;
}
这将会生成以下形式的函数
function data(uint arg1, bool arg2, uint arg3) public returns (uint a, bytes3 b) {
a = data[arg1][arg2][arg3].a;
b = data[arg1][arg2][arg3].b;
}
请注意,因为没有好的方法来提供映射的键,所以结构中的映射被省略。
函数 修饰器modifier
使用 修饰器modifier 可以轻松改变函数的行为。 例如,它们可以在执行函数之前自动检查某个条件。 修饰器modifier 是合约的可继承属性, 并可能被派生合约覆盖。
如果同一个函数有多个 修饰器modifier,它们之间以空格隔开,修饰器modifier 会依次检查执行。
修饰器modifier 或函数体中显式的 return 语句仅仅跳出当前的 修饰器modifier 和函数体。 返回变量会被赋值,但整个执行逻辑会从前一个 修饰器modifier 中的定义的 “_” 之后继续执行。
修饰器modifier 的参数可以是任意表达式,在此上下文中,所有在函数中可见的符号,在 修饰器modifier 中均可见。 在 修饰器modifier 中引入的符号在函数中不可见(可能被重载改变)。
Fallback 函数
合约可以有一个未命名的函数。这个函数不能有参数也不能有返回值。 如果在一个到合约的调用中,没有其他函数与给定的函数标识符匹配(或没有提供调用数据),那么这个函数(fallback 函数)会被执行。
除此之外,每当合约收到以太币(没有任何数据),这个函数就会执行。此外,为了接收以太币,fallback 函数必须标记为 payable
。 如果不存在这样的函数,则合约不能通过常规交易接收以太币。
在这样的上下文中,通常只有很少的 gas 可以用来完成这个函数调用(准确地说,是 2300 gas),所以使 fallback 函数的调用尽量廉价很重要。 请注意,调用 fallback 函数的交易(而不是内部调用)所需的 gas 要高得多,因为每次交易都会额外收取 21000 gas 或更多的费用,用于签名检查等操作。
具体来说,以下操作会消耗比 fallback 函数更多的 gas:
- 写入存储
- 创建合约
- 调用消耗大量 gas 的外部函数
- 发送以太币
请确保您在部署合约之前彻底测试您的 fallback 函数,以确保执行成本低于 2300 个 gas。
注解
即使 fallback 函数不能有参数,仍然可以使用 msg.data
来获取随调用提供的任何有效数据。
警告
一个没有定义 fallback 函数的合约,直接接收以太币(没有函数调用,即使用 send
或 transfer
)会抛出一个异常, 并返还以太币(在 Solidity v0.4.0 之前行为会有所不同)。所以如果你想让你的合约接收以太币,必须实现 fallback 函数。
警告
一个没有 payable fallback 函数的合约,可以作为 coinbase transaction (又名 miner block reward )的接收者或者作为 selfdestruct
的目标来接收以太币。
一个合约不能对这种以太币转移做出反应,因此也不能拒绝它们。这是 EVM 在设计时就决定好的,而且 Solidity 无法绕过这个问题。
这也意味着 this.balance
可以高于合约中实现的一些手工记帐的总和(即在 fallback 函数中更新的累加器)。
pragma solidity ^0.4.0;
contract Test {
// 发送到这个合约的所有消息都会调用此函数(因为该合约没有其它函数)。
// 向这个合约发送以太币会导致异常,因为 fallback 函数没有 `payable` 修饰符
function() public { x = 1; }
uint x;
}
// 这个合约会保留所有发送给它的以太币,没有办法返还。
contract Sink {
function() public payable { }
}
contract Caller {
function callTest(Test test) public {
test.call(0xabcdef01); // 不存在的哈希
// 导致 test.x 变成 == 1。
// 以下将不会编译,但如果有人向该合约发送以太币,交易将失败并拒绝以太币。
// test.send(2 ether);
}
}
函数重载
合约可以具有多个不同参数的同名函数。这也适用于继承函数。
重载函数也存在于外部接口中。如果两个外部可见函数仅区别于 Solidity 内的类型而不是它们的外部类型则会导致错误。
事件
事件允许我们方便地使用 EVM 的日志基础设施。 我们可以在 dapp 的用户界面中监听事件,EVM 的日志机制可以反过来“调用”用来监听事件的 Javascript 回调函数。
事件在合约中可被继承。当他们被调用时,会使参数被存储到交易的日志中 —— 一种区块链中的特殊数据结构。 这些日志与地址相关联,被并入区块链中,只要区块可以访问就一直存在(在 Frontier 和 Homestead 版本中会被永久保存,在 Serenity 版本中可能会改动)。 日志和事件在合约内不可直接被访问(甚至是创建日志的合约也不能访问)。
对日志的 SPV(Simplified Payment Verification)证明是可能的,如果一个外部实体提供了一个带有这种证明的合约,它可以检查日志是否真实存在于区块链中。 但需要留意的是,由于合约中仅能访问最近的 256 个区块哈希,所以还需要提供区块头信息。
最多三个参数可以接收 indexed
属性,从而使它们可以被搜索:在用户界面上可以使用 indexed 参数的特定值来进行过滤。
如果数组(包括 string
和 bytes
)类型被标记为索引项,则它们的 keccak-256 哈希值会被作为 topic 保存。
除非你用 anonymous
说明符声明事件,否则事件签名的哈希值是 topic 之一。 同时也意味着对于匿名事件无法通过名字来过滤。
所有非索引参数都将存储在日志的数据部分中。
pragma solidity ^0.4.0;
contract ClientReceipt {
event Deposit(
address indexed _from,
bytes32 indexed _id,
uint _value
);
function deposit(bytes32 _id) public payable {
// 我们可以过滤对 `Deposit` 的调用,从而用 Javascript API 来查明对这个函数的任何调用(甚至是深度嵌套调用)。
Deposit(msg.sender, _id, msg.value);
}
}
使用 JavaScript API 调用事件的用法如下:
var abi = /* abi 由编译器产生 */;
var ClientReceipt = web3.eth.contract(abi);
var clientReceipt = ClientReceipt.at("0x1234...ab67" /* 地址 */);
var event = clientReceipt.Deposit();
// 监视变化
event.watch(function(error, result){
// 结果包括对 `Deposit` 的调用参数在内的各种信息。
if (!error)
console.log(result);
});
// 或者通过回调立即开始观察
var event = clientReceipt.Deposit(function(error, result) {
if (!error)
console.log(result);
});
日志的底层接口
通过函数 log0
,log1
, log2
, log3
和 log4
可以访问日志机制的底层接口。 logi
接受 i + 1
个 bytes32
类型的参数。其中第一个参数会被用来做为日志的数据部分, 其它的会做为 topic。上面的事件调用可以以相同的方式执行。
pragma solidity ^0.4.10;
contract C {
function f() public payable {
bytes32 _id = 0x420042;
log3(
bytes32(msg.value),
bytes32(0x50cb9fe53daa9737b786ab3646f04d0150dc50ef4e75f59509d83667ad5adb20),
bytes32(msg.sender),
_id
);
}
}
其中的长十六进制数的计算方法是 keccak256("Deposit(address,hash256,uint256)")
,即事件的签名。
继承
通过复制包括多态的代码,Solidity 支持多重继承。
所有的函数调用都是虚拟的,这意味着最远的派生函数会被调用,除非明确给出合约名称。
当一个合约从多个合约继承时,在区块链上只有一个合约被创建,所有基类合约的代码被复制到创建的合约中。
总的来说,Solidity 的继承系统与 Python的继承系统 ,非常 相似,特别是多重继承方面。
下面的例子进行了详细的说明。
调用 Final.kill()
时会调用最远的派生重载函数 Base2.kill
,但是会绕过 Base1.kill
, 主要是因为它甚至都不知道 Base1
的存在。解决这个问题的方法是使用 super
:
如果 Base2
调用 super
的函数,它不会简单在其基类合约上调用该函数。 相反,它在最终的继承关系图谱的下一个基类合约中调用这个函数,所以它会调用 Base1.kill()
(注意最终的继承序列是——从最远派生合约开始:Final, Base2, Base1, mortal, ownerd)。 在类中使用 super 调用的实际函数在当前类的上下文中是未知的,尽管它的类型是已知的。 这与普通的虚拟方法查找类似。
基类构造函数的参数
派生合约需要提供基类构造函数需要的所有参数。这可以通过两种方式来完成:
pragma solidity ^0.4.0;
contract Base {
uint x;
function Base(uint _x) public { x = _x; }
}
contract Derived is Base(7) {
function Derived(uint _y) Base(_y * _y) public {
}
}
一种方法直接在继承列表中调用基类构造函数(is Base(7)
)。 另一种方法是像 修饰器modifier 使用方法一样, 作为派生合约构造函数定义头的一部分,(Base(_y * _y)
)。 如果构造函数参数是常量并且定义或描述了合约的行为,使用第一种方法比较方便。 如果基类构造函数的参数依赖于派生合约,那么必须使用第二种方法。 如果像这个简单的例子一样,两个地方都用到了,优先使用 修饰器modifier 风格的参数。
多重继承与线性化
编程语言实现多重继承需要解决几个问题。 一个问题是 钻石问题。 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 {}
代码编译出错的原因是 C
要求 X
重写 A
(因为定义的顺序是 A, X
), 但是 A
本身要求重写 X
,无法解决这种冲突。
可以通过一个简单的规则来记忆: 以从“最接近的基类”(most base-like)到“最远的继承”(most derived)的顺序来指定所有的基类。
继承有相同名字的不同类型成员
当继承导致一个合约具有相同名字的函数和 修饰器modifier 时,这会被认为是一个错误。 当事件和 修饰器modifier 同名,或者函数和事件同名时,同样会被认为是一个错误。 有一种例外情况,状态变量的 getter 可以覆盖一个 public 函数。
抽象合约
合约函数可以缺少实现,如下例所示(请注意函数声明头由 ;
结尾):
pragma solidity ^0.4.0;
contract Feline {
function utterance() public returns (bytes32);
}
这些合约无法成功编译(即使它们除了未实现的函数还包含其他已经实现了的函数),但他们可以用作基类合约:
pragma solidity ^0.4.0;
contract Feline {
function utterance() public returns (bytes32);
}
contract Cat is Feline {
function utterance() public returns (bytes32) { return "miaow"; }
}
如果合约继承自抽象合约,并且没有通过重写来实现所有未实现的函数,那么它本身就是抽象的。
接口
接口类似于抽象合约,但是它们不能实现任何函数。还有进一步的限制:
- 无法继承其他合约或接口。
- 无法定义构造函数。
- 无法定义变量。
- 无法定义结构体
- 无法定义枚举。
将来可能会解除这里的某些限制。
接口基本上仅限于合约 ABI 可以表示的内容,并且 ABI 和接口之间的转换应该不会丢失任何信息。
接口由它们自己的关键字表示:
pragma solidity ^0.4.11;
interface Token {
function transfer(address recipient, uint amount) public;
}
就像继承其他合约一样,合约可以继承接口。
库
库与合约类似,它们只需要在特定的地址部署一次,并且它们的代码可以通过 EVM 的 DELEGATECALL
(Homestead 之前使用 CALLCODE
关键字)特性进行重用。 这意味着如果库函数被调用,它的代码在调用合约的上下文中执行,即 this
指向调用合约,特别是可以访问调用合约的存储。 因为每个库都是一段独立的代码,所以它仅能访问调用合约明确提供的状态变量(否则它就无法通过名字访问这些变量)。 因为我们假定库是无状态的,所以如果它们不修改状态(也就是说,如果它们是 view
或者 pure
函数), 库函数仅可以通过直接调用来使用(即不使用 DELEGATECALL
关键字), 特别是,除非能规避 Solidity 的类型系统,否则是不可能销毁任何库的。
库可以看作是使用他们的合约的隐式的基类合约。虽然它们在继承关系中不会显式可见,但调用库函数与调用显式的基类合约十分类似 (如果 L
是库的话,可以使用 L.f()
调用库函数)。此外,就像库是基类合约一样,对所有使用库的合约,库的 internal
函数都是可见的。 当然,需要使用内部调用约定来调用内部函数,这意味着所有内部类型,内存类型都是通过引用而不是复制来传递。 为了在 EVM 中实现这些,内部库函数的代码和从其中调用的所有函数都在编译阶段被拉取到调用合约中,然后使用一个 JUMP
调用来代替 DELEGATECALL
。
Pure 函数
函数可以声明为 pure
,在这种情况下,承诺不读取或修改状态。
除了上面解释的状态修改语句列表之外,以下被认为是从状态中读取:
- 读取状态变量。
- 访问
this.balance
或者<address>.balance
。 - 访问
block
,tx
,msg
中任意成员 (除msg.sig
和msg.data
之外)。 - 调用任何未标记为
pure
的函数。 - 使用包含某些操作码的内联汇编。
pragma solidity ^0.4.16;
contract C {
function f(uint a, uint b) public pure returns (uint) {
return a * (b + 42);
}
}
批注:官方文档写的太隐晦。
只要有了pure与view修饰符的函数,那么调用函数就不会消耗gas。
而没有pure与view修饰的函数,如下面的change就会消耗gas。
使用场景:
-
view: 可以自由调用,因为它只是“查看”区块链的状态而不改变它
-
pure: 也可以自由调用,既不读取也不写入区块链
原理:
-
pure:不读取更不修改区块上的变量,使用本机的CPU资源计算我们的函数。所以不消耗任何的资源这是很容易的理解的。
-
view: 但是view既然要读取区块链上的值,为什么也不用消耗gas呢?
在Solidity中constant,view,pure三个函数修饰词的作用是告诉编译器,函数不改变/不读取状态变量,这样函数执行就可以不消耗gas了(是完全不消耗!),因为不需要矿工来验证。 在Solidity v4.17之前,只有constant,后来有人嫌constant这个词本身代表变量中的常量,不适合用来修饰函数,所以将constant拆成了view和pure。 view的作用和constant一模一样,可以读取状态变量但是不能改; pure则更为严格,pure修饰的函数不能改也不能读状态变量,否则编译通不过。 大家可以运行以下测试代码来加深这3个关键字的理解。
contract constantViewPure{ string name; uint public age; function constantViewPure() public{ name = "liushiming"; age = 29; } function getAgeByConstant() public constant returns(uint){ age += 1; //声明为constant,在函数体中又试图去改变状态变量的值,编译会报warning, 但是可以通过 return age; // return 30, 但是!状态变量age的值不会改变,仍然为29! } function getAgeByView() public view returns(uint){ age += 1; //view和constant效果一致,编译会报warning,但是可以通过 return age; // return 30,但是!状态变量age的值不会改变,仍然为29! } function getAgeByPure() public pure returns(uint){ return age; //编译报错!pure比constant和view都要严格,pure完全禁止读写状态变量! return 1; } }