Solidity中的Contracts与面向对象语言中的类相似。它们包含状态变量和函数中的持久数据,可以修改这些变量。在不同的合约(实例)上调用函数将执行EVM函数调用,从而切换上下文,使得状态变量不可访问。
创建合约
合合约可以通过以太坊交易或“从外部”创建。
IDE(例如Remix)使用UI元素使创建过程无缝。
以编程方式在以太坊上创建合同最好通过使用JavaScript API web3.js来完成。它有一个名为web3.eth.Contract的函数, 以方便合同创建。
创建合约时,其构造函数 (使用constructor
关键字声明的函数)将执行一次。
构造函数是可选的。只允许一个构造函数,这意味着不支持重载。
构造函数执行完毕后,合约的最终代码将部署到区块链中。此代码包括所有公共和外部函数以及可通过函数调用从那里访问的所有函数。部署的代码不包括仅从构造函数调用的构造函数代码或内部函数。
在内部,构造函数参数在合约代码本身之后以ABI编码传递,但如果使用,则不必关心它web3.js
。
如果合约想要创建另一个合约,则必须为创建者知道所创建的合约的源代码(和二进制)。这意味着循环创建依赖性是不可能的。
pragma solidity >=0.4.22 <0.6.0;
contract OwnedToken {
// TokenCreator is a contract type that is defined below.
// It is fine to reference it as long as it is not used
// to create a new contract.
TokenCreator creator;
address owner;
bytes32 name;
// This is the constructor which registers the
// creator and the assigned name.
constructor(bytes32 _name) public {
// State variables are accessed via their name
// and not via e.g. this.owner. This also applies
// to functions and especially in the constructors,
// you can only call them like that ("internally"),
// because the contract itself does not exist yet.
owner = msg.sender;
// We do an explicit type conversion from `address`
// to `TokenCreator` and assume that the type of
// the calling contract is TokenCreator, there is
// no real way to check that.
creator = TokenCreator(msg.sender);
name = _name;
}
function changeName(bytes32 newName) public {
// Only the creator can alter the name --
// the comparison is possible since contracts
// are explicitly convertible to addresses.
if (msg.sender == address(creator))
name = newName;
}
function transfer(address newOwner) public {
// Only the current owner can transfer the token.
if (msg.sender != owner) return;
// We also want to ask the creator if the transfer
// is fine. Note that this calls a function of the
// contract defined below. If the call fails (e.g.
// due to out-of-gas), the execution also fails here.
if (creator.isTokenTransferOK(owner, newOwner))
owner = newOwner;
}
}
contract TokenCreator {
function createToken(bytes32 name)
public
returns (OwnedToken tokenAddress)
{
// Create a new Token contract and return its address.
// From the JavaScript side, the return type is simply
// `address`, as this is the closest type available in
// the ABI.
return new OwnedToken(name);
}
function changeName(OwnedToken tokenAddress, bytes32 name) public {
// Again, the external type of `tokenAddress` is
// simply `address`.
tokenAddress.changeName(name);
}
function isTokenTransferOK(address currentOwner, address newOwner)
public
pure
returns (bool ok)
{
// Check some arbitrary condition.
return keccak256(abi.encodePacked(currentOwner, newOwner))[0] == 0x7f;
}
}
可见性
由于Solidity知道两种函数调用(内部函数调用不创建实际的EVM调用(也称为“消息调用”)和外部函数调用),因此函数和状态变量有四种类型的可见性。
功能已被指定为external
, public
,internal
或private
。对于状态变量,external
是不可能的。
external
:
外部函数是合同接口的一部分,这意味着可以从其他合同和交易中调用它们。外部函数f
不能在内部调用(即f()
不起作用,但this.f()
有效)。当外部函数接收大量数据时,它们有时会更有效。
public
:
公共函数是合同接口的一部分,可以在内部调用,也可以通过消息调用。对于公共状态变量,会生成自动getter函数
internal
:
这些函数和状态变量只能在内部访问(即从当前合同或从中获得的合同),而不使用this
。
private
:
私有函数和状态变量仅对其定义的合约可见,而不是在派生合约中可见。
注意
区块链外部的所有观察者都可以看到合同中的所有内容。private
只会阻止其他合约访问和修改信息,但它仍然可以在区块链之外看到。
可见性说明符在状态变量的类型之后以及函数的参数列表和返回参数列表之间给出。
pragma solidity >=0.4.16 <0.6.0;
contract C {
function f(uint a) private pure returns (uint b) { return a + 1; }
function setData(uint a) internal { data = a; }
uint public data;
}
在以下示例中D
,可以调用c.getData()
以检索data
状态存储的值 ,但无法调用f
。合同E
来自C
,因此,可以打电话compute
。
pragma solidity >=0.4.0 <0.6.0;
contract C {
uint private data;
function f(uint a) private pure returns(uint b) { return a + 1; }
function setData(uint a) public { data = a; }
function getData() public view returns(uint) { return data; }
function compute(uint a, uint b) internal pure returns (uint) { return a + b; }
}
// This will not compile
contract D {
function readData() public {
C c = new C();
uint local = c.f(7); // error: member `f` is not visible
c.setData(3);
local = c.getData();
local = c.compute(3, 5); // error: member `compute` is not visible
}
}
contract E is C {
function g() public {
C c = new C();
uint val = compute(3, 5); // access to internal member (from derived to parent contract)
}
}
Getter函数
编译器自动为所有公共状态变量创建getter函数。对于下面给出的合约,编译器将生成一个调用的函数data
,该函数不接受任何参数并返回uint
状态变量的值data
。声明时可以初始化状态变量。
pragma solidity >=0.4.0 <0.6.0;
contract C {
uint public data = 42;
}
contract Caller {
C c = new C();
function f() public view returns (uint) {
return c.data();
}
}
getter函数具有外部可见性。如果符号是在内部访问的(即没有this.
),则它将计算为状态变量。如果它是从外部访问的(即with this.
),则它将计算为一个函数。
pragma solidity >=0.4.0 <0.6.0;
contract C {
uint public data;
function x() public returns (uint) {
data = 3; // internal access
return this.data(); // external access
}
}
如果您有一个public
数组类型的状态变量,那么您只能通过生成的getter函数检索数组的单个元素。这种机制的存在是为了避免返回整个阵列时的高gas成本。例如,您可以使用参数指定要返回的单个元素 data(0)
。如果要在一次调用中返回整个数组,则需要编写一个函数,例如:
pragma solidity >=0.4.0 <0.6.0;
contract arrayExample {
// public state variable
uint[] public myArray;
// Getter function generated by the compiler
/*
function myArray(uint i) returns (uint) {
return myArray[i];
}
*/
// function that returns entire array
function getArray() returns (uint[] memory) {
return myArray;
}
}
现在您可以使用它getArray()
来检索整个数组,而不是 myArray(i)
每个调用返回一个元素。
下一个例子更复杂:
pragma solidity >=0.4.0 <0.6.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;
}
功能修改器
修饰符可用于轻松更改函数的行为。例如,他们可以在执行功能之前自动检查条件。修饰符是合约的可继承属性,可以由派生合约覆盖。
pragma solidity >0.4.99 <0.6.0;
contract owned {
constructor() public { owner = msg.sender; }
address payable 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 {
require(
msg.sender == owner,
"Only owner can call this function."
);
_;
}
}
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() public 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;
constructor(uint initialPrice) public { 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() public payable costs(price) {
registeredAddresses[msg.sender] = true;
}
function changePrice(uint _price) public onlyOwner {
price = _price;
}
}
contract Mutex {
bool locked;
modifier noReentrancy() {
require(
!locked,
"Reentrant call."
);
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() public noReentrancy returns (uint) {
(bool success,) = msg.sender.call("");
require(success);
return 7;
}
}
通过在空格分隔的列表中指定多个修饰符,将多个修饰符应用于函数,并按所显示的顺序进行评估。
警告
在Solidity的早期版本中,return
具有修饰符的函数中的语句表现不同。
修饰符或函数体的显式返回仅保留当前修饰符或函数体。返回变量已分配,控制流在前一个修改器中的“_”后继续。
修饰符参数允许使用任意表达式,在此上下文中,函数中可见的所有符号在修饰符中都是可见的。修饰符中引入的符号在函数中不可见(因为它们可能会因重写而改变)。
常量状态变量
状态变量可以声明为constant
。在这种情况下,必须从表达式中分配它们,该表达式在编译时是常量。不允许任何访问存储,区块链数据(例如now
,address(this).balance
或 block.number
)或执行数据(msg.value
或gasleft()
)或调用外部合同的表达式。允许可能对内存分配产生副作用的表达式,但那些可能对其他内存对象产生副作用的表达式则不允许。内置的功能keccak256
,sha256
,ripemd160
,ecrecover
,addmod
和mulmod
允许(即使他们这样做调用外部合约)
并非所有常量类型都在此时实现。唯一受支持的类型是值类型和字符串。
pragma solidity >=0.4.0 <0.6.0;
contract C {
uint constant x = 32**22 + 8;
string constant text = "abc";
bytes32 constant myHash = keccak256("abc");
}
功能
查看功能
可以声明函数,view
在这种情况下,它们承诺不修改状态。
注意
如果编译器的EVM目标是Byzantium或更新(默认),则操作码 STATICCALL
用于view
强制状态在EVM执行过程中保持不变的函数。对于库view
函数 DELEGATECALL
使用,因为没有组合DELEGATECALL
和STATICCALL
。这意味着库view
函数没有阻止状态修改的运行时检查。这不应该对安全性产生负面影响,因为库代码通常在编译时已知,静态检查器执行编译时检查。
以下语句被视为修改状态:
pragma solidity >0.4.99 <0.6.0;
contract C {
function f(uint a, uint b) public view returns (uint) {
return a * (b + 42) + now;
}
}
注意
constant
函数曾经是别名view
,但在0.5.0版本中被删除了。
注意
Getter方法会自动标记view
。
注意
在0.5.0版之前,编译器没有将STATICCALL
操作码用于view
函数。这view
通过使用无效的显式类型转换启用了对函数的状态修改。通过使用 STATICCALL
for view
功能,可以在EVM级别上防止对状态的修改。
纯函数
可以声明函数,pure
在这种情况下,它们承诺不会读取或修改状态。
注意
如果编译器的EVM目标是Byzantium或更新(默认),STATICCALL
则使用操作码,这不保证不读取状态,但至少不会修改状态。
除了上面解释的状态修改语句列表之外,还考虑以下内容:
- 从状态变量中读取。
- 访问
address(this).balance
或<address>.balance
。 - 访问任何成员
block
,tx
,msg
(与除外msg.sig
和msg.data
)。 - 调用任何未标记的功能
pure
。 - 使用包含某些操作码的内联汇编。
pragma solidity >0.4.99 <0.6.0;
contract C {
function f(uint a, uint b) public pure returns (uint) {
return a * (b + 42);
}
}
注意
在0.5.0版之前,编译器没有将STATICCALL
操作码用于pure
函数。这pure
通过使用无效的显式类型转换启用了对函数的状态修改。通过使用 STATICCALL
for pure
功能,可以在EVM级别上防止对状态的修改。
警告
不可能阻止函数在EVM级别读取状态,只能防止它们写入状态(即只能view
在EVM级别强制执行,pure
不能)。
警告
在版本0.4.17之前,编译器没有强制执行pure
不读取状态。它是一种编译时类型检查,可以避免在合同类型之间进行无效的显式转换,因为编译器可以验证合同的类型不会执行状态更改操作,但是它无法检查合同类型是否合同在运行时调用实际上是该类型。
后备功能
合约可以只有一个未命名的功能。此函数不能有参数,不能返回任何内容并且必须具有external
可见性。如果没有其他函数与给定的函数标识符匹配(或者根本没有提供数据),则在调用合约时执行它。
此外,只要合约收到普通以太网(没有数据),就会执行此功能。此外,为了接收以太网,必须标记回退功能payable
。如果不存在此类功能,则合同无法通过常规交易接收以太币。
在最坏的情况下,后备功能只能依赖2300 gas(例如使用发送或传输时),除基本记录外几乎没有空间执行其他操作。以下操作将消耗比2300 gas更多的燃气:
- 写入存储
- 创建合约
- 调用消耗大量气体的外部功能
- 发送以太币
与任何函数一样,只要有足够的gas传递给它,后备函数就可以执行复杂的操作。
注意
即使回退函数不能有参数,人们仍然可以使用它msg.data
来检索随调用提供的任何有效负载。
警告
如果调用者打算调用不可用的函数,也会执行回退功能。如果要仅实现回退功能以接收以太,则应添加一个检查以防止无效调用。require(msg.data.length == 0)
警告
直接接收以太网的合约(没有函数调用,即使用send
或transfer
)但没有定义回退函数会抛出异常,发送回Ether(这在Solidity v0.4.0之前是不同的)。因此,如果您希望合约接收以太,则必须实施应付回退功能。
警告
没有应付回退功能的合约可以接收以太币作为coinbase交易(也称为矿工块奖励)的接收者或作为a的目的地selfdestruct
。
合约不能对这种以太转移作出反应,因此也不能拒绝它们。这是EVM的设计选择,Solidity无法解决它。
它还意味着address(this).balance
可以高于合同中实现的某些手工会计的总和(即在备用功能中更新计数器)。
pragma solidity >0.4.99 <0.6.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() external { x = 1; }
uint x;
}
// This contract keeps all Ether sent to it with no way
// to get it back.
contract Sink {
function() external payable { }
}
contract Caller {
function callTest(Test test) public returns (bool) {
(bool success,) = address(test).call(abi.encodeWithSignature("nonExistingFunction()"));
require(success);
// results in test.x becoming == 1.
// address(test) will not allow to call ``send`` directly, since ``test`` has no payable
// fallback function. It has to be converted to the ``address payable`` type via an
// intermediate conversion to ``uint160`` to even allow calling ``send`` on it.
address payable testPayable = address(uint160(address(test)));
// If someone sends ether to that contract,
// the transfer will fail, i.e. this returns false here.
return testPayable.send(2 ether);
}
}
函数重载
合同可以具有相同名称但具有不同参数类型的多个功能。此过程称为“重载”,也适用于继承的函数。以下示例显示f
了合同范围内的函数重载 A
。
pragma solidity >=0.4.16 <0.6.0;
contract A {
function f(uint _in) public pure returns (uint out) {
out = _in;
}
function f(uint _in, bool _really) public pure returns (uint out) {
if (_really)
out = _in;
}
}
外部接口中也存在重载的功能。如果两个外部可见函数的Solidity类型不同,而外部类型不同,则会出错。
pragma solidity >=0.4.16 <0.6.0;
// This will not compile
contract A {
function f(B _in) public pure returns (B out) {
out = _in;
}
function f(address _in) public pure returns (address out) {
out = _in;
}
}
contract B {
}
f
上述两个函数重载最终都接受了ABI的地址类型,尽管它们在Solidity内被认为是不同的。
重载决策和参数匹配
通过将当前作用域中的函数声明与函数调用中提供的参数进行匹配来选择重载函数。如果所有参数都可以隐式转换为期望的类型,则选择函数作为重载候选。如果没有一个候选者,则解析失败。
注意
过载分辨率不考虑返回参数。
pragma solidity >=0.4.16 <0.6.0;
contract A {
function f(uint8 _in) public pure returns (uint8 out) {
out = _in;
}
function f(uint256 _in) public pure returns (uint256 out) {
out = _in;
}
}
调用f(50)
会创建一个类型错误,因为50
可以隐式转换为uint8
和uint256
类型。另一方面f(256)
会f(uint256)
因为256
无法隐式转换而解决重载问题uint8
。
事件
Solidity事件在EVM的日志记录功能之上提供了抽象。应用程序可以通过以太坊客户端的RPC接口订阅和监听这些事件。
事件是可继承的合同成员。当您调用它们时,它们会使参数存储在事务的日志中 - 区块链中的特殊数据结构。这些日志与合约的地址相关联,并入区块链,并且只要一个区块可以访问就会待在那里(永远不会出现Frontier和Homestead版本,但这可能会随着Serenity而改变)。无法从合约中访问日志及其事件数据(甚至不能从创建它们的合同中访问)。
可以为日志请求简单的付款验证(SPV),因此如果外部实体提供具有此类验证的合同,则可以检查日志实际存在于区块链内。您必须提供块头,因为合约只能看到最后256个块的哈希值。
您可以将属性添加indexed
到最多三个参数,这些参数将它们添加到称为“主题”的特殊数据结构,而不是日志的数据部分。如果使用数组(包括string
和bytes
)作为索引参数,则将其Keccak-256哈希存储为主题,这是因为主题只能包含一个单词(32个字节)。
没有indexed
属性的所有参数都被ABI编码 到日志的数据部分中。
主题允许您搜索事件,例如在过滤某些事件的块序列时。您还可以按发出事件的合约地址过滤事件。
例如,下面的代码使用web3.js subscribe("logs")
方法过滤与具有特定地址值的主题匹配的日志:
var options = {
fromBlock: 0,
address: web3.eth.defaultAccount,
topics: ["0x0000000000000000000000000000000000000000000000000000000000000000", null, null]
};
web3.eth.subscribe('logs', options, function (error, result) {
if (!error)
console.log(result);
})
.on("data", function (log) {
console.log(log);
})
.on("changed", function (log) {
});
除非您使用说明anonymous
符声明事件,否则事件签名的哈希是主题之一。这意味着无法按名称过滤特定的匿名事件。
pragma solidity >=0.4.21 <0.6.0;
contract ClientReceipt {
event Deposit(
address indexed _from,
bytes32 indexed _id,
uint _value
);
function deposit(bytes32 _id) public payable {
// Events are emitted using `emit`, followed by
// the name of the event and the arguments
// (if any) in parentheses. Any such invocation
// (even deeply nested) can be detected from
// the JavaScript API by filtering for `Deposit`.
emit Deposit(msg.sender, _id, msg.value);
}
}
JavaScript API中的用法如下:
var abi = /* abi as generated by the compiler */;
var ClientReceipt = web3.eth.contract(abi);
var clientReceipt = ClientReceipt.at("0x1234...ab67" /* address */);
var event = clientReceipt.Deposit();
// watch for changes
event.watch(function(error, result){
// result contains non-indexed arguments and topics
// given to the `Deposit` call.
if (!error)
console.log(result);
});
// Or pass a callback to start watching immediately
var event = clientReceipt.Deposit(function(error, result) {
if (!error)
console.log(result);
});
上面的输出如下所示(修剪):
{
"returnValues": {
"_from": "0x1111…FFFFCCCC",
"_id": "0x50…sd5adb20",
"_value": "0x420042"
},
"raw": {
"data": "0x7f…91385",
"topics": ["0xfd4…b4ead7", "0x7f…1a91385"]
}
}
日志的低级接口
另外,也可以通过函数来访问低层接口的记录机制log0
,log1
,log2
,log3
和log4
。 logi
获取类型的参数,其中第一个参数将用于日志的数据部分,其他参数用作主题。上面的事件调用可以以与以下相同的方式执行i + 1
bytes32
pragma solidity >=0.4.10 <0.6.0;
contract C {
function f() public payable {
uint256 _id = 0x420042;
log3(
bytes32(msg.value),
bytes32(0x50cb9fe53daa9737b786ab3646f04d0150dc50ef4e75f59509d83667ad5adb20),
bytes32(uint256(msg.sender)),
bytes32(_id)
);
}
}
其中长十六进制数等于 keccak256("Deposit(address,bytes32,uint256)")
,事件的签名。
了解事件的其他资源
继承
Solidity通过复制代码(包括多态)来支持多重继承。
所有函数调用都是虚函数,这意味着调用派生函数最多的函数,除非明确给出了契约名称。
当合约继承自其他合约时,只在区块链上创建一个合约,并将所有基本合约中的代码复制到创建的合约中。
一般继承系统与Python非常相似 ,特别是关于多重继承,但也存在一些差异。
以下示例给出了详细信息。
pragma solidity >0.4.99 <0.6.0;
contract owned {
constructor() public { owner = msg.sender; }
address payable owner;
}
// Use `is` to derive from another contract. Derived
// contracts can access all non-private members including
// internal functions and state variables. These cannot be
// accessed externally via `this`, though.
contract mortal is owned {
function kill() public {
if (msg.sender == owner) selfdestruct(owner);
}
}
// These abstract contracts are only provided to make the
// interface known to the compiler. Note the function
// without body. If a contract does not implement all
// functions it can only be used as an interface.
contract Config {
function lookup(uint id) public returns (address adr);
}
contract NameReg {
function register(bytes32 name) public;
function unregister() public;
}
// Multiple inheritance is possible. Note that `owned` is
// also a base class of `mortal`, yet there is only a single
// instance of `owned` (as for virtual inheritance in C++).
contract named is owned, mortal {
constructor(bytes32 name) public {
Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
NameReg(config.lookup(1)).register(name);
}
// Functions can be overridden by another function with the same name and
// the same number/types of inputs. If the overriding function has different
// types of output parameters, that causes an error.
// Both local and message-based function calls take these overrides
// into account.
function kill() public {
if (msg.sender == owner) {
Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
NameReg(config.lookup(1)).unregister();
// It is still possible to call a specific
// overridden function.
mortal.kill();
}
}
}
// If a constructor takes an argument, it needs to be
// provided in the header (or modifier-invocation-style at
// the constructor of the derived contract (see below)).
contract PriceFeed is owned, mortal, named("GoldFeed") {
function updateInfo(uint newInfo) public {
if (msg.sender == owner) info = newInfo;
}
function get() public view returns(uint r) { return info; }
uint info;
}
请注意,上面我们称之为mortal.kill()
“转发”销毁请求。完成此操作的方式存在问题,如以下示例所示:
pragma solidity >=0.4.22 <0.6.0;
contract owned {
constructor() public { owner = msg.sender; }
address payable owner;
}
contract mortal is owned {
function kill() public {
if (msg.sender == owner) selfdestruct(owner);
}
}
contract Base1 is mortal {
function kill() public { /* do cleanup 1 */ mortal.kill(); }
}
contract Base2 is mortal {
function kill() public { /* do cleanup 2 */ mortal.kill(); }
}
contract Final is Base1, Base2 {
}
一个调用Final.kill()
将调用Base2.kill
作为最派生的覆盖,但这个函数将绕过 Base1.kill
,主要是因为它甚至不知道 Base1
。解决这个问题的方法是使用super
:
pragma solidity >=0.4.22 <0.6.0;
contract owned {
constructor() public { owner = msg.sender; }
address payable owner;
}
contract mortal is owned {
function kill() public {
if (msg.sender == owner) selfdestruct(owner);
}
}
contract Base1 is mortal {
function kill() public { /* do cleanup 1 */ super.kill(); }
}
contract Base2 is mortal {
function kill() public { /* do cleanup 2 */ super.kill(); }
}
contract Final is Base1, Base2 {
}
如果Base2
调用函数super
,它不会简单地在其一个基本合约上调用此函数。相反,它在最终继承图中的下一个基本合约上调用此函数,因此它将调用Base1.kill()
(请注意,最终的继承序列是 - 从最大的派生契约开始:Final,Base2,Base1,mortal,owned)。使用super时调用的实际函数在使用它的类的上下文中是未知的,尽管其类型是已知的。这与普通的虚方法查找类似。
构造函数
构造函数是使用constructor
关键字声明的可选函数,该关键字在创建合约时执行,您可以在其中运行合约初始化代码。
在执行构造函数代码之前,如果以内联方式初始化状态变量,则将其初始化为指定值,否则将其初始化为零。
构造函数运行后,合约的最终代码将部署到区块链。代码的部署需要额外的gas线性到代码的长度。此代码包括作为公共接口一部分的所有函数以及可通过函数调用从那里访问的所有函数。它不包括仅从构造函数调用的构造函数代码或内部函数。
构造函数可以是public
或internal
。如果没有构造函数,则契约将采用默认构造函数,它等效于。例如:constructor() public {}
pragma solidity >0.4.99 <0.6.0;
contract A {
uint public a;
constructor(uint _a) internal {
a = _a;
}
}
contract B is A(1) {
constructor() public {}
}
构造函数设置为internal
使合同标记为抽象。
警告
在版本0.4.22之前,构造函数被定义为与合同具有相同名称的函数。不推荐使用此语法,在0.5.0版本中不再允许使用此语法。
基础构造函数的参数
将按照下面解释的线性化规则调用所有基本合约的构造函数。如果基础构造函数具有参数,则派生合约需要指定所有参数。这可以通过两种方式完成:
pragma solidity >=0.4.22 <0.6.0;
contract Base {
uint x;
constructor(uint _x) public { x = _x; }
}
// Either directly specify in the inheritance list...
contract Derived1 is Base(7) {
constructor() public {}
}
// or through a "modifier" of the derived constructor.
contract Derived2 is Base {
constructor(uint _y) Base(_y * _y) public {}
}
一种方法直接在继承列表()中。另一种方法是作为派生构造函数()的一部分调用修饰符。如果构造函数参数是常量并定义合同的行为或描述它,则第一种方法更方便。如果基类的构造函数参数依赖于派生合约的参数,则必须使用第二种方法。参数必须在继承列表中或在派生构造函数中以修饰符样式给出。在两个地方指定参数是一个错误。is Base(7)
Base(_y * _y)
如果派生合约没有指定所有基础契约构造函数的参数,那么它将是抽象的。
多重继承和线性化
允许多重继承的语言必须处理几个问题。一个是钻石问题。Solidity类似于Python,因为它使用“ C3线性化 ”来强制基类的有向无环图(DAG)中的特定顺序。这导致了单调性的理想特性,但不允许一些遗传图。特别是,is
指令中给出基类的顺序很重要:您必须按照从“最类似基数”到“最多派生”的顺序列出直接基本合约。请注意,此顺序与Python中使用的顺序相反。
解释这个问题的另一种简化方法是,当调用在不同契约中多次定义的函数时,以深度优先的方式从右到左(从左到右)搜索给定的基数,在第一个匹配时停止。如果已经搜索了基本合同,则会跳过该合同。
在下面的代码中,Solidity将给出错误“继承图的线性化不可能”。
pragma solidity >=0.4.0 <0.6.0;
contract X {}
contract A is X {}
// This will not compile
contract C is A, X {}
原因是C
请求X
覆盖A
(通过按此顺序指定),但本身请求覆盖,这是一个无法解决的矛盾。A, X
A
X
继承不同种类的同名成员
当继承导致与函数和同名修饰符的契约时,它被视为错误。此错误也由同名的事件和修饰符以及同名的函数和事件产生。作为例外,状态变量getter可以覆盖公共函数。
抽象合约
当至少其中一个函数缺少实现时,合约被标记为抽象,如下例所示(请注意,函数声明标头以其终止;
):
pragma solidity >=0.4.0 <0.6.0;
contract Feline {
function utterance() public returns (bytes32);
}
此类合约无法编译(即使它们包含已实现的功能以及未实现的功能),但它们可用作基本合约:
pragma solidity >=0.4.0 <0.6.0;
contract Feline {
function utterance() public returns (bytes32);
}
contract Cat is Feline {
function utterance() public returns (bytes32) { return "miaow"; }
}
如果契约继承自抽象契约并且没有通过覆盖实现所有未实现的函数,那么它本身就是抽象的。
请注意,没有实现的函数与函数类型不同,即使它们的语法看起来非常相似。
没有实现的函数示例(函数声明):
function foo(address) external returns (address);
函数类型的示例(变量声明,其中变量的类型function
):
function(address) external returns (address) foo;
抽象合约将合约的定义与其实现分离,从而提供更好的可扩展性和自我记录,并促进模板方法和删除代码重复等模式。抽象合约与在界面中定义方法很有用的方式非常有用。这是抽象合约的设计者说“我的任何一个孩子必须实现这种方法”的一种方式。
接口
接口类似于抽象合约,但它们不能实现任何功能。还有其他限制:
- 他们不能继承其他合约或接口。
- 所有声明的函数必须是外部的。
- 他们不能声明构造函数。
- 他们不能声明状态变量。
其中一些限制可能会在未来解除。
接口基本上限于Contract ABI可以表示的内容,并且ABI和接口之间的转换应该是可能的,而不会丢失任何信息。
接口由它们自己的关键字表示:
pragma solidity >=0.4.11 <0.6.0;
interface Token {
enum TokenType { Fungible, NonFungible }
struct Coin { string obverse; string reverse; }
function transfer(address recipient, uint amount) external;
}
合约可以继承接口,因为它们将继承其他合约。
在接口和其他类似合约的结构中定义的类型可以从其他合约访问:Token.TokenType
或Token.Coin
。
库
库类似于合约,但它们的目的是它们仅在特定地址部署一次,并且使用EVM 的DELEGATECALL
(CALLCODE
直到Homestead)功能重用它们的代码。这意味着如果调用库函数,则它们的代码在调用合约的上下文中执行,即this
指向调用合约,尤其是可以访问调用合约中的存储。由于库是一段孤立的源代码,如果它们是显式提供的,它只能访问调用合约的状态变量(否则无法命名它们,否则)。库函数只能直接调用(即不使用DELEGATECALL
),如果它们不修改状态(即它们是view
或是pure
函数),因为假定库是无状态的。特别是,不可能销毁库。
注意
在版本0.4.20之前,可以通过规避Solidity的类型系统来销毁库。从该版本开始,库包含一种机制,该机制不允许直接调用状态修改函数(即没有DELEGATECALL
)。
库可以被视为使用它们的合约的隐含基础合约。它们在继承层次结构中不会显式可见,但对库函数的调用看起来就像对显式基本合约函数的调用(L.f()
如果L
是库的名称)。此外, internal库
的功能在所有合同中都可见,就像库是基本合同一样。当然,对内部函数的调用使用内部调用约定,这意味着可以传递所有内部类型,存储在内存中的类型将通过引用传递而不是复制。为了在EVM中实现这一点,内部库函数的代码和从中调用的所有函数将在编译时被拉入调用合约,并且定期JUMP
将使用呼叫而不是a DELEGATECALL
。
下面的示例说明了如何使用库(但是手动方法请务必使用for来查看实现集合的更高级示例)。
pragma solidity >=0.4.22 <0.6.0;
library Set {
// We define a new struct datatype that will be used to
// hold its data in the calling contract.
struct Data { mapping(uint => bool) flags; }
// Note that the first parameter is of type "storage
// reference" and thus only its storage address and not
// its contents is passed as part of the call. This is a
// special feature of library functions. It is idiomatic
// to call the first parameter `self`, if the function can
// be seen as a method of that object.
function insert(Data storage self, uint value)
public
returns (bool)
{
if (self.flags[value])
return false; // already there
self.flags[value] = true;
return true;
}
function remove(Data storage self, uint value)
public
returns (bool)
{
if (!self.flags[value])
return false; // not there
self.flags[value] = false;
return true;
}
function contains(Data storage self, uint value)
public
view
returns (bool)
{
return self.flags[value];
}
}
contract C {
Set.Data knownValues;
function register(uint value) public {
// The library functions can be called without a
// specific instance of the library, since the
// "instance" will be the current contract.
require(Set.insert(knownValues, value));
}
// In this contract, we can also directly access knownValues.flags, if we want.
}
当然,您不必按照这种方式使用库:它们也可以在不定义struct数据类型的情况下使用。函数也可以在没有任何存储引用参数的情况下工作,并且它们可以在任何位置具有多个存储引用参数
调用Set.contains
,Set.insert
并且Set.remove
都被编译为调用(DELEGATECALL
)到外部合约/库。如果使用库,请注意执行实际的外部函数调用。 msg.sender
,msg.value
并且this
将保留它们的值在此调用。
下面的示例演示如何使用存储在内存中的类型和库中的内部函数来实现自定义类型,而无需外部函数调用的开销:
pragma solidity >=0.4.16 <0.6.0;
library BigInt {
struct bigint {
uint[] limbs;
}
function fromUint(uint x) internal pure returns (bigint memory r) {
r.limbs = new uint[](1);
r.limbs[0] = x;
}
function add(bigint memory _a, bigint memory _b) internal pure returns (bigint memory 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) {
// too bad, we have to add a limb
uint[] memory newLimbs = new uint[](r.limbs.length + 1);
uint i;
for (i = 0; i < r.limbs.length; ++i)
newLimbs[i] = r.limbs[i];
newLimbs[i] = carry;
r.limbs = newLimbs;
}
}
function limb(bigint memory _a, uint _limb) internal pure returns (uint) {
return _limb < _a.limbs.length ? _a.limbs[_limb] : 0;
}
function max(uint a, uint b) private pure returns (uint) {
return a > b ? a : b;
}
}
contract C {
using BigInt for BigInt.bigint;
function f() public pure {
BigInt.bigint memory x = BigInt.fromUint(7);
BigInt.bigint memory y = BigInt.fromUint(uint(-1));
BigInt.bigint memory z = x.add(y);
assert(z.limb(1) > 0);
}
}
由于编译器无法知道将在何处部署库,因此必须通过链接器将这些地址填充到最终字节码中(请参阅使用命令行编译器了解如何使用命令行编译器进行链接)。如果地址不作为编译器的参数给出,则编译的十六进制代码将包含表单的占位符__Set______
(其中 Set
是库的名称)。可以通过用库合同地址的十六进制编码替换所有这40个符号来手动填充地址。
注意
不鼓励在生成的字节码上手动链接库,因为它限制为36个字符。如果您使用编译器的标准-JSON接口,您应该要求编译器在编译合同时链接库,方法是使用--libraries
选项solc
或libraries
键。
与合约相比,库的限制:
- 没有状态变量
- 不能继承也不能继承
- 无法接收以太网
(这些可能会在以后解除。)
库的使用
正如介绍中提到,如果某个库的代码被执行使用CALL
,而不是一个DELEGATECALL
或CALLCODE
,它会恢复,除非view
或pure
函数被调用。
EVM没有为合约提供直接的方法来检测它是否被调用CALL
,但是合约可以使用ADDRESS
操作码找出它当前正在运行的“位置”。生成的代码将此地址与构造时使用的地址进行比较,以确定调用模式。
更具体地说,库的运行时代码总是以push指令开始,该指令在编译时是20字节的零。部署代码运行时,此常量将在内存中被当前地址替换,并且此修改后的代码将存储在合约中。在运行时,这会导致部署时间地址成为第一个被推送到堆栈的常量,并且调度程序代码将当前地址与任何非视图和非纯函数的此常量进行比较。
使用
该指令可用于将库函数(从库)附加到任何类型()。这些函数将接收它们被调用的对象作为它们的第一个参数(如Python中的变量)。using A for B;
A
B
self
其效果是库中的函数附加到任何类型。using A for *;
A
在这两种情况下,库中的所有函数都会被附加,即使是第一个参数的类型与对象类型不匹配的函数也是如此。在调用函数时检查类型并执行函数重载决策。
该指令仅在当前合约中有效,包括在其所有功能中,并且在使用它的合同之外无效。该指令只能在合同中使用,而不能在其任何功能中使用。using A for B;
通过包含库,可以使用包括库函数在内的数据类型,而无需添加更多代码。
让我们以这种方式重写库中的set示例 :
pragma solidity >=0.4.16 <0.6.0;
// This is the same code as before, just without comments
library Set {
struct Data { mapping(uint => bool) flags; }
function insert(Data storage self, uint value)
public
returns (bool)
{
if (self.flags[value])
return false; // already there
self.flags[value] = true;
return true;
}
function remove(Data storage self, uint value)
public
returns (bool)
{
if (!self.flags[value])
return false; // not there
self.flags[value] = false;
return true;
}
function contains(Data storage self, uint value)
public
view
returns (bool)
{
return self.flags[value];
}
}
contract C {
using Set for Set.Data; // this is the crucial change
Set.Data knownValues;
function register(uint value) public {
// Here, all variables of type Set.Data have
// corresponding member functions.
// The following function call is identical to
// `Set.insert(knownValues, value)`
require(knownValues.insert(value));
}
}
也可以用这种方式扩展基本类型:
pragma solidity >=0.4.16 <0.6.0;
library Search {
function indexOf(uint[] storage self, uint value)
public
view
returns (uint)
{
for (uint i = 0; i < self.length; i++)
if (self[i] == value) return i;
return uint(-1);
}
}
contract C {
using Search for uint[];
uint[] data;
function append(uint value) public {
data.push(value);
}
function replace(uint _old, uint _new) public {
// This performs the library function call
uint index = data.indexOf(_old);
if (index == uint(-1))
data.push(_new);
else
data[index] = _new;
}
}
请注意,所有库调用都是实际的EVM函数调用。这意味着如果传递内存或值类型,将执行复制,甚至是 self
变量。不使用复制的唯一情况是使用存储引用变量。