初學Solidity(一):語法大致總結
Solidity是物件導向的高階程式語言,是用於開發智慧合約的語言之一,語法類似於JavasSript,但又有所不同。本期,我們為初學Solidity的開發者們推薦了CSDN作者DayDayUp丶關於學習Solidity語法的一篇總結文章。
一、資料型別
1.1、值型別
1.1.1、布林
pragma solidity ^0.4.25; contract TestBool { bool flag; int num1 = 100; int num2 = 200; // default false function getFlag() public view returns(bool) { return flag; // false } // 非 function getFlag2() public view returns(bool) { return !flag; // true } // 與 function getFlagAnd() public view returns(bool) { return (num1 != num2) && !flag; // true } // 或 function getFlagOr() public view returns(bool) { return (num1 == num2) || !flag; // true } }
1.1.2、整數
加減乘除、取餘、冪運算,
pragma solidity ^0.4.25; // 整型特性與運算 contract TestInteger { int num1; // 有符號整型 int256 uint num2; // 無符號整型 uint256 function add(uint _a, uint _b) public pure returns(uint) { return _a + _b; } function sub(uint _a, uint _b) public pure returns(uint) { return _a - _b; } function mul(uint _a, uint _b) public pure returns(uint) { return _a * _b; } function div(uint _a, uint _b) public pure returns(uint) { return _a / _b; // 在solidity中,除法是做整除,沒有小數點 } function rem(uint _a, uint _b) public pure returns(uint) { return _a % _b; } function square(uint _a, uint _b) public pure returns(uint) { return _a ** _b; // 冪運算在0.8.0之後,變為右優先,即a ** b ** c => a ** (b ** c) } function max() public view returns(uint) { return uint(-1); // return type(uint).max; // 0.8不再允許uint(-1) } }
位運算,
pragma solidity ^0.4.25; // 位運算 contract TestBitwise { uint8 num1 = 3; uint8 num2 = 4; function bitAdd() public view returns(uint) { return num1 & num2; } function bitOr() public view returns(uint) { return num1 | num2; } function unBit() public view returns(uint) { return ~num1; } function bitXor() public view returns(uint) { return num1 ^ num2; } function bitRight() public view returns(uint) { return num1 >> 1; } function bitLeft() public view returns(uint) { return num1 << 1; } }
1.1.3、定長浮點型
目前只支援定義,不支援賦值使用,
fixed num; // 有符號 ufixed num; // 無符號
1.1.4、地址型別
address addr = msg.sender; address addr = 0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF;
1.1.5、合約型別
在合約TestType中使用TestBitwise合約,
TestBitwise t = TestBitwise(addr);
1.1.6、列舉型別
enum ActionChoices { Up, Down, Left, Right }
1.1.7、定長位元組陣列
pragma solidity ^0.4.25; // 固定長度的位元組陣列(靜態),以及轉換為string型別 contract TestBytesFixed { // public 自動生成同名的get方法 bytes1 public num1 = 0x7a; // 1 byte = 8 bit bytes1 public num2 = 0x68; bytes2 public num3 = 0x128b; // 2 byte = 16 bit // 獲取位元組陣列長度 function getLength() public view returns(uint) { return num3.length; // 2 } // 位元組陣列比較 function compare() public view returns(bool) { return num1 < num2; } // 位元組陣列位運算 function bitwise() public view returns(bytes1,bytes1,bytes1) { // 多返回值 return ((num1 & num2), (num1 | num2), (~num1)); } // 先轉為bytes動態陣列,再透過string構造 0x7a7a -> zz function toString(bytes2 _val) public pure returns(string) { bytes memory buf = new bytes(_val.length); for (uint i = 0; i < _val.length; i++) { buf[i] = _val[i]; } return string(buf); } }
固定長度位元組陣列的擴充和壓縮,
pragma solidity ^0.4.25; // 固定長度的位元組陣列(靜態)擴容和壓縮 contract TestBytesExpander { // public 自動生成同名的get方法 bytes6 name = 0x796f7269636b; function changeTo1() public view returns(bytes1) { return bytes1(name); // 0x79 } function changeTo2() public view returns(bytes2) { return bytes2(name); // 0x796f } function changeTo16() public view returns(bytes16) { return bytes16(name); // 0x796f7269636b00000000000000000000 } }
1.1.8、函式型別
function (<parameter types>) {internal|external|public|private} [pure|constant|view|payable] [returns (<return types>)]
1.2、引用型別
1.2.1、字串
pragma solidity ^0.4.25; // 修改string型別的資料 contract TestString { string name = 'yorick'; // 字串可以使用單引號或者雙引號 string name2 = "!@#$%^&"; // 特殊字元佔1個byte string name3 = "張三"; // 中文在string中使用utf8的編碼方式儲存,佔用3個byte function getLength() view public returns(uint) { // 不可以直接獲取string的length return bytes(name).length; // 6 } function getLength2() view public returns(uint) { return bytes(name2).length; // 7 } function getLength3() view public returns(uint) { return bytes(name3).length; // 6 } function getElmName() view public returns(bytes1) { // 不可以直接透過陣列下標name[0]獲取 return bytes(name)[0]; // 0x79 } function changeElmName() public { bytes(name)[0] = "h"; } function getName() view public returns(bytes) { return bytes(name); } }
1.2.2、變長位元組陣列
pragma solidity ^0.4.25; // 動態的位元組陣列,以及轉換為string型別 contract TestBytesDynamic { bytes public dynamicBytes; function setDynamicBytes(string memory val) public { dynamicBytes = bytes(val); } function getVal() public view returns(string){ return string(dynamicBytes); } }
1.2.3、陣列
定長陣列,
pragma solidity ^0.4.25; // 定長陣列 contract TestArrFixed { uint[5] arr = [1,2,3,4,5]; // 修改陣列元素內容 function modifyElements() public { arr[0] = 12; arr[1] = 14; } // 檢視陣列 function watchArr() public view returns(uint[5]) { return arr; } // 陣列元素求和計算 function sumArr() public view returns(uint) { uint sum = 0; for (uint i = 0; i < arr.length; i++) { sum += arr[i]; } return sum; } // 陣列長度 function getLength() public view returns(uint) { return arr.length; } // delete重置陣列某下標的元素值,不會真正刪除該元素 function deleteElm(uint idx) public { delete arr[idx]; } // delete重置整個陣列 function deleteArr() public { delete arr; } /** 定長陣列不允許改變長度和push // 壓縮陣列後,右側多餘元素被丟棄 function changeLengthTo1() public { arr.length = 1; } // 擴容陣列後,右側元素補0 function changeLengthTo10() public { arr.length = 10; } // 追加新元素 function pushElm(uint _elm) public { arr.push(_elm); } */ }
變長陣列,
pragma solidity ^0.4.25; // 變長陣列 contract TestArrDynamic { uint[] arr = [1,2,3,4,5]; // 檢視陣列 function watchArr() public view returns(uint[]) { return arr; } // 陣列長度 function getLength() public view returns(uint) { return arr.length; } // 壓縮陣列後,右側多餘元素被丟棄 function changeLengthTo1() public { arr.length = 1; } // 擴容陣列後,右側元素補0 function changeLengthTo10() public { arr.length = 10; } // 追加新元素 function pushElm(uint _elm) public { arr.push(_elm); } // delete重置陣列某下標的元素值,不會真正刪除該元素 function deleteElm(uint idx) public { delete arr[idx]; } // delete重置整個陣列 function deleteArr() public { delete arr; } }
二維陣列,
pragma solidity ^0.4.25; /** 二維陣列 solidity的二維陣列與其他語言不同,[2] [3]表示3行2列,而其他語言為2行3列; 二維動態陣列與一維陣列類似,可以改變其陣列長度; */ contract TestArr2Dimensional { uint[2][3] arr = [[1,2],[3,4],[5,6]]; function getRowSize() public view returns(uint) { return arr.length; // 3 } function getColSize() public view returns(uint) { return arr[0].length; // 2 } function watchArr() public view returns(uint[2][3]) { return arr; // 1,2,3,4,5,6 } function sumArr() public view returns(uint) { uint sum = 0; for (uint i = 0; i < getRowSize(); i++) { for (uint j = 0; j < getColSize(); j++) { sum += arr[i][j]; } } return sum; } function modifyArr() public { arr[0][0] = 99; } }
陣列字面值,
pragma solidity ^0.4.25; // 陣列字面值 contract TestArrLiteral { // 最小儲存匹配,未超過255,所以使用uint8儲存 function getLiteral8() pure public returns(uint8[3]) { return [1,2,3]; } // 超過255,所以使用uint16儲存 function getLiteral16() pure public returns(uint16[3]) { return [256,2,3]; // [255,2,3] 不被uint16允許 } // 強制轉換為uint256 function getLiteral256() pure public returns(uint[3]) { return [uint(1),2,3]; // 給任意元素強轉即可,否則不被允許 } // 計算外界傳入的內容 function addLiterals(uint[3] arr) pure external returns(uint) { uint sum = 0; for (uint i = 0; i < arr.length; i++) { sum += arr[i]; } return sum; } }
1.2.4、結構體
pragma solidity ^0.4.25; // 結構體初始化的兩種方法 contract TestStruct { struct Student { uint id; string name; mapping(uint=>string) map; } // 預設為storage型別,只能透過storage型別操作結構體中的mapping型別資料 Student storageStu; // mapping型別可以不用在定義的時候賦值,而其他型別不賦值則會報錯 function init() public pure returns(uint, string) { Student memory stu = Student(100, "Jay"); return (stu.id, stu.name); } function init2() public pure returns(uint, string) { Student memory stu = Student({name: "Jay", id: 100}); return (stu.id, stu.name); } function init3() public returns(uint, string, string) { Student memory stu = Student({name: "Jay", id: 100}); // 直接操作結構體中的mapping不被允許: Student memory out of storage // stu.map[1] = "artist"; // 透過storage型別的變數操作結構體中的mapping storageStu = stu; storageStu.map[1] = "artist"; return (storageStu.id, storageStu.name, storageStu.map[1]); } // 結構體作為入參時,為memory型別,並且不能使用public或external修飾函式:Internal or recursive type is not allowed for public or external functions. // 賦值時也要指定為memory,否則報錯 function testIn(Student stu) internal returns(uint) { return stu.id; } // 結構體作為出參,同樣只能private或internal宣告內部使用 function testOut(Student stu) private returns(Student) { return stu; } }
1.3、對映
contract TestMapping { mapping(address => uint) private scores; // <學生,分數>的單層對映 mapping(address => mapping(bytes32 => uint8)) private _scores; // <學生,<科目,分數>>的兩層對映 function getScore() public view returns(address, uint) { address addr = msg.sender; return (addr, scores[addr]); } function setScore() public { scores[msg.sender] = 100; } }
二、作用域(訪問修飾符)
contract TestAccessCtrl { constructor () public {} uint public num1 = 1; // 自動為public生成同名的get函式,但在編碼時不可直接呼叫num1() uint private num2 = 2; uint num3 = 3; // 不寫則預設private function funcPublic() public returns(string) { return "public func"; } function funcPrivate() private returns(string) { return "private func"; } function funcInternal() internal returns(string) { return "internal func"; } function funcExternal() external returns(string) { return "external func"; } function test1(uint choice) public returns(string) { if (choice == 1) return funcPublic(); if (choice == 2) return funcPrivate(); if (choice == 3) return funcInternal(); //if (choice == 4) return funcExternal(); // external不允許直接在內部用 if (choice == 4) return this.funcExternal(); // 間接透過this才可以呼叫external } } contract TestAccessCtrlSon is TestAccessCtrl { function test2(uint choice) public returns(string) { if (choice == 1) return funcPublic(); // public允許派生合約使用 //if (choice == 2) return funcPrivate(); // private不允許派生合約使用 if (choice == 3) return funcInternal(); // internal允許派生合約使用 //if (choice == 4) return funcExternal(); // external也不允許派生合約直接使用 } } contract TestAccessCtrl2 { function test2(uint choice) public returns(string) { TestAccessCtrl obj = new TestAccessCtrl(); return obj.funcExternal(); // external只允許在外部合約中這樣間接呼叫 } }
2.1、private
僅在當前合約使用,且不可被繼承,私有狀態變數只能從當前合約內部訪問,派生合約內不能訪問。
2.2、public
同時支援內部和外部呼叫。修飾狀態變數時,自動生成同名get函式,
但在編碼時不可直接呼叫num1()。
2.3、internal
只支援內部呼叫,也包括其派生合約內訪問。
2.4、external
外部才可呼叫,內部想要呼叫可以用this。
三、函式修飾符
contract TestFuncDecorator { uint public num = 1; /// pure function testPure(uint _num) public pure { //uint num1 = num; // pure不允許讀狀態變數 //num = _num; // pure不允許修改狀態變數 } /// view function testView(uint _num) public view { uint num1 = num; // 允許讀狀態變數 num = _num; // 0.4語法上允許修改狀態變數,但實際不會修改,所以num還是1 // 0.5及之後不允許在view中這樣修改,否則編譯不透過 } /// payable function () public payable {} function getBalance() public view returns(uint) { // balance獲取合約地址下的以太幣餘額 return address(this).balance; } // 充值函式payable,只有新增這個關鍵字,才能在執行這個函式時,給這個合約充以太幣,否則該函式自動拒絕所有傳送給它的以太幣 function testPayable() payable public { // transfer轉賬 address(this).transfer(msg.value); } }
3.1、pure
表明該函式是純函式,連狀態變數都不用讀,函式的執行僅僅依賴於引數。不消耗gas。承諾不讀取或修改狀態,否則編譯出錯。
3.2、view
設定了view修飾符,就是一次呼叫,不需要執行共識、進入EVM,而是直接查詢本地節點資料,因此效能會得到很大提升。不消耗gas。不會發起交易,所以不能實際改變狀態變數。
3.3、payable
允許函式被呼叫的時候,讓合約接收以太幣。如果未指定,該函式將自動拒絕所有傳送給它的以太幣。
四、建構函式
唯一,不可過載。也可以接收入參。
pragma solidity ^0.4.25; contract Test1 { address private _owner; constructor() public { _owner = msg.sender; } /**constructor(int num) public { 過載構造->編譯錯誤 _owner = msg.sender; }*/ } contract Test2 { uint public num; constructor(uint x) public { // 帶參構造,在deploy時傳入 num = x; } }
五、修飾器modifier
方法修飾器modifier,類似AOP處理。
pragma solidity ^0.4.25; contract TestModifier { address private _owner; bool public endFlag; // 執行完test後的endFlag仍是true constructor() public { _owner = msg.sender; } modifier onlyOwner { // 許可權攔截器,非合約部署賬號執行test()則被攔截 require(_owner == msg.sender, "Auth: only owner is authorized."); _; // 類似被代理的test()方法呼叫 endFlag = true; } function test() public onlyOwner { endFlag = false; } }
六、資料位置
6.1、memory
其生命週期只存在於函式呼叫期間,區域性變數預設儲存在記憶體,不能用於外部呼叫。記憶體位置是臨時資料,比儲存位置便宜。它只能在函式中訪問。
通常,記憶體資料用於儲存臨時變數,以便在函式執行期間進行計算。一旦函式執行完畢,它的內容就會被丟棄。你可以把它想象成每個單獨函式的記憶體(RAM)。
6.2、storage
狀態變數儲存的位置,只要合約存在就一直儲存在區塊鏈中。該儲存位置儲存永久資料,這意味著該資料可以被合約中的所有函式訪問。可以把它視為計算機的硬碟資料,所有資料都永久儲存。
儲存在儲存區(Storage)中的變數,以智慧合約的狀態儲存,並且在函式呼叫之間保持永續性。與其他資料位置相比,儲存區資料位置的成本較高。
6.3、calldata
calldata是不可修改的只讀的非永續性資料位置,所有傳遞給函式的值,都儲存在這裡。此外,calldata是外部函式的引數(而不是返回引數)的預設位置。
0.4的external入參宣告calldata則報錯,0.5及之後的external入參必須宣告calldata否則報錯。
從 Solidity 0.6.9 版本開始,之前僅用於外部函式的calldata位置,現在可以在內部函式使用了。
6.4、stack
堆疊是由EVM (Ethereum虛擬機器)維護的非永續性資料。EVM使用堆疊資料位置在執行期間載入變數。堆疊位置最多有1024個級別的限制。
【總結】
花費gas:storage > memory(calldata) > stack
狀態變數總是儲存在儲存區storage中。(隱式地標記狀態變數的位置)
函式引數(值型別)包括返回引數(值型別)都儲存在記憶體memory中
值型別的區域性變數:棧(stack)
值型別的區域性變數儲存在記憶體中。但是,對於引用型別,需要顯式地指定資料位置
不能顯式宣告具有值型別的區域性變數為memory還是storage
七、事件event
定義使用event,類似一個函式宣告,呼叫試圖emit,
contract Test { event testEvent(uint a, uint b, uint c, uint result); function calc(uint a, uint b, uint c) public returns(uint) { uint result = a ** b ** c; emit testEvent(a, b, c, result); return result; } }
事件會輸出在logs中,
[ { "from": "0x19a0870a66B305BE9917c0F14811C970De18E6fC", "topic": "0x271cb5fa8dca917938dbd3f2522ef54cf70092ead9e1871225a2b3b407f9a81a", "event": "testEvent", "args": { "0": "2", "1": "1", "2": "3", "3": "8", "a": "2", "b": "1", "c": "3", "result": "8" } } ]
透過給event形參新增indexed,便於事件條件的篩選,indexed不能超過三個,否則編譯出錯,
event testEvent(uint indexed a, uint b, uint c, uint result);
八、單位和全域性變數
時間單位不加預設s,以太幣預設wei,
function testUnit() pure public { require(1 == 1 seconds); require(1 minutes == 60 seconds); require(1 hours == 60 minutes); require(1 days == 24 hours); require(1 weeks == 7 days); require(1 years == 365 days); // years 從 0.5.0 版本開始不再支援 require(1 ether == 1000 finney); require(1 finney == 1000 szabo); // 從0.7.0開始 finney 和 szabo 被移除了 require(1 szabo == 1e12 wei); //require(1 gwei == 1e9); // 0.7.0開始加入gwei }
全域性變數,
pragma solidity ^0.8.0; contract TestGlobalVariable { function test1() public view returns(/**bytes32,*/ uint, uint, address, uint, uint, uint, uint) { return ( // blockhash(block.number - 1), // 指定區塊的區塊雜湊,僅可用於最新的 256 個區塊且不包括當前區塊,否則返回0。0.5移除了block.blockhash block.basefee, // 當前區塊的基礎費用 block.chainid, // 當前鏈 id block.coinbase, // 挖出當前區塊的礦工地址 block.difficulty, // 當前區塊難度 block.gaslimit, // 當前區塊 gas 限額 block.number, // 當前區塊號 block.timestamp // 自 unix epoch 起始當前區塊以秒計的時間戳,0.7.0移除了now ); } function test2() public payable returns(bytes memory, address, bytes4, uint, uint256, uint, address) { return ( msg.data, // 完整的 calldata msg.sender, // 訊息傳送者(當前呼叫) msg.sig, // calldata 的前 4 位元組(也就是函式識別符號) msg.value, // 隨訊息傳送的 wei 的數量 gasleft(), // 剩餘的 gas,0.5移除了msg.gas tx.gasprice, // 交易的 gas 價格 tx.origin // 交易發起者(完全的呼叫鏈) ); } }
九、異常處理
9.1、assert
assert(1 == 1 seconds);
9.2、require
require(1 == 1 seconds);
9.3、revert
if (x != y) revert("x should equal to b");
9.4、try/catch
Solidity0.6版本之後加入。try/catch僅適用於外部呼叫,另外,try大括號內的程式碼是不能被catch捕獲的。
pragma solidity ^0.6.10; contract TestTryCatch { function execute (uint256 amount) external returns(bool){ try this.onlyEvent(amount){ return true; } catch { return false; } } function onlyEvent (uint256 a) public { //code that can revert require(a % 2 == 0, "Ups! Reverting"); } }
【assert和require的選擇】
在EVM裡,處理assert和require兩種異常的方式是不一樣的,雖然他們都會回退狀態,不同點表現在:
-
gas消耗不同。assert型別的異常會消耗掉所有剩餘的gas,而require不會消耗掉剩餘的gas(剩餘的gas會返還給呼叫者)
-
運算子不同。當assert發生異常時,Solidity會執行一個無效操作(無效指令0xfe)。當發生require型別的異常時,Solidity會執行一個回退操作(REVERT指令0xfd)
-
優先使用require()
-
用於檢查使用者輸入。
-
用於檢查合約呼叫返回值,如require(external.send(amount))。
-
用於檢查狀態,如msg.send == owner。
-
通常用於函式開頭。
-
不知道使用哪一個的時候,就使用require。
-
優先使用assert()
-
用於檢查溢位錯誤,如z = x + y; assert(z >= x);
-
用於檢查不應該發生的異常情況。
-
用於在狀態改變之後,檢查合約狀態。
-
儘量少使用assert。
-
通常用於函式中間或者尾部。
十、過載
// 過載 function addNums(uint x, uint y) public pure returns(uint) { return x + y; } function addNums(uint x, uint y, uint z) public pure returns(uint) { return x + y + z; }
十一、繼承
子合約is父合約的格式,
pragma solidity ^0.4.25; contract TestExtendA { // 父類要寫在子類之前 uint public a; constructor() public { a = 1; } } contract TestExtend is TestExtendA { uint public b; constructor() public { b = 2; } }
直接在繼承列表中指定基類的構造引數,
contract A { // 父類要寫在子類之前 uint public x; constructor(uint _a) public { // 帶參構造 x = _a; } } contract B is A(1) { // 指定父類構造引數 uint public y; constructor() public { y = 2; } }
透過派生合約(子類)的建構函式中使用修飾符方式呼叫基類合約,
contract A { // 父類要寫在子類之前 uint public x; constructor(uint _a) public { // 帶參構造 x = _a; } } // 方式一: contract B1 is A { uint public b; constructor() A(1) public { // 子類構造使用父類帶參修飾符A(1) b = 2; } } // 方式二: contract B2 is A { uint public b; constructor(uint _b) A(_b / 2) public { // 子類帶參構造使用父類帶參修飾符A(_b / 2) b = _b; } }
連續繼承、多重繼承,
/// 連續繼承,Z繼承Y,Y又繼承X contract X { uint public x; constructor() public{ x = 1; } } contract Y is X { uint public y; constructor() public{ y = 1; } } // 如果是多個基類合約之間也有繼承關係,那麼is後面的合約書寫順序就很重要。順序應該是,基類合約在前面,派生合約在後面,否則無法編譯。 // 實際上Z只需要繼承Y就行 contract Z is X,Y { // 所以必須是X,Y而不是Y,X } /// 多重繼承,子類可以擁有多個基類的屬性 contract Father { uint public x = 180; } contract Mother { uint public y = 170; } contract Son is Father, Mother { }
十二、抽象合約
0.6開始支援。
如果一個合約有建構函式,且是內部(internal)函式,或者合約包含沒有實現的函式,這個合約將被標記為抽象合約,使用關鍵字abstract,抽象合約無法成功部署,他們通常用作基類合約。
抽象合約可以宣告一個virtual純虛擬函式,純虛擬函式沒有具體實現程式碼的函式。其函式宣告用;結尾,而不是用{}結尾。
如果合約繼承自抽象合約,並且沒有透過重寫(override)來實現所有未實現的函式,那麼他本身就是抽象合約的,隱含了一個抽象合約的設計思路,即要求任何繼承都必須實現其方法。
//pragma solidity ^0.4.25; // 0.4/0.5不相容 pragma solidity ^0.6.10; abstract contract TestAbstractContract { uint public a; constructor(uint _a) internal { a = _a; } function get () virtual public; }
十三、重寫
0.8以下不支援。
合約中的虛擬函式(函式使用了virtual修飾的函式)可以在子合約重寫該函式,以更改他們在父合約中的行為。重寫的函式需要使用關鍵字override修飾。
pragma solidity ^0.8.0; contract TestOverride { function get() virtual public{} } contract Middle is TestOverride { } contract Inherited is Middle{ function get() public override{ } }
對於多重繼承,如果有多個父合約有相同定義的函式,override關鍵字後必須指定所有的父合約名稱,
contract Base1 { function get () virtual public{} } contract Base2 { function get () virtual public{} } contract Middle2 is Base1, Base2{ // 指定所有父合約名稱 function get() public override( Base1, Base2){ } }
注意:如果函式沒有標記為virtual(除介面外,因為介面裡面所有的函式會自動標記為virtual),那麼派生合約是不能重寫來更改函式行為的。另外,private的函式是不可標記為virtual的。
十四、介面
0.8以下不支援。
介面和抽象合約類似,與之不同的是,介面不實現任何函式,同時還有以下限制:
-
無法繼承其他合約或者介面
-
無法定義建構函式
-
無法定義變數
-
無法定義結構體
-
無法定義列舉
pragma solidity ^0.8.0; interface TestInterface { function transfer (address recipient, uint amount) external; } contract TestInterfaceSon { function transfer(address recipient, uint amount) public { } }
就像繼承其他合約一樣,合約可以繼承介面,介面中的函式會隱式地標記為virtual,意味著他們會被重寫。
十五、庫
開發合約的時候,總是會有一些函式經常被多個合約呼叫,這個時候可以把這些函式封裝為一個庫,庫的關鍵字用library來定義。
如果合約引用的庫函式都是內部(internal)函式,那麼編譯器在編譯合約時,會把庫函式的程式碼嵌入到合約裡,就像合約自己實現了這些函式,這時並不會單獨部署。
pragma solidity >=0.4.0 <0.7.0; //pragma solidity ^0.4.25; library TestLibrary{ function add (uint a,uint b) internal pure returns (uint){ uint c = a + b; require(c > a, "SafeMath: addition overflow"); return c; } }
庫的呼叫,
contract Test { function add (uint x, uint y) public pure returns(uint){ return TestLibrary.add(x, y); // 呼叫庫 } }
除了使用上面的TestLibrary.add(x, y)這種方式來呼叫庫函式,還有一個是使用using LibA for B這種附著庫的方式。
它表示把所有LibA的庫函式關聯到資料型別B,這樣就可以在B型別直接呼叫庫函式。
contract Test { using TestLibrary for uint; //using TestLibrary for *; function add2 (uint x,uint y) public pure returns (uint){ return x.add(y); // uint的資料x就可以直接呼叫add(y) } }
0x1、示例程式碼
在下,基於不同版本語法的差異,分別用三個合約檔案基本覆蓋到了以上語法,
-
Test04.sol => ^0.4.25
-
Test06.sol => ^0.6.10
-
Test08.sol => ^0.8.0
1.Test04.sol,
pragma solidity ^0.4.25; //pragma solidity ^0.8.0; /** 1.1.1 */ contract TestBool { bool flag; int num1 = 100; int num2 = 200; // default false function getFlag() public view returns(bool) { return flag; // false } // 非 function getFlag2() public view returns(bool) { return !flag; // true } // 與 function getFlagAnd() public view returns(bool) { return (num1 != num2) && !flag; // true } // 或 function getFlagOr() public view returns(bool) { return (num1 == num2) || !flag; // true } } /** 1.1.2 */ // 整型特性與運算 contract TestInteger { int num1; // 有符號整型 int256 uint num2; // 無符號整型 uint256 function add(uint _a, uint _b) public pure returns(uint) { return _a + _b; } function sub(uint _a, uint _b) public pure returns(uint) { return _a - _b; } function mul(uint _a, uint _b) public pure returns(uint) { return _a * _b; } function div(uint _a, uint _b) public pure returns(uint) { return _a / _b; // 在solidity中,除法是做整除,沒有小數點 } function rem(uint _a, uint _b) public pure returns(uint) { return _a % _b; } function square(uint _a, uint _b) public pure returns(uint) { return _a ** _b; // 冪運算在0.8.0之後,變為右優先,即a ** b ** c => a ** (b ** c) } function max() public view returns(uint) { return uint(-1); // return type(uint).max; // 0.8不再允許uint(-1) } } // 位運算 contract TestBitwise { uint8 num1 = 3; uint8 num2 = 4; function bitAdd() public view returns(uint) { return num1 & num2; } function bitOr() public view returns(uint) { return num1 | num2; } function unBit() public view returns(uint) { return ~num1; } function bitXor() public view returns(uint) { return num1 ^ num2; } function bitRight() public view returns(uint) { return num1 >> 1; } function bitLeft() public view returns(uint) { return num1 << 1; } } /** 1.1.3 - 1.1.6 */ contract TestType { fixed num; ufixed num2; fixed8x8 decimal; // fixedMxN, M表示位寬,必須位8的整數倍,N表示十進位制小數部分的位數 address addr = msg.sender; address addr2 = 0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF; TestBitwise t = TestBitwise(addr); enum ActionChoices { Up, Down, Left, Right } } /** 1.1.7 */ // 固定長度的位元組陣列(靜態),以及轉換為string型別 contract TestBytesFixed { // public 自動生成同名的get方法 bytes1 public num1 = 0x7a; // 1 byte = 8 bit bytes1 public num2 = 0x68; bytes2 public num3 = 0x128b; // 2 byte = 16 bit // 獲取位元組陣列長度 function getLength() public view returns(uint) { return num3.length; // 2 } // 位元組陣列比較 function compare() public view returns(bool) { return num1 < num2; } // 位元組陣列位運算 function bitwise() public view returns(bytes1,bytes1,bytes1) { // 多返回值 return ((num1 & num2), (num1 | num2), (~num1)); } // 先轉為bytes動態陣列,再透過string構造 0x7a7a -> zz function toString(bytes2 _val) public pure returns(string) { bytes memory buf = new bytes(_val.length); for (uint i = 0; i < _val.length; i++) { buf[i] = _val[i]; } return string(buf); } } // 固定長度的位元組陣列(靜態)擴容和壓縮 contract TestBytesExpander { // public 自動生成同名的get方法 bytes6 name = 0x796f7269636b; function changeTo1() public view returns(bytes1) { return bytes1(name); // 0x79 } function changeTo2() public view returns(bytes2) { return bytes2(name); // 0x796f } function changeTo16() public view returns(bytes16) { return bytes16(name); // 0x796f7269636b00000000000000000000 } } /** 1.2.1 */ // 修改string型別的資料 contract TestString { string name = 'yorick'; // 字串可以使用單引號或者雙引號 string name2 = "!@#$%^&"; // 特殊字元佔1個byte string name3 = "張三"; // 中文在string中使用utf8的編碼方式儲存,佔用3個byte -> 切換到0.8則中文字元報錯 // string memory str = unicode"Hello ?"; // 0.7.0支援Unicode字串 // string memory str2 = unicode"\u20ac"; // 0.7.0支援Unicode字串 function getLength() view public returns(uint) { // 不可以直接獲取string的length return bytes(name).length; // 6 } function getLength2() view public returns(uint) { return bytes(name2).length; // 7 } function getLength3() view public returns(uint) { return bytes(name3).length; // 6 } function getElmName() view public returns(bytes1) { // 不可以直接透過陣列下標name[0]獲取 return bytes(name)[0]; // 0x79 } function changeElmName() public { bytes(name)[0] = "h"; } function getName() view public returns(bytes) { return bytes(name); } } /** 1.2.2 */ // 動態的位元組陣列,以及轉換為string型別 contract TestBytesDynamic { bytes public dynamicBytes; function setDynamicBytes(string memory val) public { dynamicBytes = bytes(val); } function getVal() public view returns(string){ return string(dynamicBytes); } } /** 1.2.3 */ // 定長陣列 contract TestArrFixed { uint[5] arr = [1,2,3,4,5]; // 修改陣列元素內容 function modifyElements() public { arr[0] = 12; arr[1] = 14; } // 檢視陣列 function watchArr() public view returns(uint[5]) { return arr; } // 陣列元素求和計算 function sumArr() public view returns(uint) { uint sum = 0; for (uint i = 0; i < arr.length; i++) { sum += arr[i]; } return sum; } // 陣列長度 function getLength() public view returns(uint) { return arr.length; } // delete重置陣列某下標的元素值,不會真正刪除該元素 function deleteElm(uint idx) public { delete arr[idx]; } // delete重置整個陣列 function deleteArr() public { delete arr; } /** 定長陣列不允許改變長度和push // 壓縮陣列後,右側多餘元素被丟棄 function changeLengthTo1() public { arr.length = 1; } // 擴容陣列後,右側元素補0 function changeLengthTo10() public { arr.length = 10; } // 追加新元素 function pushElm(uint _elm) public { arr.push(_elm); } */ } // 變長陣列 contract TestArrDynamic { uint[] arr = [1,2,3,4,5]; // 檢視陣列 function watchArr() public view returns(uint[] memory) { return arr; } // 陣列長度 function getLength() public view returns(uint) { return arr.length; } // 壓縮陣列後,右側多餘元素被丟棄 function changeLengthTo1() public { arr.length = 1; } // 擴容陣列後,右側元素補0 function changeLengthTo10() public { arr.length = 10; } // 追加新元素 function pushElm(uint _elm) public { arr.push(_elm); } /** // 彈出元素 function popElm(uint _elm) public { arr.pop(); // 0.4不支援pop } */ // delete重置陣列某下標的元素值,不會真正刪除該元素 function deleteElm(uint idx) public { delete arr[idx]; } // delete重置整個陣列 function deleteArr() public { delete arr; } } /** 二維陣列 solidity的二維陣列與其他語言不同,[2] [3]表示3行2列,而其他語言為2行3列; 二維動態陣列與一維陣列類似,可以改變其陣列長度; */ contract TestArr2Dimensional { uint[2][3] arr = [[1,2],[3,4],[5,6]]; function getRowSize() public view returns(uint) { return arr.length; // 3 } function getColSize() public view returns(uint) { return arr[0].length; // 2 } function watchArr() public view returns(uint[2][3]) { return arr; // 1,2,3,4,5,6 } function sumArr() public view returns(uint) { uint sum = 0; for (uint i = 0; i < getRowSize(); i++) { for (uint j = 0; j < getColSize(); j++) { sum += arr[i][j]; } } return sum; } function modifyArr() public { arr[0][0] = 99; } } // 陣列字面值 contract TestArrLiteral { // 最小儲存匹配,未超過255,所以使用uint8儲存 function getLiteral8() pure public returns(uint8[3]) { return [1,2,3]; } // 超過255,所以使用uint16儲存 function getLiteral16() pure public returns(uint16[3]) { return [256,2,3]; // [255,2,3] 不被uint16允許 } // 強制轉換為uint256 function getLiteral256() pure public returns(uint[3]) { return [uint(1),2,3]; // 給任意元素強轉即可,否則不被允許 } // 計算外界傳入的內容 function addLiterals(uint[3] arr) pure external returns(uint) { uint sum = 0; for (uint i = 0; i < arr.length; i++) { sum += arr[i]; } return sum; } } /** 1.2.4 */ // 結構體初始化的兩種方法 contract TestStruct { struct Student { uint id; string name; mapping(uint=>string) map; } // 預設為storage型別,只能透過storage型別操作結構體中的mapping型別資料 Student storageStu; // mapping型別可以不用在定義的時候賦值,而其他型別不賦值則會報錯 function init() public pure returns(uint, string) { Student memory stu = Student(100, "Jay"); return (stu.id, stu.name); } function init2() public pure returns(uint, string) { Student memory stu = Student({name: "Jay", id: 100}); return (stu.id, stu.name); } function init3() public returns(uint, string, string) { Student memory stu = Student({name: "Jay", id: 100}); // 直接操作結構體中的mapping不被允許: Student memory out of storage // stu.map[1] = "artist"; // 透過storage型別的變數操作結構體中的mapping storageStu = stu; storageStu.map[1] = "artist"; return (storageStu.id, storageStu.name, storageStu.map[1]); } // 結構體作為入參時,為memory型別,並且不能使用public或external修飾函式:Internal or recursive type is not allowed for public or external functions. // 賦值時也要指定為memory,否則報錯 function testIn(Student stu) internal returns(uint) { return stu.id; } // 結構體作為出參,同樣只能private或internal宣告內部使用 function testOut(Student stu) private returns(Student) { return stu; } } /** 1.3 */ contract TestMapping { mapping(address => uint) private scores; // <學生,分數>的單層對映 mapping(address => mapping(bytes32 => uint8)) private _scores; // <學生,<科目,分數>>的兩層對映 function getScore() public view returns(address, uint) { address addr = msg.sender; return (addr, scores[addr]); } function setScore() public { scores[msg.sender] = 100; } } /** 二 */ contract TestAccessCtrl { constructor () public {} uint public num1 = 1; // 自動為public生成同名的get函式,但在編碼時不可直接呼叫num1() uint private num2 = 2; uint num3 = 3; // 不寫則預設private function funcPublic() public returns(string) { return "public func"; } function funcPrivate() private returns(string) { return "private func"; } function funcInternal() internal returns(string) { return "internal func"; } function funcExternal() external returns(string) { return "external func"; } function test1(uint choice) public returns(string) { if (choice == 1) return funcPublic(); if (choice == 2) return funcPrivate(); if (choice == 3) return funcInternal(); //if (choice == 4) return funcExternal(); // external不允許直接在內部用 if (choice == 4) return this.funcExternal(); // 間接透過this才可以呼叫external } } contract TestAccessCtrlSon is TestAccessCtrl { function test2(uint choice) public returns(string) { if (choice == 1) return funcPublic(); // public允許派生合約使用 //if (choice == 2) return funcPrivate(); // private不允許派生合約使用 if (choice == 3) return funcInternal(); // internal允許派生合約使用 //if (choice == 4) return funcExternal(); // external也不允許派生合約直接使用 } } contract TestAccessCtrl2 { function test2(uint choice) public returns(string) { TestAccessCtrl obj = new TestAccessCtrl(); if (choice == 4) { return obj.funcExternal(); // external只允許在外部合約中這樣間接呼叫 } else return "0x0"; } } /** 三 */ contract TestFuncDecorator { uint public num = 1; /// pure function testPure(uint _num) public pure { //uint num1 = num; // pure不允許讀狀態變數 //num = _num; // pure不允許修改狀態變數 } /// view function testView(uint _num) public view { uint num1 = num; // 允許讀狀態變數 num = _num; // 0.4語法上允許修改狀態變數,但實際不會修改,所以num還是1 // 0.5及之後不允許在view中這樣修改,否則編譯不透過 } /// payable function () public payable {} function getBalance() public view returns(uint) { // balance獲取合約地址下的以太幣餘額 return address(this).balance; } // 充值函式payable,只有新增這個關鍵字,才能在執行這個函式時,給這個合約充以太幣,否則該函式自動拒絕所有傳送給它的以太幣 function testPayable() payable public { // transfer轉賬 address(this).transfer(msg.value); } } /** 四 */ contract TestConstruct1 { address private _owner; constructor() public { _owner = msg.sender; } /**constructor(int num) public { // 過載構造->編譯錯誤 _owner = msg.sender; }*/ /** function TestFuncDecorator(uint x) {} // 0.5之前還可以用同名函式定義 */ } contract TestConstruct2 { uint public num; constructor(uint x) public { // 帶參構造,在deploy時傳入 num = x; } } /** 五 */ contract TestModifier { address private _owner; bool public endFlag; // 執行完test後的endFlag仍是true constructor() public { _owner = msg.sender; } modifier onlyOwner { // 許可權攔截器,非合約部署賬號執行test()則被攔截 require(_owner == msg.sender, "Auth: only owner is authorized."); _; // 類似被代理的test()方法呼叫 endFlag = true; } function test() public onlyOwner { endFlag = false; } } /** 七 */ contract TestEvent { event testEvent(uint indexed a, uint indexed b, uint indexed c, uint result); // indexed不能超過三個 function calc(uint a, uint b, uint c) public returns(uint) { uint result = a ** b ** c; emit testEvent(a, b, c, result); // 事件會輸出在logs中 return result; } } /** 八 */ contract TestUnit { function testUnit() pure public { require(1 == 1 seconds); require(1 minutes == 60 seconds); require(1 hours == 60 minutes); require(1 days == 24 hours); require(1 weeks == 7 days); require(1 years == 365 days); // years 從 0.5.0 版本開始不再支援 require(1 ether == 1000 finney); require(1 finney == 1000 szabo); require(1 szabo == 1e12 wei); //require(1 gwei == 1e9); // 0.7.0開始加入gwei } } /** 九 */ contract TestException { function testAssert(int x) public pure { assert(x >= 0); } function testRequire(int x) public pure { require(x >= 0); //require(x >= 0, "x < 0"); } function testRevert(int x, int y) public pure { if (x != y) revert("x should equal to b"); } } /** 十 */ contract TestOverload { // 過載 function addNums(uint x, uint y) public pure returns(uint) { return x + y; } function addNums(uint x, uint y, uint z) public pure returns(uint) { return x + y + z; } } /** 十一 */ contract TestExtendA { // 父類TestExtendA要寫在子類TestExtend之前 uint public a; constructor() public { a = 1; } } contract TestExtend is TestExtendA { uint public b; constructor() public { b = 2; } } /// 直接在繼承列表中指定基類的構造引數 contract A { // 父類要寫在子類之前 uint public x; constructor(uint _a) public { // 帶參構造 x = _a; } } contract B is A(1) { // 指定父類構造引數 uint public y; constructor() public { y = 2; } } /// 透過派生合約(子類)的建構函式中使用修飾符方式呼叫基類合約 // 方式一: contract B1 is A { uint public b; constructor() A(1) public { // 子類構造使用父類帶參修飾符A(1) b = 2; } } // 方式二: contract B2 is A { uint public b; constructor(uint _b) A(_b / 2) public { // 子類帶參構造使用父類帶參修飾符A(_b / 2) b = _b; } } /// 連續繼承,Z繼承Y,Y又繼承X contract X { uint public x; constructor() public{ x = 1; } } contract Y is X { uint public y; constructor() public{ y = 1; } } // 如果是多個基類合約之間也有繼承關係,那麼is後面的合約書寫順序就很重要。順序應該是,基類合約在前面,派生合約在後面,否則無法編譯。 // 實際上Z只需要繼承Y就行 contract Z is X,Y { // 所以必須是X,Y而不是Y,X } /// 多重繼承,子類可以擁有多個基類的屬性 contract Father { uint public x = 180; } contract Mother { uint public y = 170; } contract Son is Father, Mother { } /** 十五 */ library TestLibrary{ function add (uint a,uint b) internal pure returns (uint){ uint c = a + b; require(c > a, "Math: addition overflow"); return c; } } // 庫的呼叫 contract TestLibraryCall { function add(uint x, uint y) public pure returns(uint){ return TestLibrary.add(x, y); // 呼叫庫 } } // 除了使用上面的TestLibrary.add(x, y)這種方式來呼叫庫函式,還有一個是使用using LibA for B這種附著庫的方式。 // 它表示把所有LibA的庫函式關聯到資料型別B,這樣就可以在B型別直接呼叫庫函式。 contract TestLibraryUsing { using TestLibrary for uint; //using TestLibrary for *; function add2(uint x,uint y) public pure returns (uint){ return x.add(y); // uint的資料x就可以直接呼叫add(y) } }
2.Test06.sol,
pragma solidity ^0.6.10; /** 1.2.3 */ contract TestArrDynamic { uint[] arr = [1,2,3,4,5]; // 彈出元素 function popElm() public { arr.pop(); } function watchArr() public view returns(uint[] memory) { return arr; } /** 0.6開始不再可以透過修改length改變陣列長度,需要透過push(),push(value),pop的方式,或者賦值一個完整的陣列 */ /**function changeLengthTo1() public { arr.length = 1; }*/ } /** 九 */ // Solidity0.6版本之後加入。try/catch僅適用於外部呼叫,另外,try大括號內的程式碼是不能被catch捕獲的。 contract TestTryCatch { function execute (uint256 amount) external returns(bool) { try this.run(amount) { // 這裡的函式異常會被捕獲 return true; // 這裡的異常不再會被捕獲 } catch { return false; } } function run(uint256 a) public { //code that can revert require(a % 2 == 0, "Ups! Reverting"); } } /** 十二 */ // 0.6後支援。抽象合約不能使用new建立 abstract contract TestAbstractContract { uint public a; constructor(uint _a) internal { a = _a; } function get () virtual public; }
3.Test08.sol,
pragma solidity ^0.8.0; /** 1.2.1 */ contract TestString { function test() public view returns(string memory, string memory, string memory) { string memory str = unicode"Hello ?"; // Hello ? string memory str2 = unicode"\u20ac"; // € string memory str3 = hex"414243444546474849"; // ABCDEFGHI // string memory name3 = "張三"; // 0.8不允許中文字元,必須改為unicode return (str, str2, str3); } } /** 八 */ contract TestGlobalVariable { function test1() public view returns(/**bytes32,*/ uint, uint, address, uint, uint, uint, uint) { return ( // blockhash(block.number - 1), // 指定區塊的區塊雜湊,僅可用於最新的 256 個區塊且不包括當前區塊,否則返回0。0.5移除了block.blockhash block.basefee, // 當前區塊的基礎費用 block.chainid, // 當前鏈 id block.coinbase, // 挖出當前區塊的礦工地址 block.difficulty, // 當前區塊難度 block.gaslimit, // 當前區塊 gas 限額 block.number, // 當前區塊號 block.timestamp // 自 unix epoch 起始當前區塊以秒計的時間戳,0.7.0移除了now ); } function test2() public payable returns(bytes memory, address, bytes4, uint, uint256, uint, address) { return ( msg.data, // 完整的 calldata msg.sender, // 訊息傳送者(當前呼叫) msg.sig, // calldata 的前 4 位元組(也就是函式識別符號) msg.value, // 隨訊息傳送的 wei 的數量 gasleft(), // 剩餘的 gas,0.5移除了msg.gas tx.gasprice, // 交易的 gas 價格 tx.origin // 交易發起者(完全的呼叫鏈) ); } } /** 十三 */ // 合約中的虛擬函式(函式使用了virtual修飾的函式)可以在子合約重寫該函式,以更改他們在父合約中的行為。重寫的函式需要使用關鍵字override修飾。 // 0.8以下不支援。 contract TestOverride { function get() virtual public{} } contract Middle is TestOverride { } contract Inherited is Middle { function get() public override { } } // 對於多重繼承,如果有多個父合約有相同定義的函式,override關鍵字後必須指定所有的父合約名稱 contract Base1 { function get() virtual public {} } contract Base2 { function get() virtual public {} } contract Middle2 is Base1, Base2 { // 指定所有父合約名稱 function get() public override (Base1, Base2){ } } /** 十四 */ // 0.8以下不支援。 interface TestInterface { function transfer(address recipient, uint amount) external; } contract TestInterfaceSon { function transfer(address recipient, uint amount) public { } }
0x2、各版本主要變化
0.5.0
-
sha3改用keccak256, keccak256只允許接收一個引數,使用abi.encodePacked等組合params
-
建構函式由同名空參方法變成constructor
0.6.0
-
僅標記virtual的介面才可以被覆蓋,覆蓋時需要使用新關鍵字override,如果多個基類同方法名時,需要像這樣列出 override(Base1, Base2)
-
不能透過修改length來修改陣列長度,需要透過push(),push(value),pop的方式,或者賦值一個完整的陣列
-
使用abstract標識抽象合約,抽象合約不能使用new建立
-
回撥函式由function()拆分為fallback()和receive()
-
新增try/catch,可對呼叫失敗做一定處理
-
陣列切片,例如: abi.decode(msg.data[4:], (uint, uint)) 是一個對函式呼叫payload進行解碼底層方法
-
payable(x) 把 address 轉換為 address payable
0.7.0
-
call方式呼叫方法由x.f.gas(1000).value(1 ether)(arg1,arg2)改成 x.f{gas:1000,value:1 ether}(arg1,arg2)
-
now 不推薦使用,改用block.timestamp
-
gwei增加為關鍵字
-
字串支援ASCII字元,Unicode字串
-
建構函式不在需要 public修飾符,如需防止建立,可定義成abstract
-
不允許在同一繼承層次結構中具有同名同引數型別的多個事件
-
using A for B,只在當前合約有效, 以前是會繼承的,現在需要使用的地方,都得宣告一次
0.8.0
-
棄用safeMath,預設加了溢位檢查,如需不要檢查使用 unchecked { ... } , 可以節省丟丟手續費
-
預設支援ABIEncoderV2,不再需要宣告
-
求冪是右結合的,即表示式a**b**c被解析為a**(b**c)。在 0.8.0 之前,它被解析為(a**b)**c
-
assert 不在消耗完 完整的gas,功能和require基本一致,但是try/catch錯誤裡面體現不一樣,還有一定作用…
-
不再允許使用uint(-1),改用type(uint).max
版權宣告:本文為CSDN博主「DayDayUp丶」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處連結及本宣告。
原文連結:
https://blog.csdn.net/songzehao/article/details/126294980
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70012206/viewspace-2926415/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Solidity語言學習筆記————1、初識Solidity語言Solid筆記
- 初談學習的大致路徑
- Solidity學習疑問總結Solid
- 初學Solidity(四):Solidity的庫Solid
- 初學Solidity(三):Solidity物件導向Solid物件
- html初學總結HTML
- ES6 語法學習總結
- 初學Solidity(六):Solidity異常處理Solid
- 韓語學習經驗總結,給韓語初學者指路
- HTML 語法總結HTML
- sql語法總結SQL
- Solidity初學-0.8新特性Solid
- 初學Solidity(五):Solidity的事件與檔案Solid事件
- 初學 PHP 總結建立物件PHP物件
- Go語言基礎語法總結Go
- 初學Python(1)基礎語法Python
- go 奇葩語法總結篇Go
- PHP高階語法總結PHP
- open policy agent 語法總結
- HTML標記語法總結HTML
- es6語法總結
- HTML5語法總結HTML
- Yii學習系列之yii大致結構
- iOS初學之填坑總結iOS
- AST語法結構樹初學者完整教程AST
- Solidity語言學習筆記————15、結構體StructSolid筆記結構體Struct
- python 語法總結:Python語法快速入門Python
- ES6常用語法總結
- css基本語法總結及使用CSS
- selenium之xpath語法總結
- Emmet外掛常用語法總結
- Hibernate-hql語法總結.
- 常用CSS縮寫語法總結CSS
- 05-ES6語法總結
- ES6個人初學總結-XY
- Solidity語言學習筆記————38、Solidity彙編Solid筆記
- 初級進階版SQL語句總結(1)SQL
- 鵬哥C語言初識課程總結C語言