1. 程式人生 > >solidity語言學習(9)—— 合約(Contract)

solidity語言學習(9)—— 合約(Contract)

在solidity中合約類似於面對物件的語言中的類。他包括了狀態變數中那些長期的資料以及能夠修改這些狀態的函式。在一個合約中呼叫另一個合約(例項)的函式會執行一個EVM函式呼叫,它將轉換上下文內容是得狀態變數不可觸及。

創造合約

合約是通過以太坊交易或者來自solidity合約內部,“從外部”產生的。

一些典型的IDE,比如說Remix,能夠使用UI元素創造無縫的程序。

在以太坊中程式化的產生一個合約,最好是通過使用JavaScript API web3.js的辦法。另外今天有一個新的方法——web3,eth.Contract 來使合約創造變得更加容易。

當一個合約被生成出來,他的構造器(一個宣告含 constructor 關鍵字的函式)已被執行一次。constructor可以是隨意的,但是隻允許有一個constructor,這意味著重載入(overloading)是不支援的。

在內部,構造引數在傳遞通過合約程式碼後被送到ABI 編碼處,但如果你使用web3js,你不需要考慮這些。

如果你的合約想要建立另一個合約,生成的新合約的原始碼(還有二進位制碼)必須被創造者提前知曉。這意味著遞迴迴圈創造合約是不可能的。

pragma solidity ^0.4.22;

contract OwnedToken {
   // 代幣建立(TokenCreator)是下面定義的一種合約型別。
   // 只要它沒有被用於創造新合約,引用它都沒問題
   TokenCreator creator;
   address owner;
   bytes32 name;

   // 下面這個構造器記錄了創造者和賦值名稱
constructor(bytes32 _name) public { // 狀態變數可以通過它們的名稱訪問,但不能通過 // 諸如 this.owner 來訪問。這一點也同樣適用於函式。而且尤其是在構造器當中, // 你只能像這樣呼叫他們(“internally”)(我估摸意思是想內部呼叫一樣呼叫引數), // 因為合約此時還並不存在 owner = msg.sender; // 我們做一個顯式的型別轉換,將address轉換為TokenCreator。並且只能假設呼叫者合約的 // 型別為TokenCreator,但實際上並沒有真正確定它的辦法
creator = TokenCreator(msg.sender); name = _name; } function changeName(bytes32 newName) public { // 只有創造者能夠修改名字 // 下面的比較是可實現的,因為合約被隱式的轉化為address if (msg.sender == address(creator)) name = newName; } function transfer(address newOwner) public { // 只有當前的代幣擁有者可以交易代幣 if (msg.sender != owner) return; // 我們也希望詢問代幣的創造者這樣的交易是否ok。注意這裡呼叫了一個 // 下面定義的合約中的一個函式。如果這個呼叫失敗了(比如說因為耗盡了gas) // 這裡的執行將立即停止 if (creator.isTokenTransferOK(owner,newOwner)) owner. = newOwner; } } contract TokenCreator { function createToken(byres32 name) public returns (OwndeToken tokenAddress) { // 創造一個新的代幣合約,並返回它的address。 // 從JavaScript的角度,返回型別是一個簡單的‘address’ // 但這是在ABI中最嚴密(closest)的可用型別 return new OwndeToken(name); } function changeName(OwndeToken tokenAddress,bytes32 name) public{ // 同上面一樣,‘tokenAddress’的外部型別只是簡單的‘address’ tokenAddress.changeName(name); } function isTokenTransferOK(address currentOwner,address newOwner) public view returns (bool ok) { // 檢查一些不確定的(arbitrary)狀態 address tokenAddress = msg.sender; return (keccak256(newOwner) & 0xff) == (bytes20(tokenAddress) & 0xff); } }

可見性 和 獲得者(Visibility and Getter)

因為solidity知道兩種函式呼叫方式(內部呼叫不會創造一個實際的EVM呼叫(也稱為資訊呼叫),而外部呼叫會這樣做),對於函式和狀態變數,這裡提供了四種可見性。

函式有四種特定的可見性,external,public,internal或者private,而預設是public。對於狀態變數,external是不可實現的但是預設為internal。

external:
external 函式 是 合約介面的一部分,這意味著我們可以通過其他合約或通過交易來呼叫它們。一個external函式f不能被從內部呼叫(比如:f()就沒有效果,但是this.f()就有用)。當接收到大的資料陣列時,external函式有時會更有效率。

public:
public 函式是合約介面的一部分,而且既可以被內部呼叫,也可以通過訊息呼叫。對於public狀態變數,會自動生成一個獲取(它的值)的函式。

internal:
這些函式和狀態變數只能在內部被訪問(比如說從當前合約或者當前合約的匯出合約),而且不需要使用this。

private:
private函式和狀態變數僅僅在建立它的合約中可見,即使是匯出合約中也不可見。

note:
一個合約中的所有東西在內部都是可見的。讓某些量 private 化僅僅是為了阻止其他合約訪問或修改這些資訊,但是其實他對於整個區塊鏈外的事件依然是可見的。

可見性標示在狀態變數的後面標示出來,以及在函式的引數列表和返回引數列表之間標示。

pragma solidity ^0.4.16;

contract C {
    funtion f(uint a) private pure reutrns (uint b) { return a + 1; }
    function setData(uint a) internal { data = a; }
    uint public data;
}

在下面這個例子中,D函式可以呼叫c.getData() 來取回狀態儲存中data的值,但它並不能呼叫f。合約E是C匯出的合約(E繼承了C),因此 他可以呼叫 compute 函式:

// This will not compile
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);// 可以訪問internal成員(因為是從匯出合約向母合約)
    }
}

getter 函式
編譯其會為所有public狀態變數自動生成一個getter函式。比如為下面給出的這個合約,編譯器會生成一個叫data 的函式,他不需要任何引數,但會返回一個uint,這個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函式具有外部可見性。如果這個標誌形式是內部可見的(沒有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 agr1, bool arg2, uint arg3) public returns (uint a,bytes3 b){
    a = data[arg1][arg2][arg3].a;
    b = data[arg1][arg2][arg3].b;
}

注意在結構體中mapping被省略了,因為沒有好的方法去提供mapping的key值。

函式修飾器(Function Modifiers)

修飾器能夠被用來簡單的改變函式的狀態。比如說,他們可以在函式執行之前自動檢查環境。修飾器是由可繼承性質的合約,而且他可能被繼承者直接無視掉。

pragma solidity  ^0.4.22;

contract owned {
   function owned() public { owner = msg.sender;}
   address owner;

   // 這個合約僅僅定義了一個修飾器,但並不使用它:它會被匯出合約所使用。
   // 函式體會被插入到修飾器定義程式碼中 “_” 的位置
   // 這意味著如果owner呼叫了這個函式,那麼函式體要麼會執行
   // 要麼會丟擲一個異常
   modifier onlyOwner {
      require(
          msg.sender == owner,
          "Only owner cal call this function."
        );
        _;
      }
}

contract mortal is owned {
    // 這個合約從’owned‘繼承了‘onlyOwner'修飾器並且將其應用在’close‘函式上
    // 這導致在呼叫close時僅當滴啊用者是owner時才會起作用
    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 initialPrice) public { price = initialPrice; }

   // 在這裡,提供’payable‘關鍵字也非常重要,否則函式
   // 會自動拒絕所有送來的Ether
   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;
  }

  /// 函式被一個mutex所保護,這意味著無法從“msg.sender.call"裡再次呼叫’f’函式
  /// 'return 7' 指令將7分配給一個返回值但依然執行了修飾器中
  /// ”locked=false“這條語句。
  function f() public noReentrancy returns (uint) {
      require(msg.sender.call());
      return 7;
  }
}

若要在一個函式中使用多個修飾器,可以通過一個空格分割的list,並且將按照擺放順序執行

warning:
在早期版本的solidity,return指令在由修飾器的函式中將有不同的表現

修飾器和函式的return函式,都僅僅只是退出了當前函式體。在更上一級的修飾器中,在‘_’後的部分,返回的變數依然會賦值,而控制流也會繼續。

在修飾其的闡述和上下文中任意表達式都是允許的,所有在函式中可見的符號都同樣在修飾器中可見。然而在修飾器中定義的符號在函式中就不可見了(因為它們可能被重寫(overriding)所改變)

重寫overriding:Overriding 是指子類有一個函式與父類中的某個虛擬函式的名字和簽名都相同。當一個子類的物件呼叫該虛擬函式時,就會執行子類中Overriding 的那個函式。所以Overriding 改變的是類的行為而不是類的介面。

常數狀態變數 (Constant State Variable)

狀態變數可以被定義為constan。在這種情況下,它們必須被一個表示式賦值,該表示式在編譯時就是一個常數。任何用於訪問儲存,區塊鏈資料(比如 now,this.balance 或者 block.number)或者處理資料(msg.value 或 gasleft() )或者 對外部合約進行呼叫的 表示式都是不允許的。對於記憶體分配有副作用的表示式是允許的,但那些或許對其他記憶體物體由副作用的表示式就不允許了。一些固定的內在函式 比如 keccak256,sha256,ripemd160,ecrecover,addmod和 mulmod都是允許的(即使他們能夠呼叫外部合約)。

允許使用對記憶體分配由副作用的表示式的原因,在於它可能構建複雜的物件,比如查詢表格。這一特性尚未完全可用。

編譯器並不會為這些變數提供一個儲存匣,所有事件都被各自的常數表示式所替換(這些值可能被最優化器計算為一個單值)

目前並非所有型別的常數都可以實現。目前暫時支援的型別有值型別和字串型別。

pragma solidity ^0.4.0;

contract C {
    uint constant x = 32**22 + 8;
    string constant text = "abc" ;
    bytes32 constant myHash = keccak256("abc");
}

函式(Function)

View Function
宣告為 view 的函式將保證並不會修改狀態。

以下的指令將被視為對狀態的修改:

  1. 寫入狀態變數。
  2. 發表事件(Emitting Events)
  3. 生成其他合約
  4. 使用自毀(selfdestruct)
  5. 通過呼叫傳輸Ether
  6. 呼叫其他未標記 view 或 pure 的函式
  7. 使用低階呼叫(low-level call)
  8. 使用包含控制程式碼的線上元件
pragma solidity ^0.4.16;

contract C {
    function f(uint a , uint b) public view returns (uint) {
       return a * (b + 42) = now;
    }
}

note:
1.對於view型函式來說,constant 只是一個(表示式的)別名(alias),但這一點已遭到反對並將在0.5.0中被移除
2.getter方法又被標明 view。
3.如果使用了無效的顯式型別轉換,即使呼叫了一個 view 函式,狀態修改依然是可行的。當呼叫這類函式時,你可以通過增加pragma experimental "v0.5.0";改變編譯器,使用STATICCALL ,以阻止在EVM層級上狀態的改變
warning:
編譯器並不會強制使view方法不能改變狀態,它只是會彈出一個警告。

Pure Function
被宣告為 pure 的函式會承諾不從狀態中讀取(資訊?)(read from)或修改狀態。

作為前面解釋狀態修改指令的補充,以下被認為而是從狀態讀取的指令:
1. 從狀態變數中讀取
2. 訪問 this.balance 或者 <address>.balance
3. 讀取 blocktxmsg的任何成員(但是 msg.sigmsg.data例外)
4. 呼叫任何沒有標記 pure 的函式
5. 使用包含確定的操作程式碼的線上元件

pragma solidity ^0.4.16;

contract C {
    function f(uint a,uint b) public pure returns (uint) {
          return a * (b + 42);
     }
}

note:
如果使用了無效的顯示型別轉換,即使標誌了 pure 的函式被呼叫了,狀態修改依然有效。同樣可以使用上面所說的改變編譯器的方法解決此問題
warning:
1.在EVM層面阻止函式讀取狀態是不可能的,唯一可能的是阻止它們寫入狀態(比如說,在EVM層面只有 view 可以被強制執行,但pure不行)
2.在 0.4.17之前,編譯器並不強制要求pure 不能讀取狀態

Fallback Function
一個合約只能恰好有一個未命名的函式。該函式不能有引數,也不能返回任何值。如果沒有其他函式匹配給予的函式標示(或者壓根沒有提供任何資料)(這裡是說呼叫命令中沒有呼叫合約中其他有用的函式),它會在呼叫合約時執行,

此外,無論何時當合約收到plain Ether(這裡不太會翻)時(沒有資料),這個函式就會執行。補充一點,為了收到Ether,回退函式(Fallback Function)必須被標記為 payable。如果沒有這樣的函式存在,則該合約無法通過正常的交易收到Ether。

在最壞的情況下,回退函式能夠僅僅依賴與可用的2300 gas(比如說當傳輸或交易需要時),而無法留下足夠空間去執行除基本登入以外其他的操作。以下操作會消耗大於2300gas:

  • 寫入儲存
  • 生成合約
  • 呼叫一個消耗大量gas的外部函式
  • 傳輸Ether
    和其他函式一樣,回退函式能夠處理複雜的操作,只要有足夠的gas傳輸給它。

Note:
即使回退函式不能由引數,但它依然可以使用 msg.data 來提取由呼叫帶來的任何負載。
warning:
1.沒有過定義回退函式,但直接收到Ether(如通過send和transfer)的合約將會丟擲一個異常並退還收到的Ether(這在0.4.0之前的版本是不同的)所以如果你的合約要收到Ether,你必須有一個回退函式。
2.一個沒有payable回退函式的合約可以收到Ether,作為一筆Coinbase交易(比如說礦工挖礦)的收據,或者作為一次自毀(selfdestruct)的目標。
一個合約並不能對這樣的Ehter交易做出反應,而且也同樣不能拒絕它們。這是EVM設計的選擇,Solidity對此並無能為力。
這也意味著this.balance能夠比一些合約提供的人工賬戶之和還高(比如在回退函式中來一個反向更新)

pragma solidity ^0.4.0;

contract Test {
     // 該函式在所有資訊傳送給該合約時就被呼叫(因為這裡沒有別的函式)
     // 如果向該合約傳送Ether,將會引起異常,
     //  因為回退函式沒有payable標示
     function() public { x = 1; }
     uint x;
}

// 這個合約保證所有傳送給他的Ether沒法在拿回去
contract Sink{
     function() public payable { }
}

contract Caller {
   function callTest(Test test) public {
       test.call(0xabcdef01); // 這個雜湊並不存在
       // 將導致 test.x 變成 1

       // 下面的並不會被編譯,但是即使由任何人傳送Ether
       // 給這個合約,交易都會失敗,Ether會被拒絕接受
       // test.send(2 ether);
     }
 }

函式過載(Function Overloading)

一個合約可以由許多重名的函式但必須有不同的引數。天河一點同樣適用與繼承函式上。下面的例子就展示了在A合約的範圍內f函式的過載。

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;
    }
}

過載函式也同樣會出現在外部介面中。如果兩個外部可見的函式無法通過他們的外部型別區分,而只能通過他們的Solidity型別區分的話就導致錯誤。

//  this will not compile
pragma solidity ^0.4.16;

contract A {
    function f(B _in) public pure returns (B out) {
         out = _in;
    }

    funciton f(address _in) public pure returns (address out) {
          out = _in;
    }
}

contract B {
}

兩個過載的 f 函式最終都會接受到ABI的地址型別,儘管他們在Solidity中被認為是不同的。

過載決議和引數匹配(Overload resolution and Argument matching)
過載函式通過匹配 呼叫提供的引數 和 在當前轄域內宣告的引數 來受到選擇。如果所有引數都被隱式的轉化為期望的型別,函式將被選擇為過載的候選。如果沒有一個確切 的 候選者,決定將失效。(簡單來說就是很多重名函式,呼叫指令是呼叫哪一個的問題,如果某函式宣告引數可以通過隱式的轉換以匹配上呼叫指令,那麼這個函式就是候選函式之一;如果沒有一個函式能匹配上,這個呼叫就失效了)

Note:
對於過載決議來說,返回引數並不在考慮範圍

pragma solidity ^0.4.16;

contract A {
     function f(uint _in) public pure returns (uint 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.

事件(Events)

Events allow the convenient usage of the EVM logging facilities, which in turn can be used to “call” JavaScript callbacks in the user interface of a dapp, which listen for these events.
事件允許方便的使用EVM的記錄裝置,這些設施能夠在用於監聽事件的dapp的使用者互動介面中,
被輪流用於呼叫JavaScript的回撥函式。

CALLBACK,即回撥函式,是一個通過函式指標呼叫的函式。如果你把函式的指標(地址)作為引數傳遞給另一個函式,當這個指標被用為呼叫它所指向的函式時,我們就說這是回撥函式。回撥函式不是由該函式的實現方直接呼叫,而是在特定的事件或條件發生時由另外的一方呼叫的,用於對該事件或條件進行響應。
實現的機制:
[1]定義一個回撥函式;
[2]提供函式實現的一方在初始化的時候,將回調函式的函式指標註冊給呼叫者;
[3]當特定的事件或條件發生的時候,呼叫者使用函式指標呼叫回撥函式對事件進行處理

事件是合約的可繼承成員。當他們被呼叫時,他的引數將被存放於交易記錄(transaction‘s log)——區塊鏈一個特殊的資料結構——當中。這些記錄和合約地址相連繫,並且會被合併金區塊鏈中,只要該塊可以訪問,就一直存放在那兒(forever as of Frontier and Homestead, but this might change with Serenity). 記錄和事件資料在合約內部是無法訪問的(即使是創造他們的合約也不行)

SPV為記錄做證明是可行的,所以如果一個外部實體 提供了一個包含這種proof的合約,它將檢查該記錄是否在區塊鏈中真實存在。但是注意塊頭部必須提供,因為合約僅能看到最近256個塊的雜湊值。

至多有3個引數能夠獲得indexed屬性,使得每個引數可以被查詢到:由特定值來在使用者介面過濾indexed引數稱為可能。

如果數列(包括 String和 bytes)被用作indexed引數,它的keccak-356 雜湊將代替作為其標籤(topic)。

除非你用anonymous識別符號來宣告一個事件,該事件簽名的雜湊值將是標籤中的一個。這也意味這用名稱來賽選特定的匿名事件是不可行的。

所有 非indexed引數都會被存放著記錄的資料部分。

Note:
indexed引數不會保持它們本身。你僅能查詢到這些值,但它們沒辦法自己取回這些值。

pragma solidity ^0.4.0;

contract ClientReceipt {
     event Deposit {
         address indexed _from,
         bytes32 indexed _id,
         uint _value
     };

     function deposit(bytes32 _id) public payable {
         // 事件通過使用’emit‘來發布,後接事件名和引數(圓括號中)
         // 任何這種呼叫(即使在很深的巢狀中)能夠被JavaScript API
         // 通過過濾’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();

// 變化後開始觀察
event.watch(function(error,result){
     // 結果中包含許多資訊,包括‘Deposit’呼叫需要的引數
     if (!error) 
           console.log(result);
});

// 或者傳一個回撥過去,立即開始觀察  
var event = clientReceipt.Deposit(function(error, result) {
       if (!error)
           console.log(result);
});        

記錄的低階介面(Low-Level Interface to Logs)
通過函式 log0,log1,log2等等,可以訪問到記錄機制的低階介面。logi 需要 i+1 個bytes32型別的引數,其中第一個引數會被用於記錄的資料部分,其餘的作為標籤。上面說道的記錄呼叫將會和下面相同的方式執行:

pragma solidity ^0.4.10;

contract C {
    function f() public payable {
          bytes32 _id = ox420042;
          log3{
              bytes32(msg.value),
              bytes32(0x50cb9fe53daa9737b786ab3646f04d0150dc50ef4e75f59509d83667ad5adb20),
              bytes32(msg.sender),
              _id
          );
     }
}

中間那串長的十六進位制數,等同於keccak256("Deposit(address,bytes32,uint256"),是該事件的簽名。

其他了解事件的資源
JavaScript文件
events的使用示例
怎麼在js中訪問他們

繼承(Inheritance)

Solidity指出通過複製程式碼(包括多型)來實現多繼承。

All function calls are virtual, which means that the most derived function is called, except when the contract name is explicitly given.
所有函式呼叫都是虛擬的,這意味這大部分匯出函式也被呼叫了,除非當合約名字已經明確給出的時候。

當一個合約從多個合約繼承,只有一個單個合約被生成在區塊鏈上,而來自其他基礎合約的程式碼將被複制到生成的合約上。

繼承系統的大部分都類似於Python的繼承系統,特別是關於多繼承的部分。

下面的例子將給出一些細節

pragma solidity ^0.4.22;

contract owned {
     constructor() { owner = msg.sender; )
     address owner;
}

// 使用‘is’來從其他合約匯出。匯出合約將訪問所有非私有成員,
// 包括internal函式和狀態變數。這些通過this都不能被內部訪問。
contract mortal is owned {
      function kill() {
          if (msg.sender == owner)  selfdestruct(owner);
     }
}

// 這些抽象合約僅僅被提供用於使介面被編譯器知曉。注意該函式沒有函式體。
// 如果一個合約並不實現任何函式,它僅僅能被用於一個介面
contract Config {
    function lookup(uint id) public returns (address adr);
}

contract NameReg {
     function register(bytes32 name) public;
     function unregister() public;
}

// 多繼承是可以實現的。注意‘ownde’也是‘mortal’的一個基礎型別,但是這裡只有一個
// ‘owned‘ 的例項(相對於C++的虛擬繼承而言)
contract named is owned, mortal {
     constructor(bytes32 name) {
         Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
         NameReg(config.lokkup(1)).register(name):
     }

    // 函式可能因為另外一個同名且有相同數量/型別的函式而被覆蓋。
    // 如果覆蓋的函式由不同型別的輸出引數,那麼可能會引起error。
    // 無論本地呼叫和基於資訊的函式呼叫,都要將這一點納入考慮。
    function kill() public {
        if (msg.sender == owner) {
            Config comfig = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
            NameReg(config.lookup(1)).unregister();
            // 呼叫一個特定的覆蓋函式依然是可能的
            mortal.kill();
         }
    }
}

// 如果一個構造器引入一個引數,它必須被提供在頭部中
// (或者像修飾器一樣宣告匯出合約的構造器中)
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;

contract owned {
    constructor() public { owner = msg.sender; )
    address 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作為主要的匯出覆蓋(override),但這個函式將繞過Base1.kill,主要因為它甚至壓根不知道Base1.解決這個問題可以使用 super:

pragma solidity ^0.4.22;

contract owned {
     constructor() public { owner = msg.sender; }
     address 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時,在使用它的地方的上下文中是不知道實際呼叫的是哪個函式,儘管它的型別是知道的。這點和普通虛擬函式查詢(ordinary virtual method lookup)是相似的。

構造器(Constructors)
一個構造器是一種可以選擇的函式,通過 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的構造器將導致合約被標記為抽象的(abstract)(後面會講)。

Note:
在0.4.22之前,構造器被定義為一個和合約由相同名字的函式。這個語法現在已經被丟棄了。
(注意,網上大部分教程或程式還採用的這種語法)

pragma solidity ^0.4.11;

contract A { 
     uint public a;

     function A(uint _) internal {
         a = _a;
      }
}

contract B is A(1) {
    function B() public {}
}

基礎構造器的引數(Arguments for Base Constructor)

所有基礎合約的構造器都會被在下面介紹的初始化規則後被呼叫。如果基礎構造器有引數,匯出合約需要指定它們。可以用兩種方法來實現:

pragma solidity ^0.4.22;

contract Base {
      uint x;
      constructor(uint _x) public { x = _x; }
}

contract Derived1 is Base(7) {
      constructor(uint _y) public {}
}

contract Derived2 is Base {
       constructor(uint _y) Base(_y * _y) public {}
}

第一種方法是直接在繼承列表中直接指定(is Base(7))。另一個是通過一個修飾器,作為匯出合約的頭部的一部分(Base(_y * _y))。第一種方法在構造器引數是常數時,以及定義一個合約的行為或描述它時是更加方便的。第二個方法在基礎函式的構造器引數依賴於匯出函式時,只能這樣使用。引數必須通過要麼在繼承表中,要麼在匯出構造器的修飾器風格 的部分中給出。但是如果同時用兩種方法就會導致錯誤。

如果一個匯出合約並不指出所有基礎合約構造器的引數,他將會是一個抽象合約。

多繼承和線性化(Multiple Inheritance and Linearization)
允許多繼承的語言都必須面臨這幾個問題。一個是 Diamond 問題。Solidity和Python在使用”C3 線性化(C3 Linearization)”去強制生成基礎型別的有向無環圖(DAG)的一個特別順序這點上是相似的。這一點實現了我們想要的單一化的性質,但使得一些繼承圖變得不可行。特別是,在is指令中給出的基礎型別的順序變得及其重要:你必須按“從最接近基礎的”到“最外層匯出的”的順序列出直接的基礎合約。注意這個順序與Python中使用的順序不一樣。在下面的程式碼中,Solidity將給出錯誤“無法實現繼承圖的線性化(Linearization of inheritance graph impossible)”。

// this will not compile 

pragma solidity ^0.4.0;

contract X {} 
contract A is X {}
contract C is A,X {}

原因在於C要求X去覆蓋A(通過按 A,X 的定義順序),但A它本身要求覆蓋X,這就形成了一個無法解決的矛盾。

繼承同名函式的不同型別成員(Linearization of inheritance graph impossible Inheriting Different Kinds of Members of the Same Name)
當繼承作用與一個有函式的合約和一個同名的修飾器時,會被認為是一個錯誤。這個錯誤同樣也會因為一個事件和同名的修飾器,或者函式和同名的修飾器而引起。這個異常中,狀態變數的getter將會覆蓋public函式。

抽象合約(Abstract Contracts)

當一個合約的至少一個函式缺少實現(implementation),該函式將被標記為抽象函式,就像下面這樣(注意該函式的宣告頭部是以;結束的):

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";  }
}

如果一個合約從一個抽象合約繼承並且並不通過覆蓋實現將所有非實現函式,那麼它自身也是抽象的。
(我理解的非實現函式就是沒有函式體的函式,無法真正執行)

注意一個沒有實現的函式不同與一個(基本的)函式型別儘管他們語法十分相似。

沒有實現的函式例子(函式的宣告):

function foo(address) external returns (address);

函式型別的例子(一個變數的宣告,變數型別為function):

function(address) external returns (address) foo;

Abstract contracts decouple the definition of a contract from its implementation providing better extensibility and self-documentation and facilitating patterns like the Template method and removing code duplication. Abstract contracts are useful in the same way that defining methods in an interface is useful. It is a way for the designer of the abstract contract to say “any child of mine must implement this method”.
抽象合約將合約的定義和它的實現分離,以提供更好的可擴充套件性,自文件性,像Template method一樣便利的模板,以及移除程式碼副本。抽象函式和在介面定義函式一樣有用。這是抽象函式的設計者傳達“我的所有繼承者都必須實現這些方法”的手段。

介面(Interface)

介面和抽象合約很像,但它們不能由任何函式實現。以及還有更多的規則:

  1. 不能繼承其他的合約或者介面
  2. 不能定義構造器
  3. 不能定義變數
  4. 不能定義結構體
  5. 不能定義元組

未來也許還會提出更多的規範。

介面基本被限制為 ABI合約能夠表現什麼,而ABI和一個介面的對話應該可以在無資訊損失的條件下實現。

介面通過他們自己的關鍵字來標示:

pragma solidity ^0.4.11;

interface Token {
     function transfer(address recipient, uint amount) public;
}

合約能夠向繼承其他合約一樣繼承介面。

庫(Libraries)

庫近似於合約,但他們的目的是他們僅在特定的地址部署(deployed)一次,而他們的程式碼可以使用EVM的DELEGATECALL特性來複用(CALLCODEuntil Homestead)。
This means that if library functions are called, their code is executed in the context of the calling contract, i.e. this points to the calling contract, and especially the storage from the calling contract can be accessed這意味這一旦庫函式被呼叫,他們的程式碼將在呼叫合約的上環境中執行,比如說用this指向來呼叫合約,以及尤其是呼叫合約的storage能夠被訪問。(這句完全沒搞懂)
當一個庫是一個孤立的原始碼片,它僅能訪問呼叫合約的狀態變數,還要在他們被顯式提供的前提下(否則它連這些變數的名字都不知道)。如果庫函式不修改它們的狀態(比如當他們是 view 或 pure 函式),它們只能被直接呼叫(即不使用DELEGATECALL),因為庫被假定為無狀態的。特別的,除非Solidity類別系統被繞過,摧毀一個庫是不可行的、

庫可以看做使用它們的合約的一個隱式的基礎合約。它們不會在遺傳階級體系中顯式可見,但呼叫庫函式看起來就和呼叫顯式的基礎函式(如果L是庫名,呼叫為L.f())一樣。此外,庫的internal函式在所有合約中都可見,就像庫是一個基礎合約。當然,對internal函式的呼叫使用internal呼叫規範,這意味著所有internal型別的都能被傳遞,而且記憶體型別將會被通過引用傳遞而非複製。要在EVM中實現這一點,internal庫函式的程式碼和所有被從同一點呼叫的函式將在編譯時被放入呼叫合約,並採用一個常規的JUMP呼叫替代DELEGATECALL。

下面的例子闡明瞭怎樣使用庫(但注意閱讀後面的using for 部分來得到更多更進一步的關於實現一個集合的例子):

pragma solidity ^0.4.22;

library Set {
    // 我們定義了一個新的結構資料型別,它將被用於在呼叫合約中儲存其資料
    struct Data { mapping(uint => bool) flags; }

    // 注意下面的第一個引數是“記憶體引用”型別,而且因此僅僅只有它的storage地址
    // 而非它的內容被作為呼叫的一部分傳遞過去。如果該函式能被視為其例項的一種方法
    // 一般慣例將第一個引數稱為“self”
    function insert(Data storage self, uint value)
         public 
         returns (bool)
    {
         if (self.flags[value])
            return false;
         self.flags[value] = true;
         return true;
     }   

     function remove(Data storage self, uint value)
          public 
          returns (bool)
     {
          if (!self.flags[value])
                return false;
          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 {
         // 庫函式能夠不需要一個庫的特定示例就可以呼叫,
         // 因為這個“例項”就是當前合約
         require(Set.insert(KnownValues, value));
     }
     // 在這個合約中,如果我們想也可以直接訪問knownValues.flags
}

當然,我們不必要非要遵從這個方式來使用庫:他們也同樣能在不定義結構資料型別Data的情況下使用。函式同樣可以在沒有任何storage索引引數的情況下執行,而且他們也能夠有多種storage索引引數,而且可以在任何地方。

呼叫指令Set.contains,Set.insertSet.remove都是被編譯為對外部合約/庫的呼叫(DELEGATECALL)。如果你使用庫,注意這裡是使用了一個實際的外部函式呼叫,儘管msg.sender,msg.valuethis這些指令在呼叫時將保持他們的值。

下面的例子展示了為了實現常見的型別而不經常使用外部函式呼叫,怎樣在庫中使用memory型別以及internal函式:

pragma solidity ^0.4.16;

library BigInt {
    struct bigint {
         uint[] limbs;
    }

    function fromUint(uint x) internal pure returns (bigint r)  {
          r.limbs = new uing[](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) {
              // too bad, we have to add a limb
              uint[] memory newLimbs = new uint[](r.limbs.length + 1);
              for ( i = 0; i < r.limbs.length; ++i)
                    newLimbs[i] = r.limbs[i];