Solidity之旅(十三)函式及其可見性和狀態可變性

BSN研習社發表於2023-12-21
01
狀態變數可見性
在這之前的文章裡,給出的例子中,宣告的狀態變數都修飾為public,因為我們將狀態變數宣告為public後,Solidity編譯器自動會為我們生成一個與狀態變數同名的、且函式可見性為public的函式!
在Solidity中,除了可以將狀態變數修飾為public,還可以修飾為另外兩種:internal、private。
  • public
對於public狀態變數會自動生成一個,與狀態變數同名的public修飾的函式。以便其他的合約讀取他們的值。當在用一個合約裡使用是,外部方式訪問(如:this.x)會呼叫該自動生成的同名函式,而內部方式訪問(如:x)會直接從儲存中獲取值。Setter函式則不會被生成,所以其他合約不能直接修改其值。
  • internal
內部可見性狀態變數只能在它們所定義的合約和派生合同中訪問,它們不能被外部訪問。這是狀態變數的預設可見性。
  • private
私有狀態變數就像內部變數一樣,但它們在派生合約中是不可見的。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract stateVarsVisible {
   uint public num;
   function showNum() public returns(uint){
      num += 1;
      return num;
   }
}
contract outsideCall {
   function myCall() public returns(uint){
      //例項化合約
      stateVarsVisible sv = new stateVarsVisible();
      //呼叫 getter 函式
      return sv.num();
   }
}
undefined

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract stateVarsVisible {
   uint internal num;
   function showNum() public returns(uint){
      num += 1;
      return num;
   }
   function fn() external returns(uint){
      return num;
   }
}
contract sub is stateVarsVisible {
   function myNum() public returns(uint){
      return stateVarsVisible.num;
   }
}
contract outsideCall {
   function myCall() public returns(uint){
      //例項化合約
      stateVarsVisible sv = new stateVarsVisible();
      //外部合約 不能訪問
      //return sv.num();
   }
}
undefined

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract stateVarsVisible {
   uint private num;
   function showNum() public returns(uint){
      num += 1;
      return num;
   }
   function fn() external returns(uint){
      return num;
   }
}
contract sub is stateVarsVisible {
   function myNum() public returns(uint){
   //派生合約 無法訪問 基合約 的狀態變數
      return stateVarsVisible.num;
   }
}
undefined

02
函式可見性
前面的文章,我們多多少少有見到在函式引數列表後的一些關鍵字,那便是函式可見性修飾符。對於函式可見性這一概念,有過現代程式語言的經歷大都知曉,諸如,public(公開的)、private(私有的)、protected(受保護的)用來修飾函式的可見性,Java、PHP`等便是使用這些關鍵字來修飾函式的可見性。
當然咯,Solidity函式對外可訪問也做了修飾,分為以下4種可見性:
  • external
外部可見性函式作為合約介面的一部分,意味著我們可以從其他合約和交易中呼叫。一個外部函式f不能從內部呼叫(即f不起作用,但this.f()可以)。
  • public
public函式是合約介面的一部分,可以在內部或透過訊息呼叫。
  • internal
內部可見性函式訪問可以在當前合約或派生的合約訪問,外部不可以訪問。由於它們沒有透過合約的ABI向外部公開,它們可以接受內部可見性型別的引數:比如對映或儲存引用。
  • private
private函式和狀態變數僅在當前定義它們的合約中使用,並且不能被派生合約使用。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract FunctionVisible {
   uint private num;
   function privateFn(uint x) private returns(uint y){ y = x + 5; }
   function setNum(uint x) public { num = x;}
   function getNum() public returns(uint){ return num; }
   function sum(uint x,uint y) internal returns(uint) { return x + y; }
   function showPri(uint x) external returns(uint){ x += num; return privateFn(x); }
}
contract Outside {
   function myCall() public {
      FunctionVisible fv = new FunctionVisible();
      uint res = fv.privateFn(7); // 錯誤:privateFn 函式是私有的
      fv.setNum(4);
      res = fv.getNum();
      res = fv.sum(3,4); // 錯誤:sum 函式是 內部的
    
   }
}
undefined

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract FunctionVisible {
   uint private num;
   function privateFn(uint x) private view returns(uint y){ y = x + num; }
   function setNum(uint x) public { num = x;}
   function getNum() public  view returns(uint){ return num; }
   function sum(uint x,uint y) internal pure returns(uint) { return x + y; }
   function showPri(uint x) external view returns(uint){ x += num; return privateFn(x); }
}
contract Sub is FunctionVisible {
   function myTest() public pure returns(uint) {
        uint val = sum(3, 5); // 訪問內部成員(從繼承合約訪問父合約成員)
        val = privateFn(6);  //privateFn函式是私有的,即便是派生合約也不能訪問
        return val;
    }
}
undefined

getter函式具有外部(external)可見性。如果在內部訪問getter(即沒有this.),它被認為一個狀態變數。如果使用外部訪問(即用this.),它被認作為一個函式。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract C {
    uint public data;
    function x() public returns(uint) {
        data = 3; // 內部訪問
        uint val = this.data(); // 外部訪問
        return val;
    }
}
undefined

如果你有一個陣列型別的public狀態變數,那麼你只能透過生成的getter函式訪問陣列的單個元素。這個機制以避免返回整個陣列時的高成本gas。可以使用如myArray(0)用於指定引數要返回的單個元素。如果要在一次呼叫中返回整個陣列,則需要寫一個函式,例如:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract C {
 
  uint[] public myArray;
  // 自動生成的Getter 函式
  /*
  function myArray(uint i) public view returns (uint) {
      return myArray[i];
  }
  */
  // 返回整個陣列
  function getArray() public view returns (uint[] memory) {
      return myArray;
  }
}
undefined

03
合約之外的函式
在Solidity0.7.0版本之後,便可以將函式定義在合約之外,我們稱這種函式為“自由函式”,其函式可見性始終隱式地為internal,它們的程式碼包含在所有呼叫它們的合約中,類似於後續會講到的庫函式。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
function sum(uint[] memory arr) pure returns (uint s) {
    for (uint i = 0; i < arr.length; i++)
        s += arr[i];
}
contract ArrayExample {
    bool found;
    function f(uint[] memory arr) public {
        //編譯器會將 合約外函式的程式碼新增到這裡
        uint s = sum(arr);
        require(s >= 10); //後續會講到
        found = true;
    }
}
undefined

在合約之外定義的函式仍然在合約的上下文內執行。它們仍然可以呼叫其他合約,將其傳送以太幣或銷燬呼叫它們的合約等其他事情。與在合約中定義的函式的主要區別為:自由函式不能直接訪問儲存變數this、儲存和不在他們的作用域範圍內函式。
04
函式引數與返回值
與其它程式語言一樣,函式可能接受引數作為輸入。但Solidity和golang一樣,函式可以返回任意數量的值作為輸出。
1、函式入參
函式的引數變數這一點倒是與宣告變數是一樣的,如果未能使用到的引數可以省略引數名。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract Simple {
    uint sum;
    function taker(uint a, uint b) public {
        sum = a + b;
    }
}
undefined

2、返回值
Solidity函式返回值與golang函式返回很類似,只不過,Solidity使用returns關鍵字將返回引數宣告在小括號內。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract Simple {
    function arithmetic(uint a, uint b) public pure returns (uint sum, uint product)
    {
        sum = a + b;
        product = a * b;
    }
}
返回變數名可以被省略。返回變數可以當作為函式中的區域性變數,沒有顯式設定的話,會使用:ref:預設值<default-value>返回變數可以顯式給它附一個值(像上面),也可以使用return語句指定,使用return語句可以一個或多個值。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract Simple {
    function arithmetic(uint a, uint b)
        public
        pure
        returns (uint , uint )
    {
        return (a + b, a * b);
    }
}
05
狀態可變性
view
我們在先前的文章會看到,有些函式在修飾為public後,有多了view修飾的。而函式使用了view修飾,說明這個函式不能修改狀態變數(StateVariable),只能獲取狀態變數的值,由於view修飾的函式不能修改儲存在區塊鏈上的狀態變數,這種函式的gasfee不會很高,畢竟呼叫函式也會消耗gasfee。
而以下情況被認為是修改狀態的:
1.修改狀態變數。
2.產生事件。
3.建立其它合約。
4.使用selfdestruct。
5.透過呼叫傳送以太幣。
6.呼叫任何沒有標記為view或者pure的函式。
7.使用低階呼叫。
8.使用包含特定操作碼的內聯彙編。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract Simple {
    function f(uint a, uint b) public view returns (uint) {
        return a * (b + 42) + block.timestamp;
    }
}
undefined

Solidity0.5.0移除了view的別名constant。
Getter方法自動被標記為view。
pure
若將函式宣告為pure的話,那麼該函式是既不能讀取也不能修改狀態變數(StateVariable)。
除了上面解釋的狀態修改語句列表之外,以下被認為是讀取狀態:
1.讀取狀態變數。
2.訪問address(this).balance
或者<address>.balance。
3.訪問block,tx,msg中任意成員(除msg.sig和msg.data之外)。
4.呼叫任何未標記為pure的函式。
5.使用包含某些操作碼的內聯彙編。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract Simple {
    function f(uint a, uint b) public pure returns (uint) {
        return a * (b + 42);
    }
}
undefined

06
特別的函式
receive接收以太函式
一個合約最多有一個receive函式,宣告函式為:
receive()externalpayable{...}
不需要function關鍵字,也沒有引數和返回值並且必須是external可見性和payable修飾.它可以是virtual的,可以被過載也可以有後續會講到的修改器modifier。
在對合約沒有任何附加資料呼叫(通常是對合約轉賬)是會執行receive函式。例如透過.send()或.transfer(),如果receive函式不存在,但是有payable的接下來會講到的fallback回退函式那麼在進行純以太轉賬時,fallback函式會呼叫。
如果兩個函式都沒有,這個合約就沒法透過常規的轉賬交易接收以太(會丟擲異常)。
更糟的是,receive函式可能只有2300gas可以使用(如,當使用send或transfer時),除了基礎的日誌輸出之外,進行其他操作的餘地很小。下面的操作消耗會操作2300gas:
  • 寫入儲存
  • 建立合約
  • 呼叫消耗大量gas的外部函式
  • 傳送以太幣
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract Simple {
    event Received(address, uint);
    receive() external payable {
        emit Received(msg.sender, msg.value);
    }
}
undefined

Fallback回退函式
合約可以最多有一個回退函式。函式宣告為:
fallback()external[payable]
fallback(bytescalldatainput)external[payable]returns(bytesmemoryoutput)。
沒有function關鍵字。必須是external可見性,它可以是virtual的,可以被過載也可以有後續會講到的修改器modifier。
如果在一個對合約呼叫中,沒有其他函式與給定的函式識別符號匹配,fallback會被呼叫。或者在沒有receive函式時,而沒有提供附加資料對合約呼叫,那麼fallback函式會被執行。
fallback函式始終會接收資料,但為了同時接收以太時,必須標記為payable。
如果使用了帶引數的版本,input將包含傳送到合約的完整資料(等於msg.data),並且透過output返回資料。返回資料不是ABI編碼過的資料,相反,它返回不經過修改的資料。
更糟的是,如果回退函式在接收以太時呼叫,可能只有2300gas可以使用。
與任何其他函式一樣,只要有足夠的gas傳遞給它,回退函式就可以執行復雜的操作。
payable的fallback函式也可以在pure·以太轉賬的時候執行,如果沒有receive以太函式推薦總是定義一個receive函式,而不是定義一個payable的fallback函式,
如果想要解碼輸入資料,那麼前四個位元組用作函式選擇器,然後用abi.decode與陣列切片語法一起使用來解碼ABI編碼的資料:
(c, d) = abi.decode(_input[4:], (uint256, uint256));
請注意,這僅應作為最後的手段,而應使用對應的函式。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract Test {
    // 傳送到這個合約的所有訊息都會呼叫此函式(因為該合約沒有其它函式)。
    // 向這個合約傳送以太幣會導致異常,因為 fallback 函式沒有 `payable` 修飾符
    fallback() external { x = 1; }
    uint x;
}
// 這個合約會保留所有傳送給它的以太幣,沒有辦法返還。
contract TestPayable {
    uint x;
    uint y;
    // 除了純轉賬外,所有的呼叫都會呼叫這個函式.
    // (因為除了 receive 函式外,沒有其他的函式).
    // 任何對合約非空calldata 呼叫會執行回退函式(即使是呼叫函式附加以太).
    fallback() external payable { x = 1; y = msg.value; }
    // 純轉賬呼叫這個函式,例如對每個空empty calldata的呼叫
    receive() external payable { x = 2; y = msg.value; }
}
contract Caller {
    function callTest(Test test) public returns (bool) {
        (bool success,) = address(test).call(abi.encodeWithSignature("nonExistingFunction()"));
        require(success);
        //  test.x 結果變成 == 1。
        // address(test) 不允許直接呼叫 send ,  因為 test 沒有 payable 回退函式
        //  轉化為 address payable 型別 , 然後才可以呼叫 send
        address payable testPayable = payable(address(test));
        testPayable.transfer(2 ether);
        // 以下將不會編譯,但如果有人向該合約傳送以太幣,交易將失敗並拒絕以太幣。
        // test.send(2 ether);
        return true;
    }
    function callTestPayable(TestPayable test) public returns (bool) {
        (bool success,) = address(test).call(abi.encodeWithSignature("nonExistingFunction()"));
        require(success);
        // 結果 test.x 為 1  test.y 為 0.
        (success,) = address(test).call{value: 1}(abi.encodeWithSignature("nonExistingFunction()"));
        require(success);
        // 結果test.x 為1 而 test.y 為 1.
        // 傳送以太幣, TestPayable 的 receive 函式被呼叫.
        // 因為函式有儲存寫入, 會比簡單的使用 send 或 transfer 消耗更多的 gas。
        // 因此使用底層的call呼叫
        (success,) = address(test).call{value: 2 ether}("");
        require(success);
        // 結果 test.x 為 2 而 test.y 為 2 ether.
        return true;
    }
}
undefined

版權宣告:本文為CSDN博主「甄齊才」的原創文章,遵循CC4.0BY-SA版權協議,轉載請附上原文出處連結及本宣告。
原文連結:
https://blog.csdn.net/coco2d_x2014/article/details/12837727
文章來源:CSDN博主「甄齊才」
文章原標題:《玩以太坊鏈上專案的必備技能(函式及其可見性和狀態可變性-Solidity之旅十三)》
旨在傳播區塊鏈相關技術,如有侵權請與我們聯絡刪除。


來自 “ ITPUB部落格 ” ,連結:https://blog.itpub.net/70012206/viewspace-3000991/,如需轉載,請註明出處,否則將追究法律責任。

相關文章