📜 [專欄新文章] 類 Python 的合約語言 Vyper 開發入門:與 Solidity 差異、用 Truffle 部署、ERC20 賣幣合約實做
✍️ 田少谷 Shao
📥 歡迎投稿: https://medium.com/taipei-ethereum-meetup #徵技術分享文 #使用心得 #教學文 #medium
有鑒於個人近期關注的 Uniswap 及 Curve 皆用 Vyper 實作,索性瀏覽了官方文件並嘗試一些開發工具,希望此文能減少一些讀者初嘗 Vyper 會遇到的麻煩!
Vyper and Solidity
Outline
一. Vyper 極簡介二. 與 Solidity 語法差異三. 開發、開發環境設置 1. 語法高亮 2. 本地 Vyper compiler 安裝 3. 使用 Truffle 操作 ERC20 - 安裝 Truffle - 發幣 - 寫個簡易賣幣合約四. 已知 Remix 問題 五. 結語
一. Vyper 極簡介
Vyper 是除 Solidity 外,以太坊上的另一智能合約 (Smart contract) 語言。其語法和 Python 相近,但畢竟也是寫合約的語言,邏輯差異不大,所以若熟悉 Solidity 應該不難理解用 Vyper 寫出的合約!
Vyper 主要被設計和 Solidity 的區別是安全性及可讀性,這部分會在下一段落及後方的實作中舉例說明。
二. 與 Solidity 語法差異
Vyper 與 Solidity 的差異有許多,在本段只就個人認為感受較深的三點進行說明,其他差異只進行翻譯,有興趣的讀者可以到官方文件詳細了解:https://vyper.readthedocs.io/en/latest/index.html
1. 沒有 modifier
Solidity 常見的 onlyOwner() modifier; 由於 gist 沒有 Solidity 的語法高亮,故截圖
在 Vyper 中單純用 assert 及 assert_modifiable 來進行條件檢查,兩者差別為若要檢查函數執行後的返還值,要用後者,如下圖:
Vyper 寫法
2. 沒有 Class inheritance 繼承
繼承是物件導向程式設計 (OOP) 的核心概念,但各種繼承關係有時候確實很複雜。Vyper 沒有繼承,這無疑大幅地增加了程式可讀性及安全性,以及降低審計程式碼的難度。在此提供一個例子供不熟悉 OOP 複雜之處的讀者有個概念:
source: https://consensys.github.io/smart-contract-best-practices/recommendations/#multiple-inheritance-caution
在上例中,contract A 的 fee 值 (因繼承自 contract B 和 C,故有 fee 一值) 是 5、a 值也是 5 (因繼承自 contract Final,故有 a 一值)。原因是 A 先繼承 B 再繼承 C,因此 contract A 中的 setFee() 是使用了 contract C 的 setFee(),而 a 值是由於 C(5),這代表 contract C 的 constructor (舊版本中即 function C(),函式名稱同 contract 名稱) 被傳入的值為 5。
稍微延伸一下以上概念,將 contract A 改成:contract A is C, B。如此一來,a 值還有 fee 值都會是 3,因為這次 A 先繼承 C 再繼承 B,因此最終吃到的值是 contract B 的。
以上就是 OOP 繼承的複雜之處的簡單範例說明,應該能稍微感受到爲什麼除去繼承後會大幅提高可讀性及安全性,畢竟即使是熟悉 OOP 的人有時頭腦一混亂也會開始懷疑自己寫的程式碼繼承結構是否正確 …
3. 沒有 dynamic array 動態陣列
這應該是目前 Vyper 設計中爭議最大的部分。沒有動態陣列代表在宣告陣列時需要宣告其長度,也就是說 Solidity 中的寫法 uint[], bool[] 等等,這些是不會出現在 Vyper 的。在 Vyper 中只能出現諸如:
# Vyper 的變數宣告方式為 變數名稱: 存取範圍(變數型態(若為陣列給長度))
values: uint256[10]participants: public(address[20])
可以看到上方的 uint256 及 address 兩陣列皆需要宣告長度,不能不宣告而使其動態地配置空間。
沒有動態陣列固然可以確保執行運算的範圍、次數,但一來動態陣列真的很方便、二來在 Solidity 有此功能而 Vyper 卻沒有的情況下可能會造成麻煩,詳見此一討論串:點我。
4. 沒有 inline assembly,程式碼中不會有組合語言
5. 沒有 function overloading,函式不會因傳入的參數數目不同而結果不同
6. 沒有 operator overloading,運算符號不會有不同於預設的自定義功能
7. 沒有無限迴圈,可免於 gas limit attack
8. 十進位定點數 decimal fixed point 而非二進位 (binary) 定點數,詳見:點我
三. 開發、開發環境設置
結論先講
開發 Vyper 的最佳姿勢目前個人認為是在本地裝上 Vyper compiler、用 Truffle 部署,並在撰寫時將檔名後加上 .py 就能有 Python 的語法高亮👌
1. 語法高亮 (syntax highlighting)
有語法高亮絕對是舒服地寫程式的第一步。
Remix 有 Vyper 的語法高亮,但一來個人目前不推薦使用 Remix 來撰寫 Vyper,原因詳見下方 4. 已知 Remix 問題;二來 Remix 的語法高亮其實也沒有很清楚,因此個人推薦:在本地開發,將檔名後加上 .py 就會有 Python 的語法高亮。
2. 本地 Vyper compiler 安裝
照官方說明使用 Python 的虛擬環境 virtualenv:
source: https://vyper.readthedocs.io/en/latest/installing-vyper.html#installing-vyper
簡單兩點提醒:
如果中間那行報錯但確實已經有 Python,則可能是版本問題。依照自己電腦上的版本改成相應的即可,ex: python3.6 改成 python3
進入虛擬環境後(檔案路徑前方應有 vyper-venv 的提示),使用此指令: vyper {檔案名稱}.vy,即可編譯 .vy 檔;使用完畢後輸入 deactivate 即可退出
3. 使用 Truffle 操作 ERC20
安裝 Truffle
Truffle 雖有冗餘的 migration 但也別無他法,畢竟 Remix 目前仍不完善 :(
下載流程可以照官方文件,使用 vyper-example:
source: https://github.com/truffle-box/vyper-example-box
由於我們會接上測試網 Ropsten,因此還要下載 truffle-hdwallet-provider:
source: https://github.com/trufflesuite/truffle-hdwallet-provider
接者就可以開始使用 Vyper 寫合約了!
發幣
由於 Vyper 的官方文件中已經有許多優質範例,因此本文希望來點不一樣但大家卻又很熟悉的…以 ERC20 為例(這千篇一律的主題xD):
用 Curve 的 ERC20 程式碼為範本,發一個幣(又要發…)
寫一個簡易賣幣合約
選擇這個主題一方面畢竟 ERC20 是以太坊的最大宗應用之一,二來有興趣的讀者可以透過讀 ERC20 的程式碼來熟悉 Vyper,並在看過本文的流程後對於用 Vyper+Truffle 來操作 ERC20 有完整的概念!
好的,首先複製一份 Curve 的 ERC20 程式碼(看到就順手拿來用),並複製到 Truffle 所在路徑的 contracts 資料夾中:https://github.com/curvefi/curve-contract/blob/pool_compound/vyper/ERC20.vy
由於第一點希望著重在跑一次流程,因此不改動合約的程式碼。
將 ERC20.vy 複製到 contracts 資料夾中後,到 migrations 資料夾開啟 2_deploy_contracts.js,首先將 require() 中的參數改為 ERC20.vy 的檔名 ERC20,再來依照自己喜好決定幣的名稱、代號、小數點位數及發行總量,輸入於 deployer.deploy() 中。
接著,為了和測試網 Ropsten 互動,需要將以下程式碼寫入 truffle-config.js。
第二行的 privateKeys 是帳號的私鑰。以下實作需要兩個帳號來操作,因此請從錢包匯入兩組私鑰(並非助憶詞)。
在第 13 行中 HDWalletProvider 此函式的第三個參數代表要用第幾個帳號最為預設帳號(部署合約等),第四個函數代表總共匯入幾組帳號。而第二個參數則是需要至 Infura 申請一個 project 來得到串接 Ropsten 的連結。這兩步驟並非本文重點,因此不詳細解說步驟,Google 搜尋關鍵字應該就會找到方法!
接著,就可以輸入以下指令來將代幣發佈到 Ropsten:
truffle deploy --network ropsten
有進入虛擬環境才可以編譯 .vy 檔,若忘記就會收到如下的錯誤訊息:
記得打開虛擬環境才能編譯 .vy 檔
成功後就可以在 contract address 中看到代幣發佈的位置,加入到 Metamask 中就可以看到。本文的例子是維尼代幣 Winnie the Coin, WTC ;)
contract address 便是 ERC20 的所在
Winnie the Coin, WTC
好了,到此測試網上又多了一個測試用的垃圾廢幣。
寫個簡易賣幣合約
賣幣合約中我想要簡單有兩個功能就好:付錢買幣 、結束銷售,以下就是程式碼。買幣的部分就不寫太詳細,固定價格為 0.01 Ether 可以買 500 代幣。
簡單說明幾點:
Solidity 的 constructor() 在 Vyper 中為 Python 風的 __init__():
函式的屬性(public, private, payable 等等)放在函式上方,與 Python 的修飾器位置相同
總之寫法跟 Python 很像,次方也一樣是用兩次乘法代表:**
變數前加上 self 代表是當前合約的變數/全域變數,因此非常容易與函式中的變數/區域變數做區隔
由於已經在第一行匯入了 ERC20 那份合約,因此透過將地址傳入合約當參數,就可以呼叫在該地址的合約:ERC20(self.tokenAddress) 。並且,可以將部署的合約存成一個變數 erc20 較方便
寫完合約後一樣要更改 migrations 資料夾中的 2_deploy_contracts.js 如下,將代幣所在的地址作為參數輸入。
由於先前已經部署過一次了,因此要重置才能再部署第二次,輸入以下指令:
truffle deploy --reset --network ropsten
部署成功之後就要來試著買幣啦!輸入以下來進入 console:
truffle console --network ropsten
成功進入後應該會看到 truffle(ropsten)> 的字樣。接著,首先取得部署的兩合約,並查看是否有返回合約資訊:
# ERC20 及 SellToken 是先前在 2_deploy_contracts.js 中的變數名稱,代表被部署的合約
let instance1 = await ERC20.deployed()instance1 # 印出 instance1 的資訊
let instance2 = await SellToken.deployed()instance2 # 印出 instance2 的資訊
再來,為了讓 SellToken 可以賣幣,要先用 ERC20 的合約匯幣到 SellToken 的合約。因此,輸入以下指令:
instance1.transfer(instance2.address, 10000)
# 這裡數字只要設為 > 500 就可以
接著,我們要利用第二個帳號去買幣(第一個帳號為預設帳號,因此就是代幣擁有者)。將帳號的資訊存入變數 accounts 中,再指定送出交易的帳號是第二個帳號。由於我個人匯入私鑰的順序是將第一個帳號存在 truffle-config.js 的 privateKeys[0]、第二個帳號存在 privateKeys[1],因此第二個帳號的地址就會在 accounts[1] 的位置:
let accounts = await web3.eth.getAccounts()
instance2.buyToken({from: accounts[1], value: 10000000000000000})
# value 為 10^16 是因為在 SellToken 的 buyToken 函式中買一次要 0.01 Ether, 即為 10^16 wei
然後應該就會在自己的第二個帳號中看到匯入的幣了~
最後,由於合約中結束銷售就是一個自殺 selfdestruct 函式,因此可以呼叫看看,第一個帳戶錢包中的錢應該會增加,因為第二個帳戶有付款買幣;並且,可以到 Ropsten 上瀏覽,應該能看到相關提示:
中間 contract 的右上角有 Self Destruct 的樣式
四. 已知 Remix 問題
Remix 目前有兩個版本,只有新版有 Vyper 的編譯器。在此整理目前遇到的問題,如果有人也遇到可以對照一下本處,可以省去很多自我懷疑xD
不會報錯
Remix 的編譯結果有時會是錯的、和本地端編譯出來的結果不同
舉上方的 SellToken 合約為例,將其複製到 Remix 中使用左邊的 Remote Compiler 有錯,但又不報錯 q_q (ERC20 的合約有在同檔案目錄)
左方有紅色三角形,代表編譯失敗,但沒有報錯訊息可以看…
getter function 竟然要花錢
用 Solidity 寫的合約,查詢 public 變數的值應該是不用消耗 gas 的,但不知何故查詢 Vyper 寫的合約的 public 變數卻要消耗 gas,如下圖…
可以看到中下方有 22026 gas 的消耗
Local compiler 無法使用
圖中的 Local Compiler 此選項,個人雖照官方文件執行 vyper-serve 但卻失敗,因此若有讀者成功希望能留個言不吝分享!
五. 結語
Vyper 作為一個比 Solidity 更新的合約語言,在寫程式碼的方面沒什麼問題,但相關的開發工具、學習資源等都遠不及 Solidity。
Vyper 主打的兩個特色:可讀性的部分相信看完上面的讀者應該已經有些感覺;安全性…小白如作者我倒是沒有感受到顯著的不同。況且 Solidity 已經發展許久,很多錯誤的寫法、知名的安全漏洞大家應該也很熟悉了,還有 Openzeppelin 提供安全合約寫法的範本,因此有待以後高人解說安全性是否真的是 Vyper 較好。
有興趣者可以查看 Vyper 的安全報告:點我,大意是目前 Vyper 的編譯器仍有許多問題待改進! (感謝 Chih-Cheng Liang 的提供)
本文對 Vyper 的介紹及其與 Solidity 的差異只講了個大概,欲知更詳細的介紹還是要麻煩讀者前往官方文件了:https://vyper.readthedocs.io/en/latest/index.html
最後,如果本文有任何錯誤,請不吝提出,我會盡快做修正;而如果我的文章有幫助到你,可以看看我的其他文章,歡迎一起交流 :)
田少谷 Shao - Medium
類 Python 的合約語言 Vyper 開發入門:與 Solidity 差異、用 Truffle 部署、ERC20 賣幣合約實做 was originally published in Taipei Ethereum Meetup on Medium, where people are continuing the conversation by highlighting and responding to this story.
👏 歡迎轉載分享鼓掌
python呼叫py 在 竹亭聽雨 - 小咪媽咪三寶媽 Facebook 的最佳解答
2015/07/04 程式書
最近真的用Python寫了一個簡單parser來印出不同檔案的特定資訊數據比較,不過對函式和語法不熟得查筆記+Google才寫得出來,下次用這個當範本改就會快很多了! 特別的是改了py檔後下次呼叫就直接是新的效果,不用編譯好方便XD 不知何時可以把後面寫網站的章節也看完,然後自己寫一個我的部落格...
python呼叫py 在 Anaconda黑科技讓HTML檔可直接執行Python程式碼 - YouTube 的必吃
對於想要跟別人分享、展示 Python 程式碼,以及想用 Python 寫網頁程式的人, ... 函式庫2:27 非 Python 內建函式庫的 呼叫 3:24 PyScript外連. py 檔案3:49 結尾. ... <看更多>
python呼叫py 在 我是剛加入Python 的新手, 正在學Python 並且學習群益api , ... 的必吃
我目前是用群益範例裡的order.py 修改,測試, 連線正常,取得憑證,取得期貨帳號都正常 ... 在windows下python 可以呼叫群益api,讓熟悉C# 的開發者,又想用python 的人多 ... ... <看更多>
python呼叫py 在 Re: [問題] py程式之間的值如何傳遞- 看板Python - 批踢踢實業坊 的必吃
謝謝你的討論,我好像看到適合我的東西 XD
我的例子是我要寫出 sensor(比如讀取溫度計)
還有另外寫出使用 sensor 值運作的程式
socket 我會寫,但處理斷線,timeout 的邏輯累死我了
後來我用 mqtt
除掉處理斷線,timeout 等等問題
這種類似 call back 的邏輯給我另一種困擾, 比如
sensor.py 送出溫度計值
然後 relay.py 就必需接收到這個值處理起來
但其實我不在乎溫度計何時傳值給我
我在乎的是最後一個值是什麼
比如五秒有五次更新值,反正我在第五秒才想處理
那前四次的值其實我不在乎,就讓它消失吧..
用 mqtt 其實我就是在收到訊息時,on_message 自己維護一塊記憶體先塞進去
何時要用此值,才去問那塊記憶體
檔案可以擔此重任,用覆蓋的就好
但不同程式間 multitask, 就不要寫入端開檔/清空,還沒寫入,讀取端就來讀耶
那我又有其他困擾了
》何時要用此值,才去問那塊記憶體
Redis 不就替我做了這事
而且可以跨電腦
檔案的話,不知有沒有一定要關檔(所以保證寫入完畢),其他程式才能開檔的檢查
這問題在 multi-thread 裡即使是記憶體變數,也是很常面對的
但用 python 我一直都不用理,因為 python 的多工有個 GIL 在,它的 ATOM 太大了
若在 C,我就吃了不少虧,常常要設 critical section
而 Redis 應該就是跨電腦很適合吧..
當有多台電腦跑 relay.py,
他們何時需要資料,就何時去問 sensor.py 那台電腦
而不是 sensor.py 經常主動推播,漏收訊息也不好
不想收,它也一直推給所有來註冊的電腦
(喔,mqtt 採訂閱制,不想收就不訂閱,困擾沒那麼大)
很值得我參考。
--
※ 發信站: 批踢踢實業坊(ptt.cc), 來自: 123.204.157.162 (臺灣)
※ 文章網址: https://www.ptt.cc/bbs/Python/M.1674701472.A.C2B.html
補上檔案的問題
from time import sleep
file = 'output.txt'
with open(file, 'w') as f:
f.write('apple\n')
sleep(10)
f.write('banana\n')
這個例子是個極端做法,在寫檔中間插入等待十秒
在做 multi-thread/task 極端 debug 時,有時得這麼做
我們知道寫檔很快,但萬一沒那麼快時,有沒有問題?
開啟兩個 terminal, 一個用命令列執行這個程式
(比如 python3 test.py)
要十秒後,游標才會掉下來
這時趕快去另一個 terminal 試 cat output.txt
十秒內,檔案已經存在,但內容是空的
-------------
以前學微軟的 SDK 時,關於開檔,有更多參數可下
share read/write 限制等等
如果我不開 share, 基本上就是不共享了
(不知有沒有記錯;頂多記反,但確定有這個概念)
不共享的意思是,當有一個程序開了此檔做寫入
那麼別人就無法做讀取
那這個 bug 也就不會發生
不然若我用檔案當做 IPC 中介傳遞,在 a.py 寫入時,恰好 b.py 要讀取
那就會讀到空檔,怎麼辦?
當然這狀況很難發生,因為檔案並不會寫到十秒那麼久
但如果是一萬次發生一次,也會讓人頭疼,bug 重製不易
所以刻意改變執行速度也是個技巧
要說我龜毛嗎?
※ 編輯: HuangJC (49.217.46.86 臺灣), 01/26/2023 16:02:43
抱歉,看不懂,可以給多一點資訊嗎?謝謝
https://bbs.huaweicloud.com/blogs/325805
是像這個例子嗎?
這例子不是我要的
它可以在檔案改變時觸發 event
於是就好像 call back
call back(或說 interrupt)的確有時對我有用
但比如有個溫度計,sensor.py 每一秒去讀它一次
我並不在乎何時讀了並更新到 output.txt 裡
我在乎的是,另一支程式,relay.py 要去取用檔案裡的溫度值時
任何時候,我想讀,就有目前的值;我只要最後一個值而已
因此新值不斷覆蓋舊值,我不在乎
但我在乎檔案寫到一半不能被讀取時,很恰巧的我去讀了...
所以我需要一些像是 critical section 這樣的東西
但我想 redis 有做好這件事
※ 編輯: HuangJC (49.217.46.86 臺灣), 01/26/2023 16:15:03
舉個例子,共享的若是一個溫度值,int or float, 問題不大
但若是一張相片,檔案還真不小,寫入需要時間,那問題就出來了
當然或許可以用寫入 tmp file 然後 rename 的方式,減少空窗期
但還是有點機率去讀到空圖。。
※ 編輯: HuangJC (49.217.46.86 臺灣), 01/26/2023 16:18:12
https://myapollo.com.tw/zh-tw/python-fcntl-flock/
找到了,用 flock 來做
麻煩到讓我想。。運氣不會那麼糟吧,不要做這些事 XDDDD
沒看懂,什麼是 PV?
其實很早前我就覺得 mysql 可以用,只是它放硬碟,速度慢
而且我要自己建一個 table,裡面是 name 對應 value
還要自己編碼給各種資料型態
(value 用 text 就好,不管什麼 binary 都有法子編入 text
其實是很久前的工作,土法練鋼,和主管玩過一次)
但有 redis 就蠻像在做這些事,只是別人做好了,不用自己打造輪子
我的負擔是輕量化的,常是傳送 int, float, 頂多一些 string
image 是故意壓力測試的,其實我還想不到我有這需求
所以 redis 就夠了
目前用 MQTT 也夠了,就自己打造
方法是向 server 訂閱後,server 只要更新,我都抓下來 local 存而不用
等要用時再去存放處拿資料
雖然多了些 code, 但是不會有資料被洗掉剛好拿到空資料這種事
(有這概念,就算用 socket 自己從頭打造,也大概是這種樣子)
EPICS 先別說學不學得會了,它專長在哪,為何要用它,我都比較不出來 XD
粒子加速器?望遠鏡? 用這種方法描述似乎不能讓我看到重點
還是說'望遠鏡需要傳大畫面,所以這是能傳大檔,且符合我前面描述需求'的東西
那這樣我算有聽懂一些了
因為我的負擔不大,所以。。那應該就是 redis 就好
然後 PV 是什麼?
※ 編輯: HuangJC (49.217.130.232 臺灣), 01/26/2023 19:17:48
喔。。我的溫度就是
說到 call back,我有的用得到
比如何時遠端按鍵被按,或者脈衝式流量計
不過脈衝式流量計我會傾向於在 sensor.py 裡,就把它從脈衝變成數值
(有點像 DA 轉換,但多了積分)
傳出來的數值已經是類比型式,所以仍然可以讓遠端的 relay.py 去 polling
而不是需要被 call back
因此我大概都用不到 call back.
※ 編輯: HuangJC (49.217.130.232 臺灣), 01/26/2023 20:37:55
謝謝,我的確想過用 share memory 做
那麼這和 redis 的差別是什麼?要理解成競爭軟體嗎?
畢竟一個東西從來不限制只有一家公司能做
競爭的話,就是看誰便宜,看誰效能好的差別了
(我當然知道指令長得不一樣,但在決策上我還是會覺得兩者很像)
https://stackoverflow.com/questions/19477821/redis-cache-vs-using-memory-directly
https://tinyurl.com/6edh5emu
還真有人問
似乎是說,redis 可以存入硬碟,可以在下次執行時續用(因為它就是資料庫)
資料庫的話,跨語言也不成問題(比如 python 寫入,c 讀取)
當然 redis 也輕鬆跨電腦,畢竟它基於網路
但也因為基於網路,所以比純 memory 略慢
因此如果我的 sensor.py 和 relay.py 在同一台電腦,就用 share memory
要跨電腦,就用 redis...
-----
小惡魔的心聲:老闆才給多少錢,我才不要寫兩套
用 redis 就對了,不管在同一台電腦或跨電腦都能用 XD
※ 編輯: HuangJC (49.216.44.32 臺灣), 01/28/2023 18:58:28
感謝
ZMQ 似乎能讓我不用再從 TCP/IP 手工打造 message loop
D-BUS 看不懂
> D-Bus是一個行程間通訊及遠端程序呼叫機制,可以讓多個不同的電腦程式(即行程)
> 在同一臺電腦上同時進行通訊
既然它說遠端(RPC),那怎麼不說它也能跨電腦?
遠端呼叫經常是慢的,要寫成非同步,最後我的腦袋會變成自己思考 message loop
這樣我會有決策困難 XDDDD
※ 編輯: HuangJC (49.216.44.32 臺灣), 01/31/2023 07:01:22
如果它能跨電腦呢?那我就想去修 wiki 了
前不久在討論 RPI 晶片時,有位網友帶著我讀資料
他一邊翻 wiki 給我,一邊回憶他一路追這些晶片的沿革
然後很順口的說:所以 wiki 上寫錯了
XDDD 熱心的人很多,但錯誤也不少啦,但我們不敢馬上改下去
改東西不好輕舉妄動..
https://tinyurl.com/mtu3pvbc
有人做了這個比較
我想,dbus 能做的事太多了,對我的需求是殺雞用牛刀
所以我才看不懂。
(其實 zmq 我也是一行都沒用過,算不上會;但我就是能看懂一些特徵
會覺得我如果要用它,就能學會,會很好用)
就像在實作 message queue 前我只是像 mqtt 一樣丟資料,收資料
但有時發送端會一口氣丟一堆,接收端收一個,做一件事,才回報一件事做好了
因此才產生 message queue 的需求
懂了需求才會懂為何要有 queue,不然一開始我也不想做出 queue
※ 編輯: HuangJC (49.217.70.24 臺灣), 01/31/2023 18:34:01
... <看更多>