可见性和访问限制符(Visibility And Accessors)
因为Solidity可以理解两种函数调用(“内部调用”,不创建一个真实的EVM调用(也称为“消息调用”);“外部的调用”-要创建一个真实的EMV调用), 有四种的函数和状态变量的可见性。
函数可以被定义为external, public, internal or private,缺省是 public。对状态变量而言, external是不可能的,默认是 internal。
external: 外部函数是合约接口的一部分,这意味着它们可以从其他合约调用, 也可以通过事务调用。外部函数f不能被内部调用(即 f()不执行,但this.f()执行)。外部函数,当他们接收大数组时,更有效。
public:公共函数是合约接口的一部分,可以通过内部调用或通过消息调用。对公共状态变量而言,会有的自动访问限制符的函数生成(见下文)。
internal:这些函数和状态变量只能内部访问(即在当前合约或由它派生的合约),而不使用(关键字)this 。
private:私有函数和状态变量仅仅在定义该合约中可见, 在派生的合约中不可见。
请注意
在外部观察者中,合约的内部的各项均可见。用 private 仅仅防止其他合约来访问和修改(该合约中)信息, 但它对blockchain之外的整个世界仍然可见。
可见性说明符是放在在状态变量的类型之后,(也可以放在)参数列表和函数返回的参数列表之间。
contract c {
function f(uint a) private returns (uint b) { return a + 1; }
function setData(uint a) internal { data = a; }
uint public data;
}
其他合约可以调用c.data()来检索状态存储中data的值,但不能访问(函数)f。由c派生的合约可以访问(合约中)setData(函数),以便改变data的值(仅仅在它们自己的范围里)。
访问函数(Getter Functions)
编译器为自动为所有的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();
}
}
在acessExternal函数中,如果直接返回return this.c;,会出现报错Return argument type function () constant external returns (uint256) is not implicitly convertible to expected type (type of first return variable) uint256.。原因应该是通过外部(external)的方式只能访问到this.c作为函数的对象,所以它认为你是想把一个函数转为uint故而报错。
函数修改器(Function Modifiers)
修改器(Modifiers)可以用来轻易的改变一个函数的行为。比如用于在函数执行前检查某种前置条件。修改器是一种合约属性,可被继承,同时还可被派生的合约重写(override)。下面我们来看一段示例代码:
pragma solidity ^0.4.0;
contract owned {
function owned() { owner = msg.sender; }
address owner;
// This contract only defines a modifier but does not use
// it - it will be used in derived contracts.
// The function body is inserted where the special symbol
// "_;" in the definition of a modifier appears.
// This means that if the owner calls this function, the
// function is executed and otherwise, an exception is
// thrown.
modifier onlyOwner {
if (msg.sender != owner)
throw;
_;
}
}
contract mortal is owned {
// This contract inherits the "onlyOwner"-modifier from
// "owned" and applies it to the "close"-function, which
// causes that calls to "close" only have an effect if
// they are made by the stored owner.
function close() onlyOwner {
selfdestruct(owner);
}
}
contract priced {
// Modifiers can receive arguments:
modifier costs(uint price) {
if (msg.value >= price) {
_;
}
}
}
contract Register is priced, owned {
mapping (address => bool) registeredAddresses;
uint price;
function Register(uint initialPrice) { price = initialPrice; }
// It is important to also provide the
// "payable" keyword here, otherwise the function will
// automatically reject all Ether sent to it.
function register() payable costs(price) {
registeredAddresses[msg.sender] = true;
}
function changePrice(uint _price) onlyOwner {
price = _price;
}
}
修改器可以被继承,使用将modifier置于参数后,返回值前即可。
特殊_表示使用修改符的函数体的替换位置。
从合约Register可以看出全约可以多继承,通过,号分隔两个被继承的对象。
修改器也是可以接收参数的,如priced的costs。
使用修改器实现的一个防重复进入的例子。
pragma solidity ^0.4.0;
contract Mutex {
bool locked;
modifier noReentrancy() {
if (locked) throw;
locked = true;
_;
locked = false;
}
/// This function is protected by a mutex, which means that
/// reentrant calls from within msg.sender.call cannot call f again.
/// The `return 7` statement assigns 7 to the return value but still
/// executes the statement `locked = false` in the modifier.
function f() noReentrancy returns (uint) {
if (!msg.sender.call()) throw;
return 7;
}
}
例子中,由于call()方法有可能会调回当前方法,修改器实现了防重入的检查。
如果同一个函数有多个修改器,他们之间以空格隔开,修饰器会依次检查执行。
需要注意的是,在Solidity的早期版本中,有修改器的函数,它的return语句的行为有些不同。
在修改器中和函数体内的显式的return语句,仅仅跳出当前的修改器和函数体。返回的变量会被赋值,但整个执行逻辑会在前一个修改器后面定义的"_"后继续执行。
修改器的参数可以是任意表达式。在对应的上下文中,所有的函数中引入的符号,在修改器中均可见。但修改器中引入的符号在函数中不可见,因为它们有可能被重写。
常量(constant state variables)
状态变量可以被定义为constant,常量。这样的话,它必须在编译期间通过一个表达式赋值。赋值的表达式不允许:1)访问storage;2)区块链数据,如now,this.balance,block.number;3)合约执行的中间数据,如msg.gas;4)向外部合约发起调用。也许会造成内存分配副作用表达式是允许的,但不允许产生其它内存对象的副作用的表达式。内置的函数keccak256,keccak256,ripemd160,ecrecover,addmod,mulmod可以允许调用,即使它们是调用的外部合约。
允许内存分配,从而带来可能的副作用的原因是因为这将允许构建复杂的对象,比如,查找表。虽然当前的特性尚未完整支持。
编译器并不会为常量在storage上预留空间,每个使用的常量都会被对应的常量表达式所替换(也许优化器会直接替换为常量表达式的结果值)。
不是所有的类型都支持常量,当前支持的仅有值类型和字符串。
pragma solidity ^0.4.0;
contract C {
uint constant x = 32**22 + 8;
string constant text = "abc";
bytes32 constant myHash = keccak256("abc");
}
常函数(Constant Functions)
函数也可被声明为常量,这类函数将承诺自己不修改区块链上任何状态。
pragma solidity ^0.4.0;
contract C {
function f(uint a, uint b) constant returns (uint) {
return a * (b + 42);
}
}
访问器(Accessor)方法默认被标记为constant。当前编译器并未强制一个constant的方法不能修改状态。但建议大家对于不会修改数据的标记为constant。
回退函数(fallback function)
每一个合约有且仅有一个没有名字的函数。这个函数无参数,也无返回值。如果调用合约时,没有匹配上任何一个函数(或者没有传哪怕一点数据),就会调用默认的回退函数。
此外,当合约收到ether时(没有任何其它数据),这个函数也会被执行。在此时,一般仅有少量的gas剩余,用于执行这个函数(准确的说,还剩2300gas)。所以应该尽量保证回退函数使用少的gas。
下述提供给回退函数可执行的操作会比常规的花费得多一点。
- 写入到存储(storage)
- 创建一个合约
- 执行一个外部(external)函数调用,会花费非常多的gas
- 发送ether
请在部署合约到网络前,保证透彻的测试你的回退函数,来保证函数执行的花费控制在2300gas以内。
一个没有定义一个回退函数的合约。如果接收ether,会触发异常,并返还ether(solidity v0.4.0开始)。所以合约要接收ether,必须实现回退函数。下面来看个例子:
pragma solidity ^0.4.0;
contract Test {
// This function is called for all messages sent to
// this contract (there is no other function).
// Sending Ether to this contract will cause an exception,
// because the fallback function does not have the "payable"
// modifier.
function() { x = 1; }
uint x;
}
// This contract keeps all Ether sent to it with no way
// to get it back.
contract Sink {
function() payable { }
}
contract Caller {
function callTest(Test test) {
test.call(0xabcdef01); // hash does not exist
// results in test.x becoming == 1.
// The following call will fail, reject the
// Ether and return false:
test.send(2 ether);
}
}
事件(Event)
事件
在介绍事件前,我们先明确事件,日志这两个概念。事件发生后被记录到区块链上成为了日志。总的来说,事件强调功能,一种行为;日志强调存储,内容。
事件是以太坊EVM提供的一种日志基础设施。事件可以用来做操作记录,存储为日志。也可以用来实现一些交互功能,比如通知UI,返回函数调用结果等。
当定义的事件触发时,我们可以将事件存储到EVM的交易日志中,日志是区块链中的一种特殊数据结构。日志与合约关联,与合约的存储合并存入区块链中。只要某个区块可以访问,其相关的日志就可以访问。但在合约中,我们不能直接访问日志和事件数据(即便是创建日志的合约)。下面我们来看看,如何在Solidity中实现一个事件:
pragma solidity ^0.4.0;
contract Transfer{
event transfer(address indexed _from, address indexed _to, uint indexed value);
function deposit() payable {
address current = this;
uint value = msg.value;
transfer(msg.sender, current, value);
}
function getBanlance() constant returns(uint) {
return this.balance;
}
/* fallback function */
function(){}
}
从上面的例子中,我们使用event关键字定义一个事件,参数列表中为要记录的日志参数的名称及类型。
监听事件
在web3.js中,提供了响应事件的方法,如下:
var event = myContract.transfer();
// 监听
event.watch(function(error, result){
console.log("Event are as following:-------");
for(let key in result){
console.log(key + " : " + result[key]);
}
console.log("Event ending-------");
});
另外一种简便写法是直接加入事件回调,这样就不用再写watch的部分:
var event = myContract.transfer(function(error, result){
console.log("Event are as following:-------");
for(let key in result){
console.log(key + " : " + result[key]);
}
console.log("Event ending-------");
});
检索日志
Indexed属性
可以在事件参数上增加indexed
属性,最多可以对三个参数增加这样的属性。加上这个属性,可以允许你在web3.js中通过对加了这个属性的参数进行值过滤,方式如下:
var event = myContract.transfer({value: "100"});
上面实现的是对value值为100的日志,过滤后的返回。
如果你想同时匹配多个值,还可以传入一个要匹配的数组。
var event = myContract.transfer({value: ["99","100","101"]});
增加了indexed
的参数值会存到日志结构的Topic
部分,便于快速查找。而未加indexed
的参数值会存在data
部分,成为原始日志。需要注意的是,如果增加indexed
属性的是数组类型(包括string
和bytes
),那么只会在Topic
存储对应的数据的web3.sha3
哈希值,将不会再存原始数据。因为Topic是用于快速查找的,不能存任意长度的数据,所以通过Topic实际存的是数组这种非固定长度数据哈希结果。要查找时,是将要查找内容哈希后与Topic内容进行匹配,但我们不能反推哈希结果,从而得不到原始值。
View
函数可以被声明为view,在这种情况下,它们承诺不修改状态。
以下语句被认为是修改状态:
- 写入状态变量。
- 发射事件。
- 创建其他合约。
- 使用selfdestruct。
- 通过调用发送Ether。
- 调用其他函数不被标记view或者pure。
- 使用低等级调用。
- 使用包含某些操作码的内联汇编。
contract C {
function f(uint a, uint b) view returns (uint) {
return a * (b + 42) + now;
}
}
constant是view的别名。
Getter方法被标记为view。
编译器并没有强制执行view方法不修改状态。
Pure
函数可以声明为pure,在这种情况下,它们承诺不会从该状态中读取或修改该状态。
除了上述状态修改语句的列表外,以下是从状态读取的:
- 从状态变量读取。
- 访问this.balance或.balance。
- 访问block,tx,msg的任何成员(除了msg.sig和msg.data)。
- 调用任何未标记为pure的功能。
- 使用包含某些操作码的内联汇编。
contract C {
function f(uint a, uint b) pure returns (uint) {
return a * (b + 42);
}
}
编译器并没有强制执行pure方法不是从状态中读取的。
继承(Inheritance)
pragma solidity ^0.4.0;
contract owned {
function owned() { owner = msg.sender; }
address owner;
}
contract mortal is owned {
function kill() {
if (msg.sender == owner) selfdestruct(owner);
}
}
抽象(Abstract Contracts)
抽象函数是没有函数体的的函数。如下:
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"; }
}
如果一个合约从一个抽象合约里继承,但却没实现所有函数,那么它也是一个抽象合约。