Solidity 簡易教程0x001

Solidity是以太坊的主要編程語言,它是一種靜態類型的 JavaScript-esque 語言,是面向合約的、爲實現智能合約而創建的高級編程語言,設計的目的是能在以太坊虛擬機(EVM)上運行。

本文基於CryptoZombies,教程地址爲:https://cryptozombies.io/zh/lesson/2

地址(address)

以太坊區塊鏈由 account (賬戶)組成,你可以把它想象成銀行賬戶。一個帳戶的餘額是以太 (在以太坊區塊鏈上使用的幣種),你可以和其他帳戶之間支付和接受以太幣,就像你的銀行帳戶可以電匯資金到其他銀行帳戶一樣。

每個帳戶都有一個“地址”,你可以把它想象成銀行賬號。這是賬戶唯一的標識符,它看起來長這樣:

0x0cE446255506E92DF41614C46F1d6df9Cc969183
這是 CryptoZombies 團隊的地址,爲了表示支持CryptoZombies,可以讚賞一些以太幣!

address:地址類型存儲一個 20 字節的值(以太坊地址的大小)。 地址類型也有成員變量,並作爲所有合約的基礎。

address 類型是一個160位的值,且不允許任何算數操作。這種類型適合存儲合約地址或外部人員的密鑰對。

映射(mapping)

Mappings 和哈希表類似,它會執行虛擬初始化,以使所有可能存在的鍵都映射到一個字節表示爲全零的值。

映射是這樣定義的:

//對於金融應用程序,將用戶的餘額保存在一個 uint類型的變量中:
mapping (address => uint) public accountBalance;
//或者可以用來通過userId 存儲/查找的用戶名
mapping (uint => string) userIdToName;

映射本質上是存儲和查找數據所用的鍵-值對。在第一個例子中,鍵是一個 address,值是一個 uint,在第二個例子中,鍵是一個uint,值是一個 string。

映射類型在聲明時的形式爲 mapping(_KeyType => _ValueType)。 其中 _KeyType 可以是除了映射、變長數組、合約、枚舉以及結構體以外的幾乎所有類型。 _ValueType 可以是包括映射類型在內的任何類型。

對映射的取值操作如下:

userIdToName[12]
// 如果鍵12 不在 映射中,得到的結果是0
映射中,實際上並不存儲 key,而是存儲它的 keccak256 哈希值,從而便於查詢實際的值。所以映射是沒有長度的,也沒有 key 的集合或 value 的集合的概念。,你不能像操作python字典那應該獲取到當前 Mappings 的所有鍵或者值。

特殊變量

在 Solidity 中,在全局命名空間中已經存在了(預設了)一些特殊的變量和函數,他們主要用來提供關於區塊鏈的信息或一些通用的工具函數。

msg.sender

msg.sender指的是當前調用者(或智能合約)的 address。

注意:在 Solidity 中,功能執行始終需要從外部調用者開始。 一個合約只會在區塊鏈上什麼也不做,除非有人調用其中的函數。所以對於每一個外部函數調用,包括 msg.sender 和 msg.value 在內所有 msg 成員的值都會變化。這裏包括對庫函數的調用。

以下是使用 msg.sender 來更新 mapping 的例子:

mapping (address => uint) favoriteNumber;

function setMyNumber(uint _myNumber) public {
  // 更新我們的 `favoriteNumber` 映射來將 `_myNumber`存儲在 `msg.sender`名下
  favoriteNumber[msg.sender] = _myNumber;
  // 存儲數據至映射的方法和將數據存儲在數組相似
}

function whatIsMyNumber() public view returns (uint) {
  // 拿到存儲在調用者地址名下的值
  // 若調用者還沒調用 setMyNumber, 則值爲 `0`
  return favoriteNumber[msg.sender];
}

在這個小小的例子中,任何人都可以調用 setMyNumber 在我們的合約中存下一個 uint 並且與他們的地址相綁定。 然後,他們調用 whatIsMyNumber 就會返回他們存儲的 uint。

使用 msg.sender 很安全,因爲它具有以太坊區塊鏈的安全保障 —— 除非竊取與以太坊地址相關聯的私鑰,否則是沒有辦法修改其他人的數據的。

以下是其它的一些特殊變量。

區塊和交易屬性

  • block.blockhash(uint blockNumber) returns (bytes32):指定區塊的區塊哈希——僅可用於最新的 256 個區塊且不包括當前區塊;而 blocks 從 0.4.22 版本開始已經不推薦使用,由 blockhash(uint blockNumber) 代替
  • block.coinbase (address): 挖出當前區塊的礦工地址
  • block.difficulty (uint): 當前區塊難度
  • block.gaslimit (uint): 當前區塊 gas 限額
  • block.number (uint): 當前區塊號
  • block.timestamp (uint): 自 unix epoch 起始當前區塊以秒計的時間戳
  • gasleft() returns (uint256):剩餘的 gas
  • msg.data (bytes): 完整的 calldata
  • msg.gas (uint): 剩餘 gas - 自 0.4.21 版本開始已經不推薦使用,由 gesleft() 代替
  • msg.sender (address): 消息發送者(當前調用)
  • msg.sig (bytes4): calldata 的前 4 字節(也就是函數標識符)
  • msg.value (uint): 隨消息發送的 wei 的數量
  • now (uint): 目前區塊時間戳(block.timestamp)
  • tx.gasprice (uint): 交易的 gas 價格
  • tx.origin (address): 交易發起者(完全的調用鏈)

錯誤處理

Solidity 使用狀態恢復異常來處理錯誤。這種異常將撤消對當前調用(及其所有子調用)中的狀態所做的所有更改,並且還向調用者標記錯誤。

函數 assertrequire 可用於檢查條件並在條件不滿足時拋出異常。

  • assert 函數只能用於測試內部錯誤,並檢查非變量。
  • require 函數用於確認條件有效性,例如輸入變量,或合約狀態變量是否滿足條件,或驗證外部合約調用返回的值。

這裏主要介紹 require

require使得函數在執行過程中,當不滿足某些條件時拋出錯誤,並停止執行:

function sayHiToVitalik(string _name) public returns (string) {
  // 比較 _name 是否等於 "Vitalik". 如果不成立,拋出異常並終止程序
  // (敲黑板: Solidity 並不支持原生的字符串比較, 我們只能通過比較
  // 兩字符串的 keccak256 哈希值來進行判斷)
  require(keccak256(_name) == keccak256("Vitalik"));
  // 如果返回 true, 運行如下語句
  return "Hi!";
}

如果你這樣調用函數 sayHiToVitalik("Vitalik") ,它會返回“Hi!”。而如果調用的時候使用了其他參數,它則會拋出錯誤並停止執行。

因此,在調用一個函數之前,用 require 驗證前置條件是非常有必要的。

注意:在 Solidity 中,關鍵詞放置的順序並不重要
// 以下兩個語句等效
require(keccak256(_name) == keccak256("Vitalik"));
require(keccak256("Vitalik") == keccak256(_name));

外/內部函數

除 public 和 private 屬性之外,Solidity 還使用了另外兩個描述函數可見性的修飾詞:internal(內部) 和 external(外部)。

internalprivate 類似,不過,如果某個合約繼承自其父合約,這個合約即可以訪問父合約中定義的“內部(internal)”函數

externalpublic 類似,只不過external函數只能在合約之外調用 - 它們不能被合約內的其他函數調用。

聲明函數 internal 或 external 類型的語法,與聲明 private 和 public類 型相同:

contract Sandwich {
  uint private sandwichesEaten = 0;

  function eat() internal {
    sandwichesEaten++;
  }
}

contract BLT is Sandwich {
  uint private baconSandwichesEaten = 0;

  function eatWithBacon() public returns (string) {
    baconSandwichesEaten++;
    // 因爲eat() 是internal 的,所以我們能在這裏調用
    eat();
  }
}

Solidity 有兩種函數調用(內部調用不會產生實際的 EVM 調用或稱爲消息調用,而外部調用則會產生一個 EVM 調用), 函數和狀態變量有四種可見性類型。 函數可以指定爲 external ,public ,internal 或者 private,默認情況下函數類型爲 public。 對於狀態變量,不能設置爲 external ,默認是 internal 。

  • external :

外部函數作爲合約接口的一部分,意味着我們可以從其他合約和交易中調用。 一個外部函數 f 不能從內部調用(即 f 不起作用,但 this.f() 可以)。 當收到大量數據的時候,外部函數有時候會更有效率。

  • public :

public 函數是合約接口的一部分,可以在內部或通過消息調用。對於公共狀態變量, 會自動生成一個 getter 函數。

  • internal :

這些函數和狀態變量只能是內部訪問(即從當前合約內部或從它派生的合約訪問),不使用 this 調用。

  • private :

private 函數和狀態變量僅在當前定義它們的合約中使用,並且不能被派生合約使用。

合約中的所有內容對外部觀察者都是可見的。設置一些 private 類型只能阻止其他合約訪問和修改這些信息, 但是對於區塊鏈外的整個世界它仍然是可見的。

可見性標識符的定義位置,對於狀態變量來說是在類型後面,對於函數是在參數列表和返回關鍵字中間。

pragma solidity ^0.4.16;

contract C {
    // 對於函數是在參數列表和返回關鍵字中間。
    function f(uint a) private pure returns (uint b) { return a + 1; }
    function setData(uint a) internal { data = a; }
    uint public data;  // 對於狀態變量來說是在類型後面
}

函數多值返回

和 python 類似,Solidity 函數支持多值返回,比如:


function multipleReturns() internal returns(uint a, uint b, uint c) {
  return (1, 2, 3);
}

function processMultipleReturns() external {
  uint a;
  uint b;
  uint c;
  // 這樣來做批量賦值:
  (a, b, c) = multipleReturns();
}

// 或者如果我們只想返回其中一個變量:
function getLastReturnValue() external {
  uint c;
  // 可以對其他字段留空:
  (,,c) = multipleReturns();
}
這裏留空字段使用,的方式太不直觀了,還不如 python/go 使用下劃線_代替無用字段。

Storage與Memory

在 Solidity 中,有兩個地方可以存儲變量 —— storage 或 memory。

Storage 變量是指永久存儲在區塊鏈中的變量。 Memory 變量則是臨時的,當外部函數對某合約調用完成時,內存型變量即被移除。 你可以把它想象成存儲在你電腦的硬盤或是RAM中數據的關係。

storage 和 memory 放到狀態變量名前邊,在類型後邊,格式如下:
變量類型 <storage|memory> 變量名

大多數時候都用不到這些關鍵字,默認情況下 Solidity 會自動處理它們。 狀態變量(在函數之外聲明的變量)默認爲“存儲”形式,並永久寫入區塊鏈;而在函數內部聲明的變量是“內存”型的,它們函數調用結束後消失。

然而也有一些情況下,你需要手動聲明存儲類型,主要用於處理函數內的 結構體數組 時:

contract SandwichFactory {
  struct Sandwich {
    string name;
    string status;
  }

  Sandwich[] sandwiches;

  function eatSandwich(uint _index) public {
    // Sandwich mySandwich = sandwiches[_index];

    // ^ 看上去很直接,不過 Solidity 將會給出警告
    // 告訴你應該明確在這裏定義 `storage` 或者 `memory`。

    // 所以你應該明確定義 `storage`:
    Sandwich storage mySandwich = sandwiches[_index];
    // ...這樣 `mySandwich` 是指向 `sandwiches[_index]`的指針
    // 在存儲裏,另外...
    mySandwich.status = "Eaten!";
    // ...這將永久把 `sandwiches[_index]` 變爲區塊鏈上的存儲

    // 如果你只想要一個副本,可以使用`memory`:
    Sandwich memory anotherSandwich = sandwiches[_index + 1];
    // ...這樣 `anotherSandwich` 就僅僅是一個內存裏的副本了
    // 另外
    anotherSandwich.status = "Eaten!";
    // ...將僅僅修改臨時變量,對 `sandwiches[_index + 1]` 沒有任何影響
    // 不過你可以這樣做:
    sandwiches[_index + 1] = anotherSandwich;
    // ...如果你想把副本的改動保存回區塊鏈存儲
  }
}

如果你還沒有完全理解究竟應該使用哪一個,也不用擔心 —— 在本教程中,我們將告訴你何時使用 storage 或是 memory,並且當你不得不使用到這些關鍵字的時候,Solidity 編譯器也發警示提醒你的。

現在,只要知道在某些場合下也需要你顯式地聲明 storage 或 memory就夠了!

繼承

Solidity 的繼承和 Python 的繼承相似,支持多重繼承。
看下面這個例子:

contract Doge {
  function catchphrase() public returns (string) {
    return "So Wow CryptoDoge";
  }
}

contract BabyDoge is Doge {
  function anotherCatchphrase() public returns (string) {
    return "Such Moon BabyDoge";
  }
}

// 可以多重繼承。請注意,Doge 也是 BabyDoge 的基類,
// 但只有一個 Doge 實例(就像 C++ 中的虛擬繼承)。
contract BlackBabyDoge is Doge, BabyDoge {
  function color() public returns (string) {
    return "Black";
  }
}

BabyDogeDoge 那裏 inherits(繼承)過來。 這意味着當編譯和部署了 BabyDoge,它將可以訪問 catchphrase() 和 anotherCatchphrase()和其他我們在 Doge 中定義的其他公共函數(private 函數不可訪問)。

Solidity使用 is 從另一個合約派生。派生合約可以訪問所有非私有成員,包括內部函數和狀態變量,但無法通過 this 來外部訪問。

基類構造函數的參數

派生合約需要提供基類構造函數需要的所有參數。這可以通過兩種方式來完成:

pragma solidity ^0.4.0;

contract Base {
    uint x;
    // 這是註冊 Base 和設置名稱的構造函數。
    function Base(uint _x) public { x = _x; }
}

contract Derived is Base(7) {
    function Derived(uint _y) Base(_y * _y) public {
    }
}

contract Derived1 is Base {
    function Derived1(uint _y) Base(_y * _y) public {
    }
}

一種方法直接在繼承列表中調用基類構造函數(is Base(7))。 另一種方法是像 修飾器 modifier 使用方法一樣, 作爲派生合約構造函數定義頭的一部分,(Base(_y * _y))。 如果構造函數參數是常量並且定義或描述了合約的行爲,使用第一種方法比較方便。 如果基類構造函數的參數依賴於派生合約,那麼必須使用第二種方法。 如果像這個簡單的例子一樣,兩個地方都用到了,優先使用 修飾器modifier 風格的參數。

抽象合約

合約函數可以缺少實現,如下例所示(請注意函數聲明頭由 ; 結尾):

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

如果合約繼承自抽象合約,並且沒有通過重寫來實現所有未實現的函數,那麼它本身就是抽象的。

接口(Interface)

接口類似於抽象合約,但是它們不能實現任何函數。還有進一步的限制:

  • 無法繼承其他合約或接口。
  • 無法定義構造函數。
  • 無法定義變量。
  • 無法定義結構體
  • 無法定義枚舉。

首先,看一下一個interface的例子:


contract NumberInterface {
  function getNum(address _myAddress) public view returns (uint);
}

請注意,這個過程雖然看起來像在定義一個合約,但其實內裏不同:

  • 首先,只聲明瞭要與之交互的函數 —— 在本例中爲 getNum —— 在其中沒有使用到任何其他的函數或狀態變量。
  • 其次,並沒有使用大括號({ 和 })定義函數體,單單用分號(;)結束了函數聲明。這使它看起來像一個合約框架。

編譯器就是靠這些特徵認出它是一個接口的。

就像繼承其他合約一樣,合約可以繼承接口。

可以在合約中這樣使用接口:

contract MyContract {
  address NumberInterfaceAddress = 0xab38...;
  // ^ 這是FavoriteNumber合約在以太坊上的地址
  NumberInterface numberContract = NumberInterface(NumberInterfaceAddress);
  // 現在變量 `numberContract` 指向另一個合約對象

  function someFunction() public {
    // 現在我們可以調用在那個合約中聲明的 `getNum`函數:
    uint num = numberContract.getNum(msg.sender);
    // ...在這兒使用 `num`變量做些什麼
  }
}

通過這種方式,只要將合約的可見性設置爲public(公共)或external(外部),它們就可以與以太坊區塊鏈上的任何其他合約進行交互。

與其他合約的交互

如果一個合約需要和區塊鏈上的其他的合約會話,則需先定義一個 interface (接口)。

先舉一個簡單的栗子。 假設在區塊鏈上有這麼一個合約:

contract LuckyNumber {
  mapping(address => uint) numbers;

  function setNum(uint _num) public {
    numbers[msg.sender] = _num;
  }

  function getNum(address _myAddress) public view returns (uint) {
    return numbers[_myAddress];
  }
}

這是個很簡單的合約,可以用它存儲自己的幸運號碼,並將其與調用者的以太坊地址關聯。 這樣其他人就可以通過地址查找幸運號碼了。

現在假設我們有一個外部合約,使用 getNum 函數可讀取其中的數據。

首先,我們定義 LuckyNumber 合約的 interface :


contract NumberInterface {
  function getNum(address _myAddress) public view returns (uint);
}

使用這個接口,合約就知道其他合約的函數是怎樣的,應該如何調用,以及可期待什麼類型的返回值。

下面是一個示例代碼,會用到上邊的知識點:

pragma solidity ^0.4.19;

contract ZombieFactory {

    event NewZombie(uint zombieId, string name, uint dna);

    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;

    struct Zombie {
        string name;
        uint dna;
    }

    Zombie[] public zombies;

    // 創建一個叫做 zombieToOwner 的映射。其鍵是一個uint,值爲 address。映射屬性爲public
    mapping (uint => address) public zombieToOwner;
    // 創建一個名爲 ownerZombieCount 的映射,其中鍵是 address,值是 uint
    mapping (address => uint) ownerZombieCount;

    function _createZombie(string _name, uint _dna) private {
        uint id = zombies.push(Zombie(_name, _dna)) - 1;
        zombieToOwner[id] = msg.sender;
        ownerZombieCount[msg.sender]++;
        NewZombie(id, _name, _dna);
    }

    function _generateRandomDna(string _str) private view returns (uint) {
        uint rand = uint(keccak256(_str));
        return rand % dnaModulus;
    }

    function createRandomZombie(string _name) public {
        // 我們使用了 require 來確保這個函數只有在每個用戶第一次調用它的時候執行,用以創建初始殭屍
        require(ownerZombieCount[msg.sender] == 0);
        uint randDna = _generateRandomDna(_name);
        _createZombie(_name, randDna);
    }

}

// CryptoKitties 合約提供了getKitty 函數,它返回所有的加密貓的數據,包括它的“基因”(殭屍遊戲要用它生成新的殭屍)。
// 一個獲取 kitty 的接口
contract KittyInterface {
  
  // 在interface裏定義了 getKitty 函數 在 returns 語句之後用分號
  function getKitty(uint256 _id) external view returns (
    bool isGestating,
    bool isReady,
    uint256 cooldownIndex,
    uint256 nextActionAt,
    uint256 siringWithId,
    uint256 birthTime,
    uint256 matronId,
    uint256 sireId,
    uint256 generation,
    uint256 genes
  );
}

//ZombieFeeding繼承自 `ZombieFactory 合約
contract ZombieFeeding is ZombieFactory {
  
  // CryptoKitties 合約的地址
  address ckAddress = 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d;
  // 創建一個名爲 kittyContract 的 KittyInterface,並用 ckAddress 爲它初始化 
  KittyInterface kittyContract = KittyInterface(ckAddress);
  
  function feedAndMultiply(uint _zombieId, uint _targetDna, string _species) public {
      // 確保對自己殭屍的所有權
      require(msg.sender == zombieToOwner[_zombieId]);
      // 聲明一個名爲 myZombie 數據類型爲Zombie的 storage 類型本地變量
      Zombie storage myZombie = zombies[_zombieId];
      _targetDna = _targetDna % dnaModulus;
      uint newDna = (myZombie.dna + _targetDna) / 2;
      // Add an if statement here
      if (keccak256(_species) == keccak256("kitty")){
          newDna = newDna - newDna%100 + 99;
      }
      _createZombie("NoName", newDna);
  }
  
  function feedOnKitty(uint _zombieId, uint _kittyId) public {
    uint kittyDna;
    // 多值返回,這裏只需要最後一個值
    (,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
    feedAndMultiply(_zombieId, kittyDna, "kitty");
  }
}
這段代碼看起來內容有點多,可以拆分一下,把 ZombieFactory代碼提取到一個新的文件zombiefactory.sol,現在就可以使用 import 語句來導入另一個文件的代碼。

import

在 Solidity 中,當你有多個文件並且想把一個文件導入另一個文件時,可以使用 import 語句:


import "./someothercontract.sol";

contract newContract is SomeOtherContract {

}

這樣當我們在合約(contract)目錄下有一個名爲 someothercontract.sol 的文件( ./ 就是同一目錄的意思),它就會被編譯器導入。

這一點和 go 類似,在同一目錄下文件中的內容可以直接使用,而不用使用 xxx.name 的形式。

測試調用

編譯和部署 ZombieFeeding,就可以將這個合約部署到以太坊了。最終完成的這個合約繼承自 ZombieFactory,因此它可以訪問自己和父輩合約中的所有 public 方法。

下面是一個與ZombieFeeding合約進行交互的例子, 這個例子使用了 JavaScript 和 web3.js:

var abi = /* abi generated by the compiler */
var ZombieFeedingContract = web3.eth.contract(abi)
var contractAddress = /* our contract address on Ethereum after deploying */
var ZombieFeeding = ZombieFeedingContract.at(contractAddress)

// 假設我們有我們的殭屍ID和要攻擊的貓咪ID
let zombieId = 1;
let kittyId = 1;

// 要拿到貓咪的DNA,我們需要調用它的API。這些數據保存在它們的服務器上而不是區塊鏈上。
// 如果一切都在區塊鏈上,我們就不用擔心它們的服務器掛了,或者它們修改了API,
// 或者因爲不喜歡我們的殭屍遊戲而封殺了我們
let apiUrl = "https://api.cryptokitties.co/kitties/" + kittyId
$.get(apiUrl, function(data) {
  let imgUrl = data.image_url
  // 一些顯示圖片的代碼
})

// 當用戶點擊一隻貓咪的時候:
$(".kittyImage").click(function(e) {
  // 調用我們合約的 `feedOnKitty` 函數
  ZombieFeeding.feedOnKitty(zombieId, kittyId)
})

// 偵聽來自我們合約的新殭屍事件好來處理
ZombieFactory.NewZombie(function(error, result) {
  if (error) return
  // 這個函數用來顯示殭屍:
  generateZombie(result.zombieId, result.name, result.dna)
})

參考鏈接


最後,感謝女朋友支持和包容,比❤️

也可以在公號輸入以下關鍵字獲取歷史文章:公號&小程序 | 設計模式 | 併發&協程

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章