智能合約 Solidity 教學 (上) - Solidity 基礎語法教學
📅 發布時間: 2025-01-29 🏷️ 標籤: Solidity, 智能合約, 基礎語法, 以太坊, Remix IDE
在這篇文章中,我們會從 0 基礎開始,逐步介紹智能合約的概念、Solidity 語法,以及如何部署你的第一個智能合約。適合對區塊鏈開發有興趣但還沒有 Solidity 經驗的開發者!
目錄
- 智能合約 Solidity 教學 (上) - Solidity 基礎語法教學
- 智能合約 Solidity 教學 (中) - MetaMask 與以太坊測試網
- 智能合約 Solidity 教學 (下) - Web3.js 實際開發一個 DApp 去中心化應用前端
一、Solidity 0 到 1:智能合約入門
什麼是 Solidity?
Solidity 是一種面向智能合約的編程語言,專門用於在 Ethereum(以太坊) 和其他 EVM(Ethereum Virtual Machine)兼容的區塊鏈上開發去中心化應用(DApps)。
它受到了 JavaScript、Python 和 C++ 的影響,語法簡潔且適合初學者入門。
智能合約是什麼?
智能合約(Smart Contract)是一種 自動執行的合約,當滿足特定條件時,程式會自動執行約定的內容,無需中間人。
例如:
- 去中心化交易所(DEX) – 允許用戶直接交易代幣
- NFT 合約 – 用於發行、管理和交易 NFT
- 去中心化金融(DeFi)應用 – 如貸款、流動性挖礦等
當然不僅僅是這種與金融有關的產業,任何可以去中心化的應用都可以採用智能合約,好比說有些產業為了更好的紀錄各個生產鏈上下的資料,便可以用智能合約,公正無私的紀錄資料。
二、開始使用 Solidity
1. 安裝開發環境
目前最簡單的方式是使用線上編輯器 Remix IDE:
- 網址:https://remix.ethereum.org/
- 不需要安裝,打開瀏覽器即可編寫與部署智能合約。
如果你想在本地開發,可以使用:
- Node.js(安裝 npm/pnpm)
- Hardhat 或 Truffle(Solidity 開發框架)
另外你會需要一個加密貨幣錢包來與合約互動(放心,在本教學中不會花到任何一毛錢)
- MetaMask 錢包(測試和與合約互動)
三、第一個合約 Hello World
💻 第一個智能合約:HelloWorld
🎯 學習目標:開啟 Remix IDE 並建立新檔案理解 SPDX-License、pragma、contract、變數、建構子、函式等基礎語法逐步建構並解釋每個關鍵元素

圖: 開啟 Remix IDE (https://remix.ethereum.org)

圖: 在 Remix 建立新檔 HelloWorld.sol
步驟 1: 完整範本程式碼
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract HelloWorld {
// 儲存訊息的變數
string public message;
// 建構函式,合約部署時會自動執行
constructor() {
message = "Hello, Solidity!";
}
// 設定新的訊息
function setMessage(string memory _newMessage) public {
message = _newMessage;
}
}步驟 2: SPDX-License-Identifier 和 pragma solidity
// SPDX-License-Identifier: MIT // [註解] 宣告授權協議 MIT,讓他人知道可以自由使用此程式碼,為什麼?為了符合開源規範並避免法律糾紛
pragma solidity ^0.8.0; // [註解] 指定 Solidity 編譯器版本,為什麼?確保新舊版本相容,避免語法不相容導致的編譯錯誤步驟 3: contract HelloWorld
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract HelloWorld { // [註解] 定義合約名稱,為什麼?這是智能合約的入口,所有邏輯都在此封裝,便於部署和管理
// ...
} // [註解] 合約結束,為什麼?將所有變數和函式封閉在合約內,確保獨立執行步驟 4: 狀態變數與建構子
contract HelloWorld {
// 儲存訊息的變數 // [註解] public 變數,為什麼?自動產生 getter 函式,讓外部可讀取狀態,持久儲存在區塊鏈上
string public message; // [註解] string 型別儲存文字,為什麼?適合訊息內容,public 確保可查詢
// 建構函式,合約部署時會自動執行 // [註解] constructor 特殊函式,為什麼?初始化合約狀態,只執行一次,避免預設值不正確
constructor() { // [註解] 無參數建構子,為什麼?部署時自動呼叫,設定初始值
message = "Hello, Solidity!"; // [註解] 初始化變數,為什麼?提供預設訊息,讓合約一部署就有可用狀態
} // [註解] 建構子結束,為什麼?完成初始化,後續函式才可依賴此狀態
}步驟 5: setMessage 函式
// ... (前面的程式碼保持不變)
// 設定新的訊息 // [註解] 公開函式,為什麼?允許外部使用者更新狀態,實現互動功能
function setMessage(string memory _newMessage) public { // [註解] public 修飾符和 memory 位置,為什麼?public 允許呼叫,memory 避免永久儲存參數以節省 Gas
message = _newMessage; // [註解] 更新狀態變數,為什麼?實現動態修改,改變區塊鏈狀態(會消耗 Gas)
} // [註解] 函式結束,為什麼?完成邏輯,返回控制權給呼叫者
}四、編譯與部署
💻 編譯與部署 HelloWorld 合約
🎯 學習目標:使用 Remix Compiler 編譯合約在 Remix VM 環境部署並測試合約理解部署後的互動介面與 Gas 消耗

圖: 點擊 Remix 右側面板 Solidity Compiler

圖: 選擇正確版本 (0.8.x) 並按下 Compile HelloWorld.sol

圖: 沒有錯誤就會顯示綠色打勾

圖: 轉到 Deploy & Run Transactions 面板

圖: Environment 選擇 Remix VM (Cancun),選正確合約並點擊 Deploy

圖: 下方出現 Deployed Contracts 表示成功,注意測試 ETH 扣除(測試幣無限使用,合約永久存在區塊鏈)

圖: 展開 HelloWorld,點擊 message 查看初始值(讀取免費)

圖: 輸入新訊息如「貓貓真可愛」,點擊 setMessage 更新(修改需 Gas)
五、Solidity 中的變數型別
💻 Solidity 變數型別範例
🎯 學習目標:理解 bool, uint/int, address, string, array, struct, enum 等型別認識各型別的限制與用途
步驟 1: bool 型別
bool public myBool = true; // [註解] 布林值,為什麼?用於條件判斷如開關狀態,只存 true/false 以節省儲存空間步驟 2: 整數型別
uint256 public myUint = 123; // [註解] 無符號整數,為什麼?適合計數或正值,uint256 提供大範圍避免溢位
int public myInt = -123; // [註解] 有符號整數,為什麼?允許負值,如債務計算步驟 3: address 型別
address public myAddress = 0x1234567890123456789012345678901234567890;
myAddress.balance // 取得該錢包目前剩餘的存款 // [註解] 地址型別,為什麼?代表錢包或合約,用於轉帳或權限檢查,.balance 查詢餘額步驟 4: string 型別
string public myString = "Hello";
myString.length; //error
myString[0]; //error // [註解] 字串型別,為什麼?儲存文字但無 length 或索引存取,以 Gas 效率為優先,避免額外計算步驟 5: 陣列
uint[] public numbers; // 動態大小的陣列 // [註解] 動態陣列,為什麼?大小可變,push 新增元素適合列表
uint[5] public fixedSizeNumbers; // 大小固定為 5 的陣列
numbers.push(123); // [註解] 新增,為什麼?擴展陣列但消耗 Gas,因改變 storage步驟 6: struct
struct Person {
string name;
uint age;
} // [註解] 自定義結構,為什麼?組合多型別成一單位,如使用者資料,提升程式可讀性
Person public alice = Person("Alice", 30); // [註解] 實例化,為什麼?初始化結構體,提供完整物件步驟 7: enum
enum Status { Pending, Shipped, Completed, Rejected } // [註解] 列舉,為什麼?定義有限狀態集,避免魔法數字,提升維護性
Status public currentStatus; // [註解] 使用,為什麼?儲存狀態,易讀取如 currentStatus = Status.Shipped六、變數類型:Local、State、Global
在 Solidity 中的變數根據其宣告位置可以分為 三大類:
- Local 變數(函式內部變數)
- State 變數(合約狀態變數)
- Global 變數(區塊鏈資訊變數)
1. Local 變數
定義:
- 宣告在函式內部
- 僅存於函式執行期間(不會永久儲存到區塊鏈)
- 默認存於記憶體(memory)
- 不會影響智能合約的狀態,因此不會產生 Gas 費用
2. State 變數
定義:
- 宣告在函式之外,屬於整個合約
- 儲存在區塊鏈上(storage)
- 修改 State 變數時,會消耗 Gas(因為改變了區塊鏈狀態)
3. Global 變數
定義:
- 提供區塊鏈上的資訊
- 不需要手動宣告,直接可用
- 儲存方式根據類型不同,有些來自 storage,有些只是暫時變數
常見 Global 變數:
msg.sender呼叫合約的發送者地址msg.value交易發送的 ETH 數量block.timestamp當前區塊的時間戳block.number當前區塊號gasleft()剩餘 Gas 數量tx.origin原始交易發起者address(this).balance合約帳戶的 ETH 餘額
七、Data Locations:memory、storage、calldata
除了變數的類型(local、state、global),Solidity 也提供了 三種資料儲存方式
適用於陣列 (array)、結構 (struct)、字串 (string)、映射 (mapping) 等參考型別 (Reference Types)。
storage表示這個資料永遠儲存於區塊鏈上頭,**改變會消耗 Gas,**剛剛提到的 State 變數就屬於這一類
memory表示這個資料只是短暫出現在記憶體上,**交易結束後消失,**剛剛提到的 Local 變數就屬於這一類
calldata只讀記憶體(交易輸入)外部函式參數專用,不允許修改external 函式的輸入參數
八、瓦斯費,什麼是 Gas?
眼尖的同學應該發現,如果你修改了區塊鏈的資料是要花錢的!
不僅如此,在以太坊上,每當你執行一筆交易(例如呼叫智能合約、轉帳等),都需要向網路支付一筆 交易手續費。
這筆費用的計算方式並不是固定的,基本上對區塊鏈做越多操作會越貴。
而以太坊使用一種叫做 Gas 的單位來衡量交易在網路上執行所需的運算資源。
- 付款時使用的是 以太幣 (ETH),當然1以太幣快 3000 美元,單位太大了,所以瓦斯費習慣以以 Gwei(10^-9 ETH)為單位計價。
- 哪怕是相同的操作,在不同時間段的瓦斯費都不同,在尖峰時刻的瓦斯費會比較昂貴。
計算交易手續費 (以下僅供參考)
- Gas Used:某次交易實際執行用掉多少 Gas
- Gas Price:每單位 Gas 的價格,通常以 Gwei 為單位
- 交易手續費 (Tx Fee) = Gas Used × Gas Price
舉例:
- 你的交易執行消耗了 50,000 Gas
- 當前 Gas Price = 20 Gwei = 20 × 10−9^{-9} ETH
- 交易費用 = 50,000 × (20 × 10^-9) ETH = 0.001 ETH
哪些操作會消耗 Gas?
在智能合約中,大多數操作都會消耗 Gas,尤其是對區塊鏈「狀態 (state)」造成變化的操作。
下面列出一些常見「高 Gas 消耗」的動作:
- 寫入/修改區塊鏈上儲存 (storage)
- 最昂貴 的操作之一就是對合約的
storage做 寫入 或 更新。 - 例如:改變一個 state variable。
- 原因:永久儲存到區塊鏈資料庫,需要網路中所有節點同步保存。
- 最昂貴 的操作之一就是對合約的
- 新增資料到 mapping 或 array
- 與一般的
storage更新類似,只要有寫入都會消耗較多 Gas。 - 動態陣列的
push()也會往合約的永續狀態中寫入新元素。
- 與一般的
- 刪除資料 (釋放 storage)
- 相較寫入,可以回收部分 Gas(因為釋放空間會有 Gas Refund),但一般來說整體仍需付出操作成本。
- 合約部署
- 部署(Deploy)是一項非常昂貴的操作,因為需要將整個智能合約程式碼上傳到區塊鏈。
- 智能合約一旦部署,就成為鏈上的永久程式碼。
- 呼叫其他合約
- 在合約中透過
call、delegatecall或interface呼叫其他合約函式,需要額外的 Gas 來執行被呼叫的邏輯。
- 在合約中透過
- 觸發事件 (Event)
- 在合約中
emit Event會產生一筆鏈上日誌 (Log),也會消耗 Gas。(一般比寫storage便宜,但還是要考量頻繁記錄事件帶來的額外開銷)
- 在合約中
- 迴圈操作 (for/while loop)
- 每次迭代都需要額外的計算成本。
- 如果迴圈裡包含寫入
storage、呼叫其他合約、更複雜的邏輯,Gas 量會隨迴圈次數成倍增加。
哪些操作相對不會「直接」消耗 Gas?
- 只讀取狀態變數 (view function)
- 用錢包或前端呼叫
view或pure函式,不會執行交易,也不會在鏈上留下任何記錄,因此不需支付額外 Gas。 - 但若在合約內部使用
view或pure函式,仍然要計入該交易整體的執行成本——只是相對來說,只有計算的成本,但沒有對storage寫入的成本。
- 用錢包或前端呼叫
- 在函式內做記憶體層面的運算 (memory)
- 在
memory或calldata中的操作只會產生交易執行中計算的 Gas,通常比修改storage要便宜許多,因為它不需要永久寫入區塊鏈。
- 在
注意: 雖然「讀取數據」或「單純做計算」比寫入便宜很多,但「執行交易」本身仍有基礎費用,因為必須產生交易並在鏈上被礦工/驗證者驗證。
如何節省 Gas?
- 減少對
storage的寫入- 能在
memory、calldata中處理就盡量不要寫進storage。 - 例如先在
memory中計算好,再一次性寫回storage,或只在需要時更新storage。
- 能在
- 考慮資料結構選擇
mapping在查詢方面通常比array高效(尤其針對索引操作);- 但要根據具體邏輯權衡。
- 避免在迴圈中使用寫入或呼叫
- 如果可以把大量資料處理切分成多筆交易,或使用批量更新/分段的方式,讓單次交易的迴圈不至於過大。
- 事件 (Event) 的使用
- 雖然事件會消耗 Gas,但通常比
storage寫入便宜且可以提供鏈上日誌。 - 取代在鏈上保存大型文字資料,而是用事件紀錄,日後可從鏈下檢索到。
- 雖然事件會消耗 Gas,但通常比
- 版本與編譯器優化
- 使用新版本 Solidity 通常會有更多優化(如 0.8.x),
- 在 Remix 或 Hardhat 中開啟編譯器優化
optimizer(如設置runs = 200或其他數值)。
九、控制流程
💻 Solidity 控制流程範例
🎯 學習目標:使用 if/else 分支判斷實現 for/while 迴圈計算
步驟 1: if / else if / else
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract HelloWorld {
string public result = "";
constructor() {
int256 val = 1;
if (val == 0) {
result = "Value is zero";
} else if (val == 1) {
result = "Value is one";
} else {
result = "Value is something else";
}
}
}步驟 2: for 迴圈
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract HelloWorld {
uint public sum = 0;
constructor() {
uint max = 100;
for (uint i = 0; i < max; i++) {
sum += i;
}
}
}步驟 3: while 迴圈
// ... (前面的程式碼保持不變)
constructor() {
uint max = 100;
uint i = 0; // [註解] 初始化計數器,為什麼?while 需要手動管理迴圈變數,避免無限迴圈
while (i < max) { // [註解] 條件檢查,為什麼?決定是否繼續迴圈,防止 Gas 超限
sum += i; // [註解] 累加運算,為什麼?實現求和邏輯,每次迭代更新狀態(注意 Gas)
i ++; // [註解] 更新計數器,為什麼?推進迴圈進度,確保終止
} // [註解] 迴圈結束,為什麼?完成計算,sum 儲存結果
}十、函式 (Function)
函數就像是一個魔法盒子,我們可以把一系列的指令放進這個盒子裡,替這個盒子取一個名字
然後以後只要叫這個名字,就可以讓這些指令執行。
在 Solidity 中為了更方便使用者開發,我可以替函式加上一些設定
💻 Solidity 函式修飾符與類型
🎯 學習目標:理解 public, private, external 存取權限使用 view, pure, payable 功能修飾符
步驟 1: public 函式
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract HelloWorld {
uint public count;
// 這是一個 public 函式 // [註解] public 權限,為什麼?允許任何人呼叫,自動產生 getter/setter 便利外部互動
function increment() public { // [註解] 無參數 public 函式,為什麼?簡單更新計數,外部可觸發狀態變化
count += 1; // [註解] 修改狀態,為什麼?實現遞增,消耗 Gas 但提供功能
} // [註解] 函式結束,為什麼?原子操作,確保一致性
}步驟 2: private 函式與變數
// ... (前面的程式碼保持不變)
// 外部無法直接訪問該變數 // [註解] private 變數,為什麼?限制外部直接讀寫,但資料仍公開於鏈上(可爬取),用於內部邏輯
// 請注意!該資料仍然公開於區塊鏈上頭,仍然可以爬取到該變數
uint private innerCount; // [註解] private 狀態變數,為什麼?隱藏內部計數,避免外部依賴
// 這是一個 public 函式
function increment() public {
_increment(); // ✅ 合約內部可存取
}
// 這是一個 private 函式,習慣命名上最前面加上 _ 來表示這是 private
function _increment() private { // [註解] private 內部函式,為什麼?封裝邏輯,重用代碼而不暴露細節,提升安全性
count += 1;
}步驟 3: external 函式
// ... (前面的程式碼保持不變)
// 這是一個 public 函式
function increment() public {
this.externalIncrement(); // 內部調用必須透過 `this` // [註解] 透過 this 間接呼叫,為什麼?模擬外部呼叫,但 internal 不直接支援
}
// 這是一個 external 函式 // [註解] external 權限,為什麼?優化 Gas,只允許外部呼叫,適合大量外部互動如 API
function externalIncrement() external {
count += 1;
}步驟 4: view 函式
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract ViewExample {
uint public number = 42; // 狀態變數
// 這個函式只是讀取狀態變數,並沒有改變它
function getNumber() public view returns (uint) { // [註解] view 修飾符,為什麼?保證不改狀態,讀取免交易/Gas,適合查詢
return number;
}
}步驟 5: pure 函式
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract PureExample {
// 這個函式純粹進行計算,沒有存取合約內的狀態變數
function add(uint a, uint b) public pure returns (uint) { // [註解] pure 修飾符,為什麼?純計算無狀態依賴,最大節省 Gas,可離線驗證
return a + b;
}
}步驟 6: payable 函式
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract PayableExample {
uint public balance;
// 這個函式允許用戶發送 ETH 到合約
function deposit() public payable { // [註解] payable 修飾符,為什麼?允許接收 ETH,否則交易失敗,適合付費功能
balance += msg.value; // msg.value 是發送的 ETH 數量 // [註解] 累加接收 ETH,為什麼?追蹤存款,使用 global 變數自動記錄
}
// 這個函式回傳合約內的 ETH 餘額
function getBalance() public view returns (uint) {
return address(this).balance;
}
}十一、錯誤處理 (Error Handling)
💻 Solidity 錯誤處理範例
🎯 學習目標:使用 require 檢查輸入與條件了解 revert 和 assert 的用途
步驟 1: require()
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract PayableExample {
uint public balance;
function withdraw(uint _amount) public {
// _amount 須小於 balance 的金額,否則報錯
require(_amount < balance, "合約中資金不足"); // [註解] require 條件檢查,為什麼?驗證輸入/狀態,失敗時 revert 並退款 Gas,常用權限/邊界檢查
balance -= _amount;
}
}步驟 2: revert()
function doSomething() public {
if (someConditionNotMet()) {
revert("Condition not met"); // [註解] 自訂錯誤,為什麼?明確回滾狀態,提供除錯訊息,比 require 更靈活用於複雜邏輯
}
// ...
}步驟 3: assert()
function testAssert(uint _x) public pure {
assert(_x != 0); // _x 絕對不能為 0 // [註解] assert 不變量檢查,為什麼?偵測程式 bug,失敗不退 Gas(開發用),區別於使用者錯誤
}進一步學習建議
- 掌握測試網部署:嘗試使用 MetaMask 連接測試網 (如 Sepolia、Goerli) 進行部署。
- 熟悉 Hardhat / Truffle:在本地進行單元測試與合約部署管理。
- 安全性考量:了解常見攻擊手法(Reentrancy, Integer Overflow 等)。
- 前端整合:透過 Web3.js 或 Ethers.js 連接你的 DApp 與合約。