Solidity之旅(十三)函式及其可見性和狀態可變性
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(); } }
// 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(); } }
// 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; } }
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 函式是 內部的 } }
// 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; } }
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; } }
如果你有一個陣列型別的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; } }
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; } }
在合約之外定義的函式仍然在合約的上下文內執行。它們仍然可以呼叫其他合約,將其傳送以太幣或銷燬呼叫它們的合約等其他事情。與在合約中定義的函式的主要區別為:自由函式不能直接訪問儲存變數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; } }
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; } }
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); } }
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); } }
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; } }
版權宣告:本文為CSDN博主「甄齊才」的原創文章,遵循CC4.0BY-SA版權協議,轉載請附上原文出處連結及本宣告。
原文連結:
https://blog.csdn.net/coco2d_x2014/article/details/12837727
文章來源:CSDN博主「甄齊才」
文章原標題:《玩以太坊鏈上專案的必備技能(函式及其可見性和狀態可變性-Solidity之旅十三)》
旨在傳播區塊鏈相關技術,如有侵權請與我們聯絡刪除。
來自 “ ITPUB部落格 ” ,連結:https://blog.itpub.net/70012206/viewspace-3000991/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Solidity語言學習筆記————21、函式的訪問許可權和可見性Solid筆記函式訪問許可權
- Solidity語言學習筆記————22、可見性和GettersSolid筆記
- Solidity語言學習筆記————19、函式可見性定義符、修飾符、保留字和語法Solid筆記函式
- 你還不懂可見性、有序性和原子性?
- volatile 可見性與原子性
- volatile,可見性,有序性
- PostgreSQL vacuum可見性SQL
- java安全編碼指南之:可見性和原子性Java
- php 可變函式PHP函式
- Volatile可見性分析(一)
- 從硬體級別再看可見性和有序性
- 【譯】kotlin 協程官方文件(8)-共享可變狀態和併發性(Shared mutable state and concurrency)Kotlin
- React專題:可變狀態React
- volatile記憶體可見性和指令重排記憶體
- Java併發之原子性、有序性、可見性Java
- 執行緒安全性-原子性、可見性、有序性執行緒
- PostgreSQL MVCC可見性判斷SQLMVC
- Kotlin可見性修飾符Kotlin
- 三大性質總結:原子性、可見性以及有序性
- 可見性有序性,Happens-before來搞定APP
- 提供一種Fragment可見性改變的監測方案Fragment
- android判斷狀態列是否可見Android
- java多執行緒3:原子性,可見性,有序性Java執行緒
- 併發bug之源(一)-可見性
- 類&成員可見性&繼承繼承
- Python中的物件引用、可變性和垃圾回收Python物件
- 面向可複用性和可維護性的設計模式設計模式
- 使用 volatile 關鍵字保證變數可見性和禁止指令重排序變數排序
- 通過建立動態型別 動態構建Expression Select表示式來控制Property可見性型別Express
- MySQL:從一個案例深入剖析InnoDB隱式鎖和可見性判斷MySql
- 通過String的不變性案例分析Java變數的可變性Java變數
- Java併發程式設計Bug源頭:可見性、原子性和有序性問題Java程式設計
- [C]可變參量,debugprint函式函式
- Go函式接收可變引數Go函式
- 淺談併發的資料競爭(可見性)與競態條件(原子性)
- Record-and-Replay 可維護性和 Replay 性
- 關於HDFS的資料可見性
- [深入理解Java虛擬機器]原子性/可見性/有序性Java虛擬機