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
變量。不使用複製的唯一情況是使用存儲引用變量。