solidity -- 合約

# 合約 Solidity 中的合約類似于面向?qū)ο笳Z言中的類。 它們包含狀態(tài)變量中的持久數(shù)據(jù),以及可以修改這些變量的函數(shù)。 在不同的合約(實(shí)例)上調(diào)用函數(shù)將執(zhí)行 EVM 函數(shù)調(diào)用,從而切換上下文,使得調(diào)用合約中的狀態(tài)變量不可訪問。 需要調(diào)用合約及其功能才能發(fā)生任何事情。 以太坊中沒有“cron”概念可以在特定事件時(shí)自動(dòng)調(diào)用函數(shù)。 # 創(chuàng)建合約 可以通過以太坊交易“從外部”或從 Solidity 合約內(nèi)部創(chuàng)建合約。 一些集成開發(fā)環(huán)境,例如?[Remix](https://remix.ethereum.org/), 通過使用一些UI用戶界面使創(chuàng)建合約的過程更加順暢。 在以太坊上通過編程創(chuàng)建合約最好使用 JavaScript API?[web3.js](https://github.com/ethereum/web3.js)。 現(xiàn)在,我們已經(jīng)有了一個(gè)叫做?[web3.eth.Contract](https://web3js.readthedocs.io/en/1.0/web3-eth-contract.html#new-contract)?的方法能夠更容易的創(chuàng)建合約。 創(chuàng)建合約時(shí), 合約的?[構(gòu)造函數(shù)](https://learnblockchain.cn/docs/solidity/contracts.html#constructor)?(一個(gè)用關(guān)鍵字?`constructor`?聲明的函數(shù))會(huì)執(zhí)行一次。 構(gòu)造函數(shù)是可選的。只允許有一個(gè)構(gòu)造函數(shù),這意味著不支持重載。 構(gòu)造函數(shù)執(zhí)行完畢后,合約的最終代碼將部署到區(qū)塊鏈上。此代碼包括所有公共和外部函數(shù)以及所有可以通過函數(shù)調(diào)用訪問的函數(shù)。 部署的代碼沒有 包括構(gòu)造函數(shù)代碼或構(gòu)造函數(shù)調(diào)用的內(nèi)部函數(shù)。 在內(nèi)部,構(gòu)造函數(shù)參數(shù)在合約代碼之后通過?[ABI 編碼](https://learnblockchain.cn/docs/solidity/abi-spec.html#abi)?傳遞,但是如果你使用?`web3.js`?則不必關(guān)心這個(gè)問題。 如果一個(gè)合約想要?jiǎng)?chuàng)建另一個(gè)合約,那么創(chuàng)建者必須知曉被創(chuàng)建合約的源代碼(和二進(jìn)制代碼)。 這意味著不可能循環(huán)創(chuàng)建依賴項(xiàng)。 ```solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.22 <0.9.0; contract OwnedToken { // TokenCreator 是后面定義的合約類型. // 不創(chuàng)建新合約的話,也可以引用它。 TokenCreator creator; address owner; bytes32 name; // 這是注冊(cè) creator 和設(shè)置名稱的構(gòu)造函數(shù)。 constructor(bytes32 name_) { // 狀態(tài)變量通過其名稱訪問,而不是通過例如 this.owner 的方式訪問。 // 這也適用于函數(shù),特別是在構(gòu)造函數(shù)中,你只能像這樣(“內(nèi)部地”)調(diào)用它們, // 因?yàn)楹霞s本身還不存在。 owner = msg.sender; // 從 `address` 到 `TokenCreator` ,是做顯式的類型轉(zhuǎn)換 // 并且假定調(diào)用合約的類型是 TokenCreator,沒有真正的方法來檢查這一點(diǎn)。 creator = TokenCreator(msg.sender); name = name_; } function changeName(bytes32 newName) public { // 只有 creator (即創(chuàng)建當(dāng)前合約的合約)能夠更改名稱 —— 因?yàn)楹霞s是隱式轉(zhuǎn)換為地址的, // 所以這里的比較是可行的。 if (msg.sender == address(creator)) name = newName; } function transfer(address newOwner) public { // 只有當(dāng)前所有者才能發(fā)送 token。 if (msg.sender != owner) return; // 我們也想詢問 creator 是否可以發(fā)送。 // 請(qǐng)注意,這里調(diào)用了一個(gè)下面定義的合約中的函數(shù)。 // 如果調(diào)用失?。ū热?,由于 gas 不足),會(huì)立即停止執(zhí)行。 if (creator.isTokenTransferOK(owner, newOwner)) owner = newOwner; } } contract TokenCreator { function createToken(bytes32 name) public returns (OwnedToken tokenAddress) { // 創(chuàng)建一個(gè)新的 Token 合約并且返回它的地址。 // 從 JavaScript 方面來說,返回類型是簡(jiǎn)單的 `address` 類型,因?yàn)? // 這是在 ABI 中可用的最接近的類型。 return new OwnedToken(name); } function changeName(OwnedToken tokenAddress, bytes32 name) public { // 同樣,`tokenAddress` 的外部類型也是 `address` 。 tokenAddress.changeName(name); } function isTokenTransferOK(address currentOwner, address newOwner) public view returns (bool ok) { // 檢查一些任意的情況。 address tokenAddress = msg.sender; return (keccak256(newOwner) & 0xff) == (bytes20(tokenAddress) & 0xff); } } ``` # ****可見性和 getter 函數(shù)**** ## ****狀態(tài)變量可見性**** **`public`** 對(duì)于 public 狀態(tài)變量會(huì)自動(dòng)生成一個(gè)[g](https://learnblockchain.cn/docs/solidity/contracts.html#getter-functions)etter函數(shù)(見下面)。 以便其他的合約讀取他們的值。 當(dāng)在用一個(gè)合約里使用是,外部方式訪問 (如:?`this.x`) 會(huì)調(diào)用getter 函數(shù),而內(nèi)部方式訪問 (如:?`x`) 會(huì)直接從存儲(chǔ)中獲取值。 Setter函數(shù)則不會(huì)被生成,所以其他合約不能直接修改其值。 **`internal`** 內(nèi)部可見性狀態(tài)變量只能在它們所定義的合約和派生合同中訪問。 它們不能被外部訪問。 這是狀態(tài)變量的默認(rèn)可見性。 **`private`** 私有狀態(tài)變量就像內(nèi)部變量一樣,但它們?cè)谂缮霞s中是不可見的。 ?? 設(shè)置為?`private`和`internal`,只能防止其他合約讀取或修改信息,但它仍然可以在鏈外查看到。 ## ****函數(shù)可見性**** 由于 Solidity 有兩種函數(shù)調(diào)用:外部調(diào)用則會(huì)產(chǎn)生一個(gè) EVM 調(diào)用,而內(nèi)部調(diào)用不會(huì), 更進(jìn)一步, 函數(shù)可以確定器被內(nèi)部及派生合約的可訪問性,這里有 4 種可見性: **`external`** 外部可見性函數(shù)作為合約接口的一部分,意味著我們可以從其他合約和交易中調(diào)用。 一個(gè)外部函數(shù)?`f`?不能從內(nèi)部調(diào)用(即?`f`?不起作用,但?`this.f()`?可以)。 **`public`** public 函數(shù)是合約接口的一部分,可以在內(nèi)部或通過消息調(diào)用。 **`internal`** 內(nèi)部可見性函數(shù)訪問可以在當(dāng)前合約或派生的合約訪問,不可以外部訪問。 由于它們沒有通過合約的ABI向外部公開,它們可以接受內(nèi)部可見性類型的參數(shù):比如映射或存儲(chǔ)引用。 **`private`** private 函數(shù)和狀態(tài)變量?jī)H在當(dāng)前定義它們的合約中使用,并且不能被派生合約使用。 可見性標(biāo)識(shí)符的定義位置,對(duì)于狀態(tài)變量來說是在類型后面,對(duì)于函數(shù)是在參數(shù)列表和返回關(guān)鍵字中間。 ```solidity pragma solidity >=0.4.16 <0.9.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`可以調(diào)用?`c.getData()`來獲取狀態(tài)存儲(chǔ)中?`data`的值,但不能調(diào)用?`f`。 合約?`E`繼承自?`C`,因此可以調(diào)用?`compute`。 ```solidity pragma solidity >=0.4.16 <0.9.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; } } // 下面代碼編譯錯(cuò)誤 contract D { function readData() public { C c = new C(); uint local = c.f(7); // 錯(cuò)誤:成員 `f` 不可見 c.setData(3); local = c.getData(); local = c.compute(3, 5); // 錯(cuò)誤:成員 `compute` 不可見 } } contract E is C { function g() public { C c = new C(); uint val = compute(3, 5); // 訪問內(nèi)部成員(從繼承合約訪問父合約成員) } } ``` ## ****Getter 函數(shù)**** 編譯器自動(dòng)為所有?**public**狀態(tài)變量創(chuàng)建 getter 函數(shù)。對(duì)于下面給出的合約,編譯器會(huì)生成一個(gè)名為?`data`的函數(shù), 該函數(shù)沒有參數(shù),返回值是一個(gè)?`uint`類型,即狀態(tài)變量?`data`的值。 狀態(tài)變量的初始化可以在聲明時(shí)完成。 ```solidity pragma solidity >=0.4.16 <0.9.0; contract C { uint public data = 42; } contract Caller { C c = new C(); function f() public { uint local = c.data(); } } ``` getter 函數(shù)具有外部(external)可見性。如果在內(nèi)部訪問 getter(即沒有?`this.`),它被認(rèn)為一個(gè)狀態(tài)變量。 如果使用外部訪問(即用?`this.`),它被認(rèn)作為一個(gè)函數(shù)。 ```solidity pragma solidity >=0.4.16 <0.9.0; contract C { uint public data; function x() public { data = 3; // 內(nèi)部訪問 uint val = this.data(); // 外部訪問 } } ``` 如果你有一個(gè)數(shù)組類型的?`public`狀態(tài)變量,那么你只能通過生成的 getter 函數(shù)訪問數(shù)組的單個(gè)元素。 這個(gè)機(jī)制以避免返回整個(gè)數(shù)組時(shí)的高成本gas。 可以使用如?`myArray(0)`用于指定參數(shù)要返回的單個(gè)元素。 如果要在一次調(diào)用中返回整個(gè)數(shù)組,則需要寫一個(gè)函數(shù),例如: ```solidity pragma solidity >=0.4.0 <0.9.0; contract arrayExample { // public state variable uint[] public myArray; // 指定生成的Getter 函數(shù) /* function myArray(uint i) public view returns (uint) { return myArray[i]; } */ // 返回整個(gè)數(shù)組 function getArray() public view returns (uint[] memory) { return myArray; } } ``` 現(xiàn)在可以使用?`getArray()`?獲得整個(gè)數(shù)組,而?`myArray(i)`?是返回單個(gè)元素。 下一個(gè)例子稍微復(fù)雜一些: ```solidity pragma solidity ^0.4.0 <0.9.0; contract Complex { struct Data { uint a; bytes3 b; mapping (uint => uint) map; uint[3] c; uint[] d; bytes e; } mapping (uint => mapping(bool => Data[])) public data; } ``` 這將會(huì)生成以下形式的函數(shù),在結(jié)構(gòu)體內(nèi)的映射和數(shù)組(byte 數(shù)組除外)被省略了,因?yàn)闆]有好辦法為單個(gè)結(jié)構(gòu)成員或?yàn)橛成涮峁┮粋€(gè)鍵。 ```solidity function data(uint arg1, bool arg2, uint arg3) public returns (uint a, bytes3 b, bytes memory e) { a = data[arg1][arg2][arg3].a; b = data[arg1][arg2][arg3].b; e = data[arg1][arg2][arg3].e; } ``` # ****函數(shù)?修改器modifier**** 使用?修改器modifier可以輕松改變函數(shù)的行為。 例如,它們可以在執(zhí)行函數(shù)之前自動(dòng)檢查某個(gè)條件。?修改器modifier是合約的可繼承屬性,并可能被派生合約覆蓋 , 但前提是它們被標(biāo)記為?`virtual`。 有關(guān)詳細(xì)信息,請(qǐng)參見?[Modifier 重載](https://learnblockchain.cn/docs/solidity/contracts.html#modifier-overriding) ```solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.1 <0.9.0; contract owned { constructor() { owner = payable(msg.sender); } address owner; // 這個(gè)合約只定義一個(gè)修改器,但并未使用: 它將會(huì)在派生合約中用到。 // 修改器所修飾的函數(shù)體會(huì)被插入到特殊符號(hào) _; 的位置。 // 這意味著如果是 owner 調(diào)用這個(gè)函數(shù),則函數(shù)會(huì)被執(zhí)行,否則會(huì)拋出異常。 modifier onlyOwner { require( msg.sender == owner, "Only owner can call this function." ); _; } } contract destructible is owned { // 這個(gè)合約從 `owned` 繼承了 `onlyOwner` 修飾符,并將其應(yīng)用于 `destroy` 函數(shù), // 只有在合約里保存的 owner 調(diào)用 `destroy` 函數(shù),才會(huì)生效。 function destroy() public onlyOwner { selfdestruct(owner); } } contract priced { // 修改器可以接收參數(shù): modifier costs(uint price) { if (msg.value >= price) { _; } } } contract Register is priced, destructible { mapping (address => bool) registeredAddresses; uint price; constructor(uint initialPrice) { price = initialPrice; } // 在這里也使用關(guān)鍵字 `payable` 非常重要,否則函數(shù)會(huì)自動(dòng)拒絕所有發(fā)送給它的以太幣。 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; } // 這個(gè)函數(shù)受互斥量保護(hù),這意味著 `msg.sender.call` 中的重入調(diào)用不能再次調(diào)用 `f`。 // `return 7` 語句指定返回值為 7,但修改器中的語句 `locked = false` 仍會(huì)執(zhí)行。 function f() public noReentrancy returns (uint) { (bool success,) = msg.sender.call(""); require(success); return 7; } } ``` 如果你想訪問定義在合約?`C`?的?修改器modifier?`m`?, 可以使用?`C.m`?去引用它,而不需要使用虛擬表查找。 只能使用在當(dāng)前合約或在基類合約中定義的?修改器modifier?,?修改器modifier?也可以定義在庫里面,但是他們被限定在庫函數(shù)使用。 如果同一個(gè)函數(shù)有多個(gè)?修改器modifier,它們之間以空格隔開,修改器modifier?會(huì)依次檢查執(zhí)行。 修改器不能隱式地訪問或改變它們所修飾的函數(shù)的參數(shù)和返回值。 這些值只能在調(diào)用時(shí)明確地以參數(shù)傳遞。 在函數(shù)修改器中,指定何時(shí)運(yùn)行被修改器應(yīng)用的函數(shù)是有必要。占位符語句(用單個(gè)下劃線字符?`_`?表示)用于表示被修改的函數(shù)的主體應(yīng)該被插入的位置。 請(qǐng)注意,占位符運(yùn)算符與在變量名稱中使用下劃線作為前導(dǎo)或尾隨字符不同,后者是一種風(fēng)格上的選擇。 修改器modifier?或函數(shù)體中顯式的 return 語句僅僅跳出當(dāng)前的?修改器modifier?和函數(shù)體。 返回變量會(huì)被賦值,但整個(gè)執(zhí)行邏輯會(huì)從前一個(gè)?修改器modifier?中的定義的?`_`?之后繼續(xù)執(zhí)行。 # ****Constant 和 Immutable 狀態(tài)變量**** 狀態(tài)變量可以聲明為常量`constant`或不可變量`immutable`的。 在這兩種情況下,變量都不能在合約構(gòu)建后修改。 對(duì)于`constant`,其值必須在編譯時(shí)固定,而對(duì)于`immutable`,它仍然可以在構(gòu)造時(shí)賦值。 也可以在文件級(jí)別定義常量變量。 編譯器不會(huì)為這些變量保留一個(gè)存儲(chǔ)槽,每次出現(xiàn)都會(huì)被相應(yīng)的值替換。 與常規(guī)狀態(tài)變量相比,常量和不可變變量的gas成本要低得多。 對(duì)于常量變量,分配給它的表達(dá)式被復(fù)制到所有訪問它的地方,并且每次都重新計(jì)算。 這允許局部?jī)?yōu)化。 不可變變量在構(gòu)造時(shí)被評(píng)估一次,并且它們的值被復(fù)制到代碼中訪問它們的所有位置。 對(duì)于這些值,保留了 32 個(gè)字節(jié),即使它們適合更少的字節(jié)也是如此。 因此,常量值有時(shí)可能比不可變值便宜。 目前并非所有常量和不可變類型都已實(shí)現(xiàn)。 唯一支持的類型是字符串(僅適用于常量)和值類型。 ```solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >0.7.4; uint constant X = 32**22 + 8; contract C { string constant TEXT = "abc"; bytes32 constant MY_HASH = keccak256("abc"); uint immutable decimals; uint immutable maxBalance; address immutable owner = msg.sender; constructor(uint decimals_, address ref) { decimals = decimals_; // Assignments to immutables can even access the environment. maxBalance = ref.balance; } function isBalanceTooHigh(address _other) public view returns (bool) { return _other.balance > maxBalance; } } ``` ## ****Constant**** 對(duì)于`constant`常量變量,該值在編譯時(shí)必須是常量,并且必須在聲明變量的地方賦值。 任何訪問存儲(chǔ)、區(qū)塊鏈數(shù)據(jù)(例如 `block.timestamp`、`address(this).balance` 或 `block.number`)或執(zhí)行數(shù)據(jù)(`msg.value` 或 `gasleft()`)或調(diào)用外部合約的表達(dá)式都是不允許的。 允許使用可能對(duì)內(nèi)存分配產(chǎn)生副作用的表達(dá)式,但不允許使用可能對(duì)其他內(nèi)存對(duì)象產(chǎn)生副作用的表達(dá)式。 內(nèi)置函數(shù) `keccak256`、`sha256`、`ripemd160`、`ecrecover`、`addmod` 和 `mulmod` 是允許的(盡管除了 `keccak256`,它們確實(shí)調(diào)用外部合約)。 ## ****Immutable**** 聲明為不可變`immutable`的變量比聲明為常量`constant`的變量受到的限制要少一些:可以在合約的構(gòu)造函數(shù)中或在聲明時(shí)為不可變變量分配任意值。 它們只能分配一次,并且從那時(shí)起,即使在構(gòu)建期間也可以讀取。 編譯器生成的合約創(chuàng)建代碼將在合約返回之前修改合約的運(yùn)行時(shí)代碼,方法是將所有對(duì)不可變對(duì)象的引用替換為分配給它們的值。 如果您要將編譯器生成的運(yùn)行時(shí)代碼與實(shí)際存儲(chǔ)在區(qū)塊鏈中的代碼進(jìn)行比較,這一點(diǎn)很重要。 ?? 不可變量可以在聲明時(shí)賦值,不過只有在合約的構(gòu)造函數(shù)執(zhí)行時(shí)才被視為視為初始化。 這意味著,你不能用一個(gè)依賴于不可變量的值在行內(nèi)初始化另一個(gè)不可變量。 不過,你可以在合約的構(gòu)造函數(shù)中這樣做。 這是為了防止對(duì)狀態(tài)變量初始化和構(gòu)造函數(shù)順序的不同解釋,特別是繼承時(shí),出現(xiàn)問題。 # 函數(shù) 可以在合約內(nèi)部和外部定義函數(shù)。 合約之外的函數(shù)(也稱為“自由函數(shù)”)始終具有隱式的?`internal`[可見性](https://learnblockchain.cn/docs/solidity/contracts.html#visibility-and-getters)。 它們的代碼包含在所有調(diào)用它們合約中,類似于內(nèi)部庫函數(shù)。 ```solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.1 <0.9.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 { // This calls the free function internally. // The compiler will add its code to the contract. uint s = sum(arr); require(s >= 10); found = true; } } ``` 在合約之外定義的函數(shù)仍然總是在合約的上下文中執(zhí)行。 他們?nèi)匀豢梢哉{(diào)用其他合約,向它們發(fā)送 Ether 并銷毀調(diào)用它們的合約等。 與合約內(nèi)定義的函數(shù)的主要區(qū)別在于,自由函數(shù)不能直接訪問變量 `this`、存儲(chǔ)變量和不在其范圍內(nèi)的函數(shù)。 ## ****函數(shù)參數(shù)及返回值**** 與 Javascript 一樣,函數(shù)可能需要參數(shù)作為輸入; 而與 Javascript 和 C 不同的是,它們可能返回任意數(shù)量的參數(shù)作為輸出。 ### ****函數(shù)參數(shù)(輸入?yún)?shù))**** 函數(shù)參數(shù)的聲明方式與變量相同。不過未使用的參數(shù)可以省略參數(shù)名。 例如,如果我們希望合約接受有兩個(gè)整數(shù)形參的函數(shù)的外部調(diào)用,可以像下面這樣寫: ```solidity pragma solidity >=0.4.16 <0.9.0; contract Simple { uint sum; function taker(uint a, uint b) public { sum = a + b; } } ``` ### ****返回變量**** 函數(shù)返回變量的聲明方式在關(guān)鍵詞?`returns`?之后,與參數(shù)的聲明方式相同。 例如,如果我們需要返回兩個(gè)結(jié)果:兩個(gè)給定整數(shù)的和與積,我們應(yīng)該寫作: ```solidity pragma solidity >=0.4.16 <0.9.0; contract Simple { function arithmetic(uint a, uint b) public pure returns (uint sum, uint product) { sum = a + b; product = a * b; } } ``` 返回變量的名稱可以省略。 返回變量可以用作任何其他局部變量,它們使用默認(rèn)值初始化并具有該值,直到它們被(重新)分配。 您可以顯式分配給返回變量,然后像上面那樣保留函數(shù),或者您可以直接使用 return 語句提供返回值(單個(gè)或多個(gè)): ```solidity contract Simple { function arithmetic(uint a, uint b) public pure returns (uint sum, uint product) { return (a + b, a * b); } } ``` 您不能從非內(nèi)部函數(shù)返回某些類型。 這包括下面列出的類型以及遞歸包含它們的任何復(fù)合類型: - 映射, - 內(nèi)部函數(shù)類型, - 位置設(shè)置為存儲(chǔ)的引用類型, - 多維數(shù)組(僅適用于 ABI 編碼器 v1), - 結(jié)構(gòu)(僅適用于 ABI 編碼器 v1)。 此限制不適用于庫函數(shù),因?yàn)樗鼈兙哂胁煌膬?nèi)部 ABI。 ## 狀態(tài)可變性 ### `view` 可以將函數(shù)聲明為?`view`?類型,這種情況下要保證不修改狀態(tài)。 下面的語句被認(rèn)為是修改狀態(tài): 1. 修改狀態(tài)變量。 2. [產(chǎn)生事件](https://learnblockchain.cn/docs/solidity/contracts.html#events)。 3. [創(chuàng)建其它合約](https://learnblockchain.cn/docs/solidity/control-structures.html#creating-contracts)。 4. 使用?`selfdestruct`。 5. 通過調(diào)用發(fā)送以太幣。 6. 調(diào)用任何沒有標(biāo)記為?`view`?或者?`pure`?的函數(shù)。 7. 使用低級(jí)調(diào)用。 8. 使用包含特定操作碼的內(nèi)聯(lián)匯編。 ```solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.5.0 <0.9.0; contract C { function f(uint a, uint b) public view returns (uint) { return a * (b + 42) + block.timestamp; } } ``` ### ****`Pure`**** 函數(shù)可以聲明為`pure`函數(shù),在這種情況下它們承諾不讀取或修改狀態(tài)。 特別是,應(yīng)該可以在編譯時(shí)僅給定輸入和 `msg.data` 來評(píng)估`pure`,而無需了解當(dāng)前的區(qū)塊鏈狀態(tài)。 這意味著從不可變變量中讀取可以是一個(gè)非純操作。 除了上面解釋的狀態(tài)修改語句列表之外,以下被認(rèn)為是讀取狀態(tài): 1. 讀取狀態(tài)變量。 2. 訪問?`address(this).balance`?或者?` .balance`。 3. 訪問?`block`,`tx`,?`msg`?中任意成員 (除?`msg.sig`?和?`msg.data`?之外)。 4. 調(diào)用任何未標(biāo)記為?`pure`?的函數(shù)。 5. 使用包含某些操作碼的內(nèi)聯(lián)匯編。 ```solidity pragma solidity >=0.5.0 <0.9.0; contract C { function f(uint a, uint b) public pure returns (uint) { return a * (b + 42); } } ``` 純函數(shù)能夠使用?`revert()`?和?`require()`?在?[發(fā)生錯(cuò)誤](https://learnblockchain.cn/docs/solidity/control-structures.html#assert-and-require)?時(shí)去回滾潛在狀態(tài)更改。 還原狀態(tài)更改不被視為 “狀態(tài)修改”, 因?yàn)樗贿€原以前在沒有`view` 或?`pure`?限制的代碼中所做的狀態(tài)更改, 并且代碼可以選擇捕獲?`revert`?并不傳遞還原。 這種行為也符合?`STATICCALL`?操作碼。 ## 特殊的函數(shù) ### `receive` 一個(gè)合約最多有一個(gè)?`receive`?函數(shù), 聲明函數(shù)為:?`receive()?external?payable?{?...?}` 不需要?`function`?關(guān)鍵字,也沒有參數(shù)和返回值并且必須是 `external` 可見性和 `payable`?修飾. 它可以是?`virtual`?的,可以被重載也可以有?修改器modifier?。 在對(duì)合約沒有任何附加數(shù)據(jù)調(diào)用(通常是對(duì)合約轉(zhuǎn)賬)是會(huì)執(zhí)行?`receive`函數(shù).例如 通過?`.send()`or?`.transfer()`如果?`receive`函數(shù)不存在,但是有payable 的?[fallback 回退函數(shù)](https://learnblockchain.cn/docs/solidity/contracts.html#fallback-function) ?那么在進(jìn)行純以太轉(zhuǎn)賬時(shí),fallback 函數(shù)會(huì)調(diào)用. 如果兩個(gè)函數(shù)都沒有,這個(gè)合約就沒法通過常規(guī)的轉(zhuǎn)賬交易接收以太(會(huì)拋出異常). 更糟的是,`receive`?函數(shù)可能只有 2300 gas 可以使用(如,當(dāng)使用?`send`?或?`transfer`?時(shí)), 除了基礎(chǔ)的日志輸出之外,進(jìn)行其他操作的余地很小。下面的操作消耗會(huì)操作 2300 gas : - 寫入存儲(chǔ) - 創(chuàng)建合約 - 調(diào)用消耗大量 gas 的外部函數(shù) - 發(fā)送以太幣 ?? 一個(gè)沒有定義 fallback 函數(shù)或  receive 函數(shù)的合約,直接接收以太幣(沒有函數(shù)調(diào)用,即使用?`send`?或?`transfer`)會(huì)拋出一個(gè)異常, 并返還以太幣(在 Solidity v0.4.0 之前行為會(huì)有所不同)。 所以如果你想讓你的合約接收以太幣,必須實(shí)現(xiàn)receive函數(shù)(使用 payable fallback 函數(shù)不再推薦,因?yàn)閜ayable fallback功能被調(diào)用,不會(huì)因?yàn)榘l(fā)送方的接口混亂而失敗)。 ?? 一個(gè)沒有receive函數(shù)的合約,可以作為?*coinbase 交易*?(又名?*礦工區(qū)塊回報(bào)*?)的接收者或者作為?`selfdestruct`?的目標(biāo)來接收以太幣。 一個(gè)合約不能對(duì)這種以太幣轉(zhuǎn)移做出反應(yīng),因此也不能拒絕它們。這是 EVM 在設(shè)計(jì)時(shí)就決定好的,而且 Solidity 無法繞過這個(gè)問題。 這也意味著?`address(this).balance`?可以高于合約中實(shí)現(xiàn)的一些手工記帳的總和(例如在receive 函數(shù)中更新的累加器記帳)。 下面你可以看到一個(gè)使用函數(shù) receive 的 Sink 合約的例子。 ```solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.0 <0.9.0; // This contract keeps all Ether sent to it with no way // to get it back. contract Sink { event Received(address, uint); receive() external payable { emit Received(msg.sender, msg.value); } } ``` ### ****`Fallback`**** 合約可以最多有一個(gè)回退函數(shù)。函數(shù)聲明為:?`fallback?()?external?[payable]`?或?`fallback?(bytes?calldata?input)?external?[payable]?returns?(bytes?memory?output)` 沒有 `function` 關(guān)鍵字。 必須是 `external` 可見性,它可以是?`virtual`?的,可以被重載也可以有?修改器modifier?。 如果在一個(gè)對(duì)合約調(diào)用中,沒有其他函數(shù)與給定的函數(shù)標(biāo)識(shí)符匹配fallback會(huì)被調(diào)用. 或者在沒有?[receive 函數(shù)](https://learnblockchain.cn/docs/solidity/contracts.html#receive-ether-function) 時(shí),而沒有提供附加數(shù)據(jù)對(duì)合約調(diào)用,那么fallback 函數(shù)會(huì)被執(zhí)行。 fallback 函數(shù)始終會(huì)接收數(shù)據(jù),但為了同時(shí)接收以太時(shí),必須標(biāo)記為 `payable`?。 如果使用了帶參數(shù)的版本,?`input`?將包含發(fā)送到合約的完整數(shù)據(jù)(等于?`msg.data`?),并且通過?`output`?返回?cái)?shù)據(jù)。 返回?cái)?shù)據(jù)不是 ABI 編碼過的數(shù)據(jù),相反,它返回不經(jīng)過修改的數(shù)據(jù)。 更糟的是,如果回退函數(shù)在接收以太時(shí)調(diào)用,可能只有 2300 gas 可以使用,參考 [receive接收函數(shù)](https://learnblockchain.cn/docs/solidity/contracts.html#receive-ether-function) 與任何其他函數(shù)一樣,只要有足夠的 gas 傳遞給它,回退函數(shù)就可以執(zhí)行復(fù)雜的操作 ?? `payable`?的fallback函數(shù)也可以在純以太轉(zhuǎn)賬的時(shí)候執(zhí)行, 如果沒有接受ether的函數(shù), ?推薦總是定義一個(gè)receive函數(shù),而不是定義一個(gè)payable的fallback函數(shù), ## 函數(shù)重載 合約可以具有多個(gè)不同參數(shù)的同名函數(shù),稱為“重載”(overloading),這也適用于繼承函數(shù)。以下示例展示了合約?`A`中的重載函數(shù)?`f`。 ```solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.16 <0.9.0; contract A { function f(uint value) public pure returns (uint out) { out = value; } function f(uint value, bool really) public pure returns (uint out) { if (really) out = value; } } ``` 重載函數(shù)也存在于外部接口中。 如果兩個(gè)外部可見函數(shù)的 Solidity 類型不同而不是外部類型不同,這是一個(gè)錯(cuò)誤。 ```solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.16 <0.9.0; // 以下代碼無法編譯 contract A { function f(B value) public pure returns (B out) { out = value; } function f(address value) public pure returns (address out) { out = value; } } contract B { } ``` 以上兩個(gè)?`f`函數(shù)重載都接受了 ABI 的地址類型,雖然它們?cè)?Solidity 中被認(rèn)為是不同的。 ### ****重載解析和參數(shù)匹配**** 通過將當(dāng)前范圍內(nèi)的函數(shù)聲明與函數(shù)調(diào)用中提供的參數(shù)相匹配,可以選擇重載函數(shù)。 如果所有參數(shù)都可以隱式地轉(zhuǎn)換為預(yù)期類型,則選擇函數(shù)作為重載候選項(xiàng)。如果一個(gè)候選都沒有,解析失敗。 ```solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.16 <0.9.0; contract A { function f(uint8 val) public pure returns (uint8 out) { out = val; } function f(uint256 val) public pure returns (uint256 out) { out = val; } } ``` 調(diào)用?`f(50)`會(huì)導(dǎo)致類型錯(cuò)誤,因?yàn)?`50`既可以被隱式轉(zhuǎn)換為?`uint8`也可以被隱式轉(zhuǎn)換為?`uint256` 。 另一方面,調(diào)用?`f(256)`則會(huì)解析為?`f(uint256)`重載,因?yàn)?`256`不能隱式轉(zhuǎn)換為?`uint8`。 本文由[mdnice](https://mdnice.com/?platform=6)多平臺(tái)發(fā)布
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容