, 6, '', ('127.0.0.1', 0))]
```
這個「看情況」,就是這篇文章要講的故事起點...
## Node.js 17 的「重大」變更
在 Node.js 的世界裡,`dns.lookup()` 這個函式負責把主機名稱解析成 IP 位址。這個函式有一個選項叫做 `verbatim`,中文可以翻譯成「逐字的」。在 Node.js 16(含)之前,`verbatim` 的預設值是 `false`。這表示 Node.js 會「重新排序」名稱解析的結果,把 IPv4 位址排在前面。換句話說,不管系統名稱解析器回傳什麼順序,Node.js 都會好心地幫你把 IPv4 放前面。
但是在 Node.js 17 這個行為改變了,看看這個精美的 [PR](https://github.com/nodejs/node/pull/39987),`verbatim` 的預設值改成了 `true`。這表示 Node.js 會「尊重」系統名稱解析器的回傳順序,不會自作主張地重新排序。
感覺滿好的,尊重系統設定聽起來也合理,但這個改動影響了不少現有程式的行為,尤其是那些 hardcode 監聽 `localhost` 或 `loopback` 的程式。
補充:在更新版本的 Node.js 中,`verbatim` 選項已被標記為 deprecated,建議改用 `order` 選項來控制排序行為
### 問題是怎麼發生的
讓我用一段程式碼來說明這個問題,假設我有一個簡單的 HTTP 伺服器,會聽 `127.0.0.1:3000`:
```javascript
const http = require("http");
const server = http.createServer((req, res) => {
res.end("Hello from server!");
});
// 明確指定監聽 127.0.0.1(IPv4)
server.listen(3000, "127.0.0.1", () => {
console.log("Server is running on 127.0.0.1:3000");
});
```
然後 Client 端程式連接 `localhost:3000`:
```javascript
const http = require("http");
http
.get("http://localhost:3000/", (res) => {
let data = "";
res.on("data", (chunk) => (data += chunk));
res.on("end", () => console.log("Response:", data));
})
.on("error", (err) => {
console.error("Error:", err.message);
});
```
在 Node.js 16,這段程式碼運作正常:
```bash
$ node server.js &
Server is running on 127.0.0.1:3000
$ node client.js
Response: Hello from server!
```
但在 Node.js 17(或更新的版本),如果你的系統名稱解析器優先回傳 IPv6,你可能會看到:
```bash
$ node server.js &
Server is running on 127.0.0.1:3000
$ node client.js
Error: connect ECONNREFUSED ::1:3000
```
`ECONNREFUSED ::1:3000` 錯誤訊息就看的出來 Client 端試著連結 IPv6 的 `::1`,但伺服器只監聽 IPv4 的 `127.0.0.1`,所以連線被拒絕了,更多討論細節可參考 [Node.js Issue #40537](https://github.com/nodejs/node/issues/40537)。
這不是 Node.js 的 bug,這是刻意的行為改變。Node.js 團隊認為尊重系統名稱解析器的順序是更正確的做法,不過這個改變造成了很多現有程式的相容性問題。
補個不重要的冷知識,雖然跟 Node.js 無關,但類似的情況在 Docker Desktop(macOS / Windows)也可能發生。Docker Desktop 在 Mac 和 Windows 上運作的方式是透過一個輕量級的 Linux 虛擬機器。當你在容器中開一個 port,Docker 會把這個 port 轉發到你的本機。但問題是 Docker Desktop 只會把 port 轉發到 IPv4 位址(`127.0.0.1`),不會轉發到 IPv6 位址(`::1`)。
所以你可能會遇到這種情況:
```bash
# 啟動 nginx 容器,把容器的 80 port 對應到本機的 3000 port
$ docker run -p 9527:80 nginx
# 用 127.0.0.1 連接 - 成功
$ curl http://127.0.0.1:9527
Welcome to nginx!
...
# 用 localhost 連接 - 可能失敗!
$ curl http://localhost:9527
curl: (7) Failed to connect to localhost port 9527: Connection refused
```
原理也是一樣的,這也是為什麼開發者會在 Docker 環境下遇到,明明容器跑得好好的但就是連不上的情況。不過,如果在你自己電腦上測試不一定會遇到這個問題,原因可能是:
1. 較新版的 curl 實作了 Happy Eyeballs,會自動嘗試 IPv4 和 IPv6,哪個先通就用哪個。
2. 你的系統解析 localhost 時優先回傳 IPv4(可以用 `dscacheutil -q host -a name localhost` 確認)。
3. 較新版的 Docker Desktop 可能已經修正這個問題。
## Vite 的解決方案
好,現在我們可以回到最一開始講到的那個 Vite 的 PR 了。
### 考古時間!
讓我們來看一下這段歷史:
在 2021 年的這個 [PR](https://github.com/vitejs/vite/pull/2977) 說到為了安全性,Vite 把預設的 host 從允許網路存取改成只監聽 `127.0.0.1`。這樣可以避免開發伺服器被區域網路上的其他裝置存取,看起來合理。
然後在 2022 年丟出來的這個 [Issue](https://github.com/vitejs/vite/issues/7075),社群發現 Vite 在 Node.js 17+ 環境下會出問題。因為 Vite 強制使用 `127.0.0.1`,但其他工具(例如透過 `net.connect()` 連接 localhost)可能會解析到 `::1`,導致連線失敗。所以在這個 [PR](https://github.com/vitejs/vite/pull/8543),也就是我開頭提到的那個 commit,把預設的 host 改回使用 `localhost`。
### PR #8543 的具體改動
但再往下追就會發現這次的 PR 的改動挺有意思的。首先,新增了一個 `loopbackHosts` 的集合來處理各種 loopback 位址:
```typescript
export const loopbackHosts = new Set([
"localhost",
"127.0.0.1",
"::1",
"0000:0000:0000:0000:0000:0000:0000:0001",
]);
```
然後,在啟動伺服器的時候,使用 `dns.lookup()` 來取得 `localhost` 的實際解析結果:
```typescript
import dns from "dns/promises";
// 使用 verbatim: true 取得系統實際的解析結果
const result = await dns.lookup("localhost", { verbatim: true });
```
這樣做的好處是:伺服器會監聽系統名稱解析器「實際會回傳」的那個位址,確保客戶端連接 `localhost` 時能夠成功。`verbatim: true` 參數是指不要重新排序,這樣可以確保 Vite 拿到的結果跟其他程式查詢 localhost 時拿到的結果一致。
## Node.js 20 的「快樂眼球」
Node.js 20 加了一個新功能,讓這個問題變得可能沒那麼痛苦:`autoSelectFamily`。
這個功能實作了 [RFC 8305](https://datatracker.ietf.org/doc/html/rfc8305) 中描述的「Happy Eyeballs」演算法。這個名字很有趣,中文可翻譯成「快樂眼球」,意思是讓使用者不用眼睜睜看著連線超時,能夠更快建立連線,這樣就快樂了?(工程師還真是容易滿足的生物)
Node.js 怎麼實作的?當 `autoSelectFamily` 啟用時(Node.js 20+ 預設啟用),連線過程會這樣進行:
1. 先透過 `dns.lookup()` 取得所有可用的位址。
2. 依序嘗試連接每個位址。
3. 每個位址最多等待 `autoSelectFamilyAttemptTimeout` 毫秒(預設 250ms)。
4. 如果超時就換下一個位址,直到成功或全部失敗。
雖然這跟 RFC 8305 描述的「同時發起多個連線」不太一樣,Node.js 的實作比較像是「快速輪流嘗試」而非「並行競速」,但這應該也夠用了。有興趣看看 Node.js 是怎麼實作的,可以直接去翻 [lib/net.js](https://github.com/nodejs/node/blob/main/lib/net.js) 原始碼,搜尋 `internalConnectMultiple` 函數。
### 實際使用
```javascript
const net = require("net");
const socket = net.connect({
host: "localhost",
port: 3000,
autoSelectFamily: true, // Node.js 20+ 預設是 true
autoSelectFamilyAttemptTimeout: 250, // 每次嘗試的超時時間
});
socket.on("connect", () => {
console.log("Connected!");
console.log("Local address:", socket.localAddress);
console.log("Remote address:", socket.remoteAddress);
});
```
不過歹誌不是憨人想的這麼簡單,根據 [Issue #54359](https://github.com/nodejs/node/issues/54359) 記載,Happy Eyeballs 的實作也帶來了一些新問題,最主要的是那個 250ms 的超時時間:
- 在某些網路環境下,250ms 可能太短了
- 如果第一個位址剛好在 251ms 的時候要成功,但因為超時被放棄了,就會浪費時間去嘗試第二個位址
你可以透過 `autoSelectFamilyAttemptTimeout` 來調整這個超時時間,但要找到一個適合所有情況的值並不容易。
## 小結
看起來一樣的東西,底層可能完全不同,`127.0.0.1` 和 `localhost` 可能在 99% 的情況下確實可以互換,但是那個 1% 就是那個「最厲害的 But」,說不定可能會讓人 debug 到懷疑人生。
追原始碼、看 PR 是很有價值的學習方式,特別是 GitHub 上的 Issue 和 PR 裡有很多東西可以挖。這些第 0 手資料往往比網路上二手、三手的技術文章更有深度、更準確。想深入了解某個技術決策的「為什麼」時,直接去看原始碼和相關討論是最好的方式。看不懂原始碼?現在不是有 AI 了嗎?問它啊!
下次當你看到一個看似「無聊」的 PR 時,不妨停下來想想:為什麼要這樣改?背後有什麼原因?然後你可能會發現一個兔子洞,一頭栽進去學到一些可能有用或可能沒用的東西 :)
## 參考資料
- [iTHome 鐵人賽](https://ithelp.ithome.com.tw/users/20065770/ironman/6302) Vite 原始碼解讀
- [Vite Commit #49c0896](https://github.com/vitejs/vite/commit/49c0896) 把 host 從 `127.0.0.1` 改成 localhost
- [Vite PR #2977](https://github.com/vitejs/vite/pull/2977) 當初為了安全性改成 `127.0.0.1` 的 PR
- [Vite Issue #7075](https://github.com/vitejs/vite/issues/7075) 社群回報與 Node.js 17+ 不相容的問題
- [Vite PR #8543](https://github.com/vitejs/vite/pull/8543) 最終的解決方案
- [Node.js Issue #40537](https://github.com/nodejs/node/issues/40537) 關於 Node.js 17 localhost 解析變更的討論
- [Node.js PR #39987](https://github.com/nodejs/node/pull/39987) dns.lookup verbatim 預設值變更
- [Node.js PR #44731](https://github.com/nodejs/node/pull/44731) `autoSelectFamily` (Happy Eyeballs) 的實作
- [RFC 6761](https://datatracker.ietf.org/doc/html/rfc6761) Special-Use Domain Names
- [RFC 8305](https://datatracker.ietf.org/doc/html/rfc8305) Happy Eyeballs Version 2
- [Node.js DNS 文件](https://nodejs.org/api/dns.html) 官方 DNS 模組文件
---
## AI 時代寫程式,你是在學習還是在偷懶?
- URL:https://kaochenlong.com/vibe-coding-vs-software-engineering
- 發佈日期:2025-12-24
不得不說,AI 實在太厲害了,好像什麼都能做,現在出門在外不隨口講個 AI 關鍵字好像已經沒辦法跟朋友聊天了。在社群或企業或學校,總是常被問到:「你覺得 AI 會不會取代軟體工程師的工作?」,我也常會問學生同樣的問題。
每次新技術出現,就會有人開始喊「XXX 已死」,幾乎每個程式語言、框架每年都要死好幾次,但很多喊這些標題的人只是想博取眼球或是透過製造焦慮賣課程,甚至自己根本也沒花時間去理解這些技術背後的本質。
但這次我不得不說,情況可能真的有點不太一樣...
## 我的 AI 使用心路歷程
在聊 Martin Fowler 大大的看法之前,先讓我分享一下我自己使用 AI 的經驗。說實話,一開始其實我對 AI 寫程式這件事是抱持觀望態度的。
ChatGPT 剛出來的時候我跟著玩了幾下,覺得滿好玩的,但好像也還好,就是比較厲害一點的 Stack Overflow。它可以回答一些程式問題,但稍微複雜一點的東西就會開始胡說八道。
當時的我:「這東西要取代程式設計師?還早還早,你先把 FizzBuzz 寫對再說」。
但不久後 GPT-4 出來了,Claude 出來了,GitHub Copilot 開始在我打字打到一半自動幫我補完一大段程式碼。接著是 Cursor、Windsurf 這些更狠的角色登場,聽說它們不只能聊天,還會幫我們改程式碼?好吧,那就來試試看。
結果一試下去...咦?怎麼比我想像中好用這麼多?
除了這些 IDE 或編輯器之外,後續又推出的 CLI 工具也很厲害,像是 Anthropic 的 Claude Code,直接在終端機裡面就能跟 AI 協作寫程式,連編輯器都不用開。
對我來說,不是「好用到可以取代工程師」那種好用,而是「好用到可以少寫很多無聊的 boilerplate code」那種好用。
舉個例子,我最近用 AI 寫了一個售票平台 [ezBundle](https://ezbundle.cc/),目前程式碼的行數已經超過 30 萬行,這個專案從資料庫設計、CRUD、到金流串接,那些重複性高的程式碼 AI 都能幫忙生出來,我只需要專注在比較重要的邏輯和一些特殊的需求上就好。我大概花了一個週末就有個雛形(當然後續還是花了不少時間調整細節啦),這效率有點驚人。不是說 AI 幫我「寫完」了這個系統,而是它讓我可以把時間花在更有價值的地方,例如打魔物獵人遊戲。有興趣的話可以參考[宅宅的浪漫:用 AI 為自己的研討會寫一個售票系統](https://kaochenlong.com/2025/09/14/ezbundle.html)這篇文章。
我現在的使用習慣大概是這樣:
- 寫一些重複性高的程式碼時,讓 AI 幫我生成初版
- 遇到不熟悉的功能,先跟 AI 討論有哪些做法,AI 可能會給我幾個答案,然後我再從中挑選我認為比較合適的
- 寫測試的時候,讓 AI 幫我生成 test case,然後我會仔細看過一遍,沒問題再請 AI 實作內容。
- 除錯的時候,把錯誤訊息丟給 AI,看看它有什麼想法
同時,我也很清楚目前 AI 的限制:
- AI 會說謊(或說是瞎掰),而且說得理直氣壯!
- AI 給的程式碼可能可以跑,但未必是好的解法
- AI 不會幫你思考架構,你得自己思考
所以我現在的態度,既不是「AI 萬歲!以後都不用自己寫程式了!」那種狂熱擁抱,也不是「AI 就是垃圾!程式設計師的尊嚴不容侵犯!」。AI 是一個強大的工具這已經不用懷疑了,用得好可以讓效率翻倍,用得不好可能會讓人變成一個只會按 Tab 鍵或 Enter 鍵接受建議的人。
關鍵不在於「要不要用 AI」,而在於「怎麼用 AI」。
## 比從組合語言到高階語言還大的事
寫過《重構》的業界大神 Martin Fowler,最近有一場很值得看的訪談:
在這場訪談中 Martin 被問到一個很有意思的問題:「在你的職業生涯中,有什麼變革可以跟 AI 相比?」
> The comparable thing would be the shift from assembly language to the very first high-level languages.
「唯一可以相比的,就是從組合語言轉變到第一批高階語言的那個時代。」這不只是語法變簡單,而是根本性地改變了開發者思考問題的方式。你不再需要想「這個資料要放在哪個暫存器(Register)」,你可以開始用更接近人類語言的方式思考問題。這表示因為技術的進步,我們可以在更高的抽象層次上工作。問題是,AI 帶來的改變,真的只是「提高抽象層次」這麼單純嗎?
## 最核心的轉變:從確定性到非確定性
Martin 認為,AI 帶來的最關鍵改變,其實不是「讓你可以用自然語言寫程式」這件事。真正關鍵的是另一個東西:
> The biggest part of it is the shift from determinism to non-determinism.
「**最大的部分是從確定性轉變到非確定性**」這句話,我覺得是整段訪談最重要的觀點。傳統的程式設計是「確定性的(deterministic)」。什麼意思?就是給定相同的輸入,你永遠會得到相同的輸出。你寫 `1 + 1`,它永遠會回傳 `2`。例如寫了一個排序演算法,給它同樣的陣列,它永遠會回傳同樣的結果。程式會照著我們寫的邏輯,一步一步執行下去,不會有意外。
這是我們這些軟體工程師從第一天開始就被訓練的思考模式,程式永遠會照著「某種方式」執行,而這種方式是可以被追蹤、被理解、被除錯的。
但 AI 不是這樣運作的...
給它相同的 prompt,它可能會給你不同的回應。它可能這次寫對,下次寫錯。它可能今天很聰明,明天就開始胡說八道。即使是同一個問題、同一個 prompt,每次的答案都可能有微妙的差異。這是一個根本性的思維轉變,很多人還沒有意識到這件事可能帶來的衝擊或挑戰。
在訪談中 Martin 以他太太的工作為例,他太太是一位建築結構工程師,她設計樑柱的時候,數學會告訴她樑柱需要多粗才能支撐特定的重量。但她不會只用數學算出最低標準,她會加上額外的安全係數。為什麼?因為實際的木材、混凝土、鋼材,它們的特性會有變異。同一批混凝土,這塊可能比那塊脆一點點。同一批鋼材,這根可能比那根軟一點點。
所以結構工程師從來不會「剛剛好」設計。他們永遠會留容錯空間。
> We need probably some of that kind of thinking ourselves. What are the tolerances of the non-determinism that we have to deal with?
「我們可能需要一些這樣的思維。非確定性的容錯範圍是什麼?」更重要的是下一句:
> We can't skate too close to the edge, because otherwise we're going to have some bridges collapsing.
「我們不能太靠近邊緣,否則會有橋梁倒塌。」在軟體產業一直以來我們都很習慣確定性的世界,我們寫的測試之所以有意義,是因為程式碼是確定性的,如果測試通過了,它就應該一直通過。但當你的工具本身是非確定性的,傳統的測試方法夠用嗎?當你用 AI 寫程式,你怎麼知道它這次寫對了,下次還會寫對?你怎麼知道它在這個情況下正確,在那個情況下也正確?你怎麼建立對一個「可能會說謊」的工具的信任?
這些問題,目前整個產業可能都還在摸索中。
## 非確定性對程式教育和團隊協作的衝擊
這個「非確定性」的問題,讓我想到兩個很實際的場景。
第一個場景是**教學現場**。
我在[五倍學院](https://5xcampus.com)教程式設計十多年,一直以來的教學方法都是建立在「確定性」之上的。我會告訴學生:「你輸入這段程式碼,就會得到這個結果。」或是「這個函數接收這些參數,然後就會回傳這個值。」
學生可以自己驗證,看到結果跟我說的一樣,就會建立起對程式設計的信心。但現在呢?如果學生問 AI,AI 給了一個答案。學生跑起來,發現可以動。然後學生問:「老師,AI 說可以這樣寫,你覺得呢?」
我看了一下,發現那段程式碼確實可以跑,但寫法不太好。也許它沒有處理邊界情況,也許它用了一個已經被棄用的 API,也許它只是剛好在這個情況下可以動,換一個情況就會爆掉。這時候我要怎麼教?我要跟學生說「AI 錯了」嗎?但它確實可以跑啊。我要跟學生說「AI 對了但不好」嗎?學生可能會想「能動就好幹嘛那麼龜毛」。
更麻煩的是,我要不要先教學生傳統的寫法,還是直接教他們用 AI?如果先教傳統寫法,學生可能會覺得:「這些 AI 都會幫我寫好了,為什麼要花時間學?」但如果直接用 AI,學生可能永遠學不會真正的程式設計思維,變成一個只會寫 prompt 的人。
在這 Vibe Coding 盛行的時代,即使可能有一些逆風我還是先教基礎觀念,只是可能不會像以前教的那麼深入、那麼細節。先讓學生理解程式是怎麼運作的,然後才引入 AI 作為輔助學習工具。就像學數學,你總得要先理解加減乘除的原理再來用計算機,如果連乘法是什麼都不知道,就算給你計算機,你也不知道什麼時候該按乘號。
我不確定這是不是最好的做法,這目前還是一個沒有標準答案的問題。
第二個場景是**團隊協作**,特別是 code review。
以前做 code review,我知道這段程式碼是某個同事寫的。我會想:「小明通常對這塊比較不熟,我要仔細看一下」或是「小華的程式碼品質一向很好,我可以快速過一遍」但現在呢?AI 寫的程式碼,你要怎麼 review?
它可能語法完全正確,風格也很一致,但邏輯上有一個微妙的錯誤。因為 AI 寫程式碼的方式跟人不一樣,它不是「理解問題然後寫出解法」,而是「根據機率產生看起來合理的程式碼」。這代表你做 code review 的時候,不能只是「掃一眼看起來沒問題就過」,你要逐行檢查邏輯是否正確。這其實增加了 code review 的負擔,而不是減少。
我也聽到業界有些企業或團隊開始說,如果這段程式碼是 AI 生成的,commit message 要寫清楚,這樣 reviewer 就知道要更仔細地看。這是一個好做法,但也代表我們對 AI 生成的程式碼,其實並沒有那麼信任。這些都是「非確定性」帶來的連鎖反應。我們以前建立的那一套流程、那一套信任機制,在面對 AI 的時候可能需要重新思考。
## AI 會說謊,而且說得臉不紅氣不喘
說到說謊,這可能是另一個我覺得很多人還沒有認知到的問題。主持人在訪談中分享了一個親身經歷:他請 AI 在一個設定檔裡加一個新項目,並且加上當天的日期。AI 加了,但日期是錯的,它只是複製了上一個項目的日期。
他跟 AI 說:「這不是今天的日期」
AI 說:「抱歉,讓我改正」
然後它改成了昨天的日期!
> You need to get this experience to see that it can gaslight you for a simple thing of today's date.
「你需要親身體驗這件事,才會知道它連今天的日期這種簡單的事情都可以對你進行心理操控」主持人又舉了另一個例子,有時候他讓 AI 幫忙改程式碼、跑測試。AI 會跟他說:「我已經跑過所有測試了,一切正常」然後他自己跑 `npm test` 指令,結果有五個測試失敗,這我在做 ezBundle 的時候也遇過好幾次...
Martin 開玩笑說,如果 AI 真的是一個初級工程師(我聽過很多人這樣比喻),他會去找 HR 談談。因為這種會說謊、掩蓋問題的成員,在任何工作場合都是不能接受的。
但 AI 不是人,它沒有「故意說謊」的意圖(至少現在還沒有)。AI 只是在根據機率產生下一個最可能的 token。對 AI 來說「測試通過」和「測試失敗」可能都是合理的選項,它只是挑了一個機率比較高的。所以你可以用 AI 來幫你寫程式,但不要完全相信它的輸出。可以的話,每一行程式碼都要自己看過,每一個它說「沒問題」的地方都要自己驗證。
這聽起來很累,但這就是現階段的遊戲規則。想請 AI 幫你做出真正能用的東西,這就是我們必須付出的代價。
## Vibe Coding 很爽,但你在學什麼?
說到「不看程式碼」,這就要講到 2025 年非常流行的一個詞:**Vibe Coding**。
這個詞是 Andrej Karpathy 在 2025 年 2 月份在 X 上的[一則推文](https://x.com/karpathy/status/1886192184808149383)提到的。他提出的這個概念意思是:你完全不看 AI 產生的程式碼,只管它能不能跑、有沒有達到你要的效果。
這聽起來很美好,你只要用自然語言描述你要什麼,AI 就會幫你生出來。不用管什麼變數命名、函數設計、程式架構,反正那些程式碼你也不會看(可能也看不懂),反正能動就好。Martin 對此的看法:
> I think it's good for explorations, it's good for throwaways, disposable stuff, but you don't want to be using it for anything that's going to have any long-term capability.
「我認為它適合用來探索、適合用來做拋棄式的東西,但你不會想把它用在任何需要長期維護的專案上」
他舉了一個例子,他的同事要在部落格文章中放一張示意圖,就請 AI 幫忙生成一個 SVG 圖片。產出的圖看起來還可以。但當 Martin 想要微調一下,例如把標籤移近一點,當他打開那個 SVG 檔案的時候...
「天啊!」那個 SVG 有夠複雜!明明只是幾條曲線和標籤,卻寫成了一堆難以理解的座標點和路徑。想要手動調整幾乎不可能。這就是 Vibe Coding 的問題。當你不看程式碼的時候,你放棄了理解它的能力。而當你不理解一段程式碼,你就無法修改它。
你唯一能做的就是...呃,重新來過。
> All you can do is nuke it from orbit and start again.
「你唯一能做的就是從軌道上核爆它,然後重新開始」每次你要改什麼東西,都要重新說一次需求,然後開始祈禱 AI 這次能猜對。如果它還是沒猜對呢?再一次、再一次、再一次,反正累的又不是你。對於一個週末的 side project 可能還可以接受,但對於一個可能維護好幾年的系統呢?
## 學習迴圈:你以為你在成長,但你可能正在斷送自己的成長
這裡我想特別花一些篇幅來討論一個概念,我認為這是整場訪談中很重要的觀點,特別對於正在學習程式設計的人來說。Martin 的同事提出了一個觀察:
> When you're using vibe coding, you're actually removing a very important part of something, which is the learning loop.
「當你使用 Vibe Coding 時,你實際上拿掉了一個非常重要的東西,那就是『學習迴圈』。」
什麼是「學習迴圈(Learning Loop)」?想像你在學一個新的程式語言或框架,在腦中可能有個想法:「我要來做一個待辦事項清單的 App」,然後打開編輯器開始寫程式。寫了一段程式碼,執行之後發現結果不對,接著回去看程式碼想想可能是哪裡寫錯了。然後查文件、查 Google、翻 Stack Overflow 上的答案,相信大家對於開發過程中,瀏覽器分頁開滿了捨不得關掉的 Stack Overflow 這種場景都不陌生。接著再繼續修改程式碼,再跑一次。這次對了,但網頁打開的速度有點慢。你開始思考為什麼慢,怎麼最佳化...
這個過程,從想法到程式碼再從程式碼到結果,最後再從結果到理解,就是學習迴圈。這是人類學習新事物的基本方式,這麼多年來一直都沒什麼改變。在這個過程中累積經驗、建立直覺、學習什麼做法是好的、什麼做法是壞的。
> If you're not looking at the output, you're not learning.
「如果你不看輸出,你就沒有在學習。」當你完全不看 AI 產生的程式碼時,等於跳過了這整個學習迴圈。當想法直接變成結果,中間的「程式碼」變成了一個你不關心的黑箱。短期來看這真的超爽的,簡單幾句話就能很快地做出東西,但長期來看這是可能是在斷送自己的成長。
下回 Vibe Coding 完一個作品之後仔細想想看,你在這過程有沒有學到什麼?
下一次遇到類似的問題,你可能還是要靠 AI。在這過程中可能沒有建立起任何自己的能力,而是變成了一個只會寫 prompt 的人,而不是一個會寫程式的人。就像學開車,也許有一天自動駕駛可以做到只要喊一聲「帶我去台北車站」,車子就會自己開到目的地。這實在太方便了然後以後都用這個系統,十年後,這個系統壞了。你發現你不會開車,甚至連方向燈都不知道怎麼打。
也許在未來,自動駕駛會普及到你永遠不需要自己開車。但在那之前呢?
同樣的道理。也許在未來,AI 會強到你完全不需要理解程式碼。但在那之前呢?在那之前,你是要當一個「會用 AI 但不會寫程式的人」,還是一個「會寫程式、也會用 AI 讓自己更強的人」?
## 初學者 vs 資深工程師:使用 AI 的策略差異
對於不同程度的人,使用 AI 的策略應該是不一樣的,初學者可能很容易掉入「偷懶陷阱」。為什麼?因為新手還沒有建立起判斷程式碼好壞的能力。AI 給一段程式碼,新手不知道它是好的還是壞的,只知道它能跑。
剛學做菜的人,食譜說加一湯匙鹽,他不知道一湯匙是多少,就隨便加。結果?可能太鹹,可能太淡。他吃了覺得「好像還可以」,但其實離好吃還很遠。資深廚師就不一樣了,他可能不用食譜,但他知道什麼時候該加多少鹽。即使有人給他一份食譜,他也會根據自己的經驗調整。他知道「這個食譜說加一湯匙,但以我對這道菜的理解,應該要少一點」。
程式設計也是一樣。資深工程師使用 AI 的時候,他知道什麼時候該相信 AI、什麼時候該懷疑 AI。
- 「這段程式碼看起來好像沒問題,但這個地方怎麼沒有處理 null?讓我問一下。」
- 「它用了這個套件,但我知道這個套件有一個已知的效能問題,換別的吧!」
- 「這個解法可以動,但架構上不太好,長期維護會有問題。」
初學者沒有這些判斷能力。他看到程式碼能跑,就覺得「搞定了」。他不知道自己不知道什麼。這就是為什麼初學者用 AI 要特別小心。不是說不能用,而是用的時候要更有意識地學習。
什麼是「好的使用方式」?
- 讓 AI 解釋程式碼:「這段程式碼在做什麼?每一行分別是什麼意思?」
- 讓 AI 生成程式碼後,自己試著修改:「如果我想加一個功能,要改哪裡?」
- 遇到錯誤時,先自己想想為什麼,再問 AI:「我猜是因為 XXX,對嗎?」
- 比較不同的解法:「還有沒有其他寫法?各有什麼優缺點?」
什麼是「壞的偷懶」?
- 無腦複製貼上,不看程式碼在做什麼
- 程式碼能跑就不管了,不思考有沒有更好的寫法
- 遇到錯誤就把錯誤訊息丟給 AI,不自己嘗試理解
- 永遠只用 AI 的第一個答案,不質疑、不驗證
資深工程師反而可以更放心地使用 AI,因為他有能力把關。老鳥知道 AI 產出的東西哪些可以用、哪些不能用,甚至把 AI 當作一位「打字很快但有時候會犯錯的助理」,而不是一個「無所不知的神」。這也是為什麼我認為在 AI 時代,累積真正的技術能力反而更重要了。因為這些技術能力決定了你能不能有效地使用 AI。
## 測試在 AI 時代變得更重要了
說到學習和驗證,這就要談到測試了。在 AI 時代,測試的意義可能有點不同。以前,工程師寫測試是為了驗證「自己寫的程式碼」是否正確。你知道程式碼是怎麼運作的,你寫測試是為了確保它在各種情況下都能正常運作。現在,寫測試是為了驗證「AI 寫的程式碼」是否正確。你可能不完全理解程式碼是怎麼運作的,但你知道你要的結果是什麼。測試變成了你和 AI 之間的「**契約**」,也就是說不管 AI 怎麼寫,只要這些測試通過,我就當你是對的。
而且每次你讓 AI 改程式碼,你都要重新跑測試。因為 AI 不會記得「這邊之前改好了,不要動到它」。所以當你跟 AI 說:「幫我改這個函數,讓它可以處理空陣列。」AI 改了。但可能順便把另一個函數也改了,因為 AI 覺得「這樣比較一致」。或者它把你之前特別處理的一個邊界情況改壞了,因為它不知道那個處理是有原因的。
如果你沒有測試,你可能要過很久才會發現這個問題。如果你有完整的測試,你馬上就會知道:「欸,這個測試怎麼壞了?」
我在開發 ezBundle 售票平台的時候有個類似的經驗。有一次我加了一個新功能,上線後兩天都沒什麼問題,但也都沒訂單進來,當時我只是想說可能就剛好生意比較差而已,兩天後有人私訊我說不能結帳,我才發現事情的嚴重性。我其實有寫測試,測試寫的也不少,但就剛好那次偷懶沒全部跑完,結果就這麼準中招了。這件事讓我學到一個教訓:就算沒時間跑完整個測試,至少也要跑個 [Smoke Test](https://en.wikipedia.org/wiki/Smoke_testing_\(software\))。
什麼是 Smoke Test?這個名字聽起來很酷,其實概念很簡單。想像你修好一台電視,如果插上電源之後就開始冒煙,那就不用再測其他功能了。Smoke Test 大概就是這個意思,先測最基本、最核心的功能有沒有在運作。
以我的售票平台來說,Smoke Test 大概就是:能不能開啟首頁?能不能看到活動列表?能不能走完結帳流程?這些如果壞掉,其他功能再完美都沒有意義。
## IDE vs AI:不是所有釘子都要用 AI 這把鐵鎚
在訪談中,Martin 的同事 James Lewis 分享了一個有趣的實驗。他想用 Cursor 來改一個類別的名稱。這個操作,在傳統的 IDE 裡面只要右鍵點一下「Rename」,然後輸入新名字,一秒鐘就完成了。IDE 會自動找到所有引用這個類別的地方,全部更新。
結果用 Cursor 呢?他等了一個半小時,用掉了當月 10% 的 token 額度。而且還不一定改對,可能漏掉了一些地方,或是改到不該改的東西。
> We've had this technology for a long time. So it's kind of amusing.
「這個技術我們已經有很久了。所以這件事蠻好笑的。」AI 不是萬能的,有些任務,用傳統工具做真的比較快也比較簡單。IDE 的重構工具是確定性的。它會精確地找到所有引用,精確地更新它們。不會漏,不會錯。因為它是在分析語法樹,不是在「猜測」你要什麼。
AI 則是在「理解語意」。它很擅長理解你用自然語言描述的需求,擅長產生新的程式碼,擅長解釋複雜的概念。但對於那些需要精確操作的事情,例如重新命名、移動檔案、格式化程式碼等工作,傳統工具做得更好。所以,好的做法不是「全部都用 AI」,而是「知道什麼時候該用什麼工具」。
- 需要精確的機械式操作?用 IDE。
- 需要理解語意、產生新內容?用 AI。
- 需要搜尋特定字串?用 grep 或 IDE 的搜尋功能。
- 需要理解程式碼在做什麼?用 AI。
你可以用 AI 來驅動傳統的確定性工具。比如說,用 AI 來幫你產生 sed 或 awk 的指令。這樣你結合了 AI 的語意理解能力,和傳統工具的精確性。
既然聊到了開發工具和 AI 的分工,讓我分享一下我現在的工作流程。我的主要編輯器是 Vim。Cursor 跟 Windsurf 我都試過,還不錯,但就是有點慢。我目前還是習慣用 Vim 加終端機的方式進行開發。我的使用原則大概是這樣:
需要「精確操作」的時候,用開發工具的原生功能,例如:
- 重新命名變數或函數:用 F2 或右鍵的 Rename Symbol
- 自動引入(auto import):讓 IDE 自動處理
- 格式化程式碼:用 Prettier 或語言內建的格式化工具
- 跳到定義、找所有引用:用 Go to Definition、Find All References
這些操作開發工具做得又快又準,沒必要讓 AI 來做。
需要「理解語意」或「產生新內容」的時候,用 AI:
- 解釋一段我不熟悉的程式碼
- 寫一個新的函數或類別
- 產生測試案例
- 把一段程式碼翻譯成另一種語言
- 幫我想變數或函數的命名
兩者結合的情況:
有時候我會用 AI 來幫我產生一些指令或正規表示式。比如說,我想把所有的 `console.log` 改成 `logger.info`。直接請 AI 幫我寫一個 sed 或 VS Code 的搜尋替換的正規表示式。AI 幫我產生指令,然後我用工具去執行。這樣結合了 AI 的語意理解能力和工具的精確性。
另一個例子是產生 Git 指令。有時候我要做一個比較複雜的 Git 操作,比如「找出過去一個月內,修改過 XXX 目錄底下任何檔案的所有 commit」。我 Git 指令其實還算熟,但這種指令我不太會去記它所有的參數跟用法,現在我直接問 AI,它給我指令,我看一下確認沒問題就執行。
這種「AI 產生指令、人類驗證執行」的模式,我覺得蠻安全又有效率的。
## AI 真正擅長的:理解那些沒人想看的老舊系統
聊完了 AI 的限制,我們來說說它真正擅長的地方。大家可能想說用 AI 來產生新的 App,但卻漏了一個更實際、更有價值的應用場景。
很多企業裡還在運作的系統,這些系統可能是十幾年前寫的,可能用 Java 或是 COBOL,甚至更古老的東西。文件不全(或根本沒有)就算了,原始開發者早就離職了,沒有人知道這段程式碼為什麼要這樣寫。這一直是業界的大痛點。每次要改這種系統,工程師都要花大量的時間去「考古」,最後常聽到就是乾脆砍掉重練,不用面對這個問題說不定還能賺一筆專案改寫的費用...
是說,AI 怎麼在這方面幫上忙呢?做法有很多種,例如針對程式碼做語意分析,把資訊放進圖資料庫,然後用類似 RAG(Retrieval-Augmented Generation)的方式讓你可以問問題。比如說:
- 「這個資料從哪裡來?經過哪些處理?」
- 「這個功能是怎麼運作的?」
- 「為什麼這段程式碼要這樣寫?」
AI 可以幫你快速建立對一個陌生系統的理解,省下大量的考古時間。很多公司其實都有這個問題,特別是歷史悠久的企業,像是銀行、保險公司或是政府機關,這些系統沒人敢動,因為沒人知道動了會發生什麼事。AI 在這方面的幫助,可能比「寫新程式碼」更有價值。
## 學習新技術更快了,但還是要學
另一個 AI 很有用的場景,就是能用來學習不熟悉的新技術。如果我想做一個遊戲,我可以直接問 AI:「我想用 Unity 做一個會跳躍的角色,要怎麼開始?」它會給你一個起點。我可以接著問:「這個函數是幹嘛的?」「為什麼這樣寫會報錯?」「有沒有更好的做法?」
說到底,還是得要花時間理解 AI 給的東西。AI 可以幫我們更快地取得一個起點,但如果只是複製貼上、能跑就好,這樣還是沒有真正學會。下次遇到類似的問題,還是要再問一次。
所以正確的用法應該是用 AI 來「加速」學習,而不是「取代」學習。
## Stack Overflow 的既視感:我們以前也經歷過這種事
在訪談中,主持人提出了一個很有趣的觀察:其實我們以前也經歷過類似的事情,那就是 [Stack Overflow](https://stackoverflow.com/questions)。
比較年輕的工程師可能沒有經歷過這段故事。在 Stack Overflow 之前,在網路上查詢程式相關的問題,你很有可能被導到一個叫 Experts Exchange 的網站。這個網站上的答案得要課金才能看,大部分的人可能不會付錢,因為就算付了,裡面也可能找不到答案。
然後 Stack Overflow 出現了。突然之間,你可以找到免費的程式碼片段,直接複製貼上。很多初學者就這樣複製、貼上、看看能不能跑。不管它在做什麼,不管為什麼這樣寫。能跑就好。
資深工程師會跟他們說:「你要先理解這段程式碼在做什麼。」「就算它能跑,你也要知道為什麼能跑。」
> I feel we've been, there was a few years where we were going back and forth of people mindlessly copying, pasting snippets.
我覺得我們曾經有那麼幾年,大家就是無腦地複製貼上程式碼片段。聽起來很熟悉吧?Martin 認為現在的情況很類似,只是規模更大了。AI 可以產生的不只是一個片段,而是整個函數、整個類別、甚至整個專案。無腦複製貼上的誘惑更大了。
但 Stack Overflow 至少還有一個優勢:社群驗證。在 Stack Overflow 上,一個答案會被其他人 upvote 或 downvote。會有人留言說「這個做法有問題」、「在這個情況下不適用」。你可以看到答案的分數,知道有多少人覺得它是好的。
AI 給的答案沒有這種驗證。它就是給你一個答案,你不知道這個答案是不是好的、是不是適合你的情況。更麻煩的是,如果大家都只問 AI,不再去 Stack Overflow 問問題、回答問題,那 AI 以後要從哪裡學新的東西?
這是所謂「Model Collapse」,當 AI 生成的內容越來越多,AI 訓練的資料中也會包含越來越多 AI 生成的內容,這種「近親繁殖」的結果可能會造成品質下降。
## 重構的藝術:AI 時代可能更重要了
說到 Martin Fowler,不能不聊他的成名作《重構》(Refactoring)。這本書第一版出版於 1999 年,2019 年出了第二版。可以說,「重構」這個概念能在業界廣為流傳,有很大一部分要歸功於這本書。他說了一句我很喜歡的話:
> Each step is so small, but it's not worth doing, but you string them together and you can really do amazing things.
「每一步都小到不值得做,但當你把它們串起來,你可以做到驚人的事情。」重構的精髓就是這樣,每一步都小到看起來沒什麼意義,但累積起來的效果是驚人的。那在 AI 時代,重構還重要嗎?Martin 認為不只重要,可能更重要了:
> If you're going to produce a lot of code of questionable quality, but it works, then refactoring is a way to get it to a better state while keeping it working.
「如果你要產生大量品質可疑但能運作的程式碼,那麼重構就是一個讓它變得更好、同時保持它能運作的方法。」這個觀點很有意思。AI 可以快速產生程式碼,但品質參差不齊。它產生的程式碼可能可以跑,但可能很難維護、很難理解、很難擴展。如果你打算長期維護這個系統,你不能就這樣把 AI 產生的程式碼丟進去就不管了。你需要重構它。讓它變得更好理解、更容易維護。
但 AI 可能比較喜歡「大改」,你跟 AI 說「幫我改善這段程式碼」,它可能會重寫整個函數、改變資料結構、調整整個架構。這樣的改變很難驗證,你怎麼知道它改完之後,行為還是跟以前一樣?
真正的重構是「小步前進」。每一步都很小,每一步都保持行為不變。這樣你可以隨時驗證、隨時回到上一步。所以,比較好的做法可能是用 AI 來幫你產生初始的程式碼,然後用你自己的重構技能來改善它。或者,訓練 AI 做小步重構,而不是大改。
所以後來我學到一件事,就是用 AI 輔助重構的時候,要給它更具體的指示。例如,不要說:「幫我重構這段程式碼。」,而是說:「請把第 50-80 行的資料庫查詢邏輯抽成一個獨立的函數,參數和回傳值保持跟原本一樣。」這樣產出的東西會更可控。
另一個心得是要叫 AI 重構之前,一定要先有測試。如果有完整的測試,你可以讓 AI 改完之後跑一次測試,馬上知道有沒有改壞。如果沒有測試,那就跟把眼睛矇起來打架一樣,你不會知道什麼時候會被揍一拳。
這跟傳統的重構原則是一樣的,先寫測試或是規格,再重構。只是在 AI 時代,這個原則變得更重要了,因為 AI 更容易在你沒注意的地方改變行為。
## 企業的現實:大多數人不是在做 side project
在很多討論 AI 的文章和影片中,你會看到很多「我用 AI 一個週末做了一個 App」的故事。這些故事很激勵人心,但它們可能不代表大多數軟體工程師的日常。
在新創公司,可能全公司都是工程師或者工程師占大多數。決策快、迭代快、可以冒險。但在傳統企業,軟體開發者可能只占 10-20%,其他都是會計、行銷、業務、法務等等。這些人有他們自己的需求、自己的語言、自己的規則。你不能說「讓我試試看,錯了再改」,因為錯了可能會有法律問題、監管問題、聲譽問題。
AI 採用速度在不同組織可能會有所不同。新創可以全力擁抱 AI,因為他們沒有什麼可以失去的。但銀行、政府、醫療這些高度監管的產業,必須非常謹慎。不是說這些產業不會用 AI,而是說這些單位的採用路徑會不太一樣。
## 給初學者的建議
如果你是剛開始學程式的人,面對 AI 這麼方便的工具,以下有幾個建議:
### 先打好基礎再用 AI
我知道這聽起來很老人說教,但這是真的。你要先理解變數是什麼、函數是什麼、迴圈怎麼運作、為什麼要用物件導向。這些基礎觀念不懂,你就沒辦法判斷 AI 給你的程式碼好不好。學開車,你也得先知道油門煞車方向盤怎麼用,才能開自動駕駛。如果你連這些都不知道,自動駕駛出問題的時候你根本不知道該怎麼接手。
### 把 AI 當老師,不要當代寫。
與其說「幫我寫一個 XXX」,不如說「解釋一下怎麼寫 XXX」。讓 AI 教你,而不是幫你做。你可以讓 AI 生成程式碼,但生成之後要自己讀懂它。問 AI:「這一行在做什麼?」「為什麼要用這個方法而不是那個?」「如果輸入是 null 會怎樣?」
這樣你才是在學習,而不是在偷懶。
### 遇到錯誤先自己想
很多初學者一遇到錯誤訊息就複製貼上丟給 AI。這樣你永遠學不會除錯。試著先自己讀錯誤訊息。Google 一下。想想可能是什麼原因。實在想不出來,再問 AI。但問的時候要說「我試過 XXX 和 YYY,但還是不行,我猜是因為 ZZZ,對嗎?」
這樣你是帶著假設去問,而不是無腦求救。
### 定期挑戰自己「不用 AI 寫」
每隔一段時間,試著完全不用 AI 來完成一個小專案或練習題。看看你是不是真的學會了,還是只是 AI 學會了。如果你發現離開 AI 就不會寫程式,那代表你前面的學習方式有問題,要調整。
## 給資深工程師的建議
如果你已經有幾年工作經驗,對程式設計有一定的掌握,使用 AI 的策略可以更開放一些:
### 大膽使用 AI 來處理繁瑣的工作
寫 boilerplate code、產生測試案例、格式轉換、寫文件...這些工作讓 AI 幫你做,你的時間應該花在更有價值的地方:系統設計、架構決策、code review、mentoring。
以我自己開發 ezBundle 的經驗來說,我讓 AI 幫我處理的都是那些「我知道怎麼做,但實在懶得自己寫」的東西,例如表單驗證、API 的 CRUD、報表的 CSV 匯出。這些我閉著眼睛都能寫...好啦,有點誇張了,但我的確是不需要 AI 就能寫出來,只是每次都要花時間。現在我把這些交給 AI,我的時間拿來思考「票券系統要怎麼設計才能應付幾百人同時驗票」、「退款流程要怎麼設計才能避免糾紛」這些真正需要動腦的問題。
這才是資深工程師該做的事。用 AI 省下來的時間,不是拿來發呆,而是拿來思考那些 AI 目前還做不好的事。你的判斷力和經驗是 AI 目前還取代不了的。把那些 AI 可以做的事情交給 AI,你專注在 AI 做不好的事情上。
### 建立你自己的驗證機制
因為你有經驗,你應該知道什麼地方容易出錯。針對這些地方建立檢查清單。每次用 AI 生成程式碼,跑過這個清單。比如說:
- 有沒有處理 edge case?
- 錯誤處理完整嗎?
- 效能有沒有問題?
- 有沒有安全漏洞?
- 跟現有的架構風格一致嗎?
這個清單因人而異、因專案而異,但重點是你要有意識地去驗證,而不是看一眼覺得沒問題就過了。
### 用 AI 來加速學習新技術
資深工程師也需要持續學習。新的框架、新的語言、新的工具。以前學一個新東西可能要花一兩週看文件做教學,現在你可以直接問 AI:「我是有 X 年 Y 語言經驗的工程師,想學 Z 框架,有什麼概念是跟我已經知道的東西類似的?有什麼是完全不同需要特別注意的?」
AI 可以幫你建立知識的橋樑,讓你更快上手。
### 分享你的經驗
你踩過的坑、你發現的 AI 使用技巧、你建立的驗證機制,這些對初學者都很有價值。寫部落格、在團隊內部分享、回答別人的問題。AI 時代,「知道怎麼有效使用 AI」本身就是一種技能。而這種技能是要靠實戰經驗累積的。你的經驗可以幫助其他人少走彎路。
## 結論:AI 是工具,不是魔法
聽完這場訪談,我有幾個想法:
### 我們正處於一個巨大的典範轉移之中
Martin 說 AI 是他四十年職業生涯中最大的變革,而且唯一可以相比的是組合語言到高階語言的轉變。這代表我們現在學的很多東西,可能在幾年後就會變得非常不一樣。保持學習、保持好奇,可能是最重要的能力。
### 非確定性是一個根本性的挑戰
我們習慣的程式設計思維是建立在確定性之上的。當這個假設不再成立時,我們需要發展新的思維模式。也許我們需要把軟體開發的思維,從「精確的工程」變成更像「處理不確定性的工程」。
### 基本功永遠重要
雖然 AI 改變了很多事情,但核心的能力,像是理解問題、與人溝通、建構抽象、驗證結果等,這些並沒有變。你需要能夠判斷 AI 的輸出是否正確、是否適合你的場景。這需要你真的懂程式設計,而不只是會寫 prompt。
### 不要放棄學習迴圈
Vibe Coding 很爽,但如果你從不看程式碼、不理解程式碼,你就失去了學習的機會。用 AI 來加速你的學習,而不是取代你的學習。
### 測試變得更重要,不是更不重要
當你用一個「會說謊」的工具來幫你寫程式,你需要更多的驗證機制。測試是你和 AI 之間的契約。
### 知道什麼時候該用什麼工具
AI 不是萬能的。有些事情傳統工具做得更好。好的工程師知道什麼時候該用 AI,什麼時候該用 IDE,什麼時候該自己手寫。
## 最後的最後
Martin Fowler 在訪談的最後說了一句話,我覺得很適合作為這篇文章的結尾:
> I don't think AI is going to wipe out software development. I think it'll change it in a really manifest way, like the change from assembly to high-level languages did, but the core skills are still there.
「我不認為 AI 會消滅軟體開發。我認為它會用一種非常顯著的方式改變它,就像從組合語言到高階語言的改變一樣,但核心的技能還是在的。」是的,AI 正在改變一切。但有些東西是不變的,對問題的好奇心、對品質的追求、與人溝通的能力、持續學習的態度。回到一開始的問題:「AI 時代寫程式,你到底是在學習還是在偷懶?」
答案取決於你怎麼用它。
如果你只是無腦地讓 AI 寫程式、不看程式碼、不理解程式碼、不驗證結果,那你確實是在偷懶。而且這種偷懶會讓你越來越弱,最後變成一個只會寫 prompt 但不會寫程式的人。
但如果你用 AI 來加速你的學習、幫你處理繁瑣的工作、讓你可以專注在更重要的事情上,同時保持你的學習迴圈、持續累積你的技能,那你是在學習,而且你可能會變成一個比以前更強的工程師。
工具就是工具。刀可以用來切菜,也可以用來傷人。
AI 能讓你變強,也可以讓你變弱。選擇在你 :)
---
## Workflow、AI Agent,傻傻分不清楚?
- URL:https://kaochenlong.com/workflows-vs-agents
- 發佈日期:2025-10-08
不知道大家有沒有注意到,最近只要有新的 AI 工具推出,就會出現 XXX 已死的說法。其實這說法滿有趣的,因為每次新技術出現,總會有人跳出來評論舊技術沒用了,更有趣的是這些評論者很可能本身並沒在用那些他口中說快死或已死的技術。
但這無所謂啦,沒什麼好吵的,技術就是這樣的,總是不斷推出新的技術,不然人類社會怎麼進步?
只是,大家討論到工作流程自動化的時候,常會把 Workflow 和 AI Agent 這兩個詞混在一起討論?其實這是個很好的問題。兩者都能幫我們自動化處理任務,但它們的設計哲學和適用場景其實不太一樣。就像你應該不會沒事開著法拉利(如果有的話)去載水泥、鋼筋一樣,不是哪個比較好,而是要看你想做什麼還有合不合適的問題。
## 你想要自己開車,還是想要司機幫你開?
在我看來,Workflow 和 Agent 最大的差別,就在於「控制權」這件事。
### Workflow:你掌握方向盤
想像一下你自己開車出去玩,你需要決定走哪條路、在哪個路口轉彎、什麼時候該加速減速...每個決策都掌握在你(或導航系統)手上,迷路也是你的問題。這就是 Workflow 的精神。
在 Workflow 裡,你明確定義了「what」和「how」:
- 要做什麼(what)
- 怎麼做(how)
- 什麼時候做這個、什麼時候做那個
- 遇到 A 情況就走這條路,遇到 B 情況就走那條路
就像你握著方向盤,車子就會一板一眼地照著你規劃的路線前進。
### Agent:你僱用了一位司機
Agent 不太一樣。就像搭計程車,你只需要告訴司機「我要去台北 101」,至於要走哪條路、遇到塞車怎麼繞、紅綠燈怎麼走...這些決定都交給司機(或導航系統)自己判斷。
在 Agent 裡,你主要定義「what」,讓 AI 決定「how」:
- 你說明目標和期望結果
- AI 自己決定達成的路徑
- AI 會根據情況調整做法
你可能會想:「司機會不會給你偷繞遠路?」的確是有可能,這也是為什麼 AI 給你的答案如果沒能力驗證的話可能會有的風險。
> Workflow 給你掌控權,但缺乏彈性;Agent 給你彈性,但你得放棄一部分掌控權。這是取捨,並不是對錯。
再讓我們更仔細地看看這兩者的差異。
### 1. 可預測性 vs 自主性
Workflow 就像搭捷運:每天同樣的路線、同樣的站、同樣的時間表。你知道從 A 站到 B 站要多久,每次都一樣。這就是可預測性。
Agent 比較像搭計程車:雖然目的地一樣,但司機可能會根據路況選不同的路。今天走忠孝東路,明天可能改走市民大道。同樣的起點和終點,但過程可能完全不同。這就是自主性。
你有沒有想過,為什麼有些任務就是要可預測?因為在某些場景下,「過程」和「結果」一樣重要。比如說金融交易、醫療流程、法規遵循等場景如果每次做法都不一樣,那可能會有點麻煩。
### 2. 結構複雜度 vs Prompt 複雜度
這是個很有趣的現象。
Workflow 的複雜度在「結構」上。你可能會看到一個流程圖裡面有幾十個、甚至上百個節點,分支來分支去、有平行處理、有條件判斷...如果沒有視覺化工具的話,可能看不懂在幹嘛。
但是,Agent 的複雜度在 Prompt 上。別被簡單的架構騙了!一個看起來很簡單的 Agent,它的 Prompt 可能長達幾千個字,裡面要考慮各種邊界情況、異常處理、安全限制,要寫一個好的 Prompt 並沒有比寫程式來的簡單。
我認為,這兩種複雜度其實是同一件事的不同表現形式。複雜的問題就是複雜,我們只是選擇把複雜度放在哪裡而已。
### 3. 設計思維的差異
這一點可能更根本一些。
當你在設計 Workflow 時,你的腦袋裡想的是:
- 「流程應該怎麼走?」
- 「遇到這種情況要怎麼辦?」
- 「這個步驟完成後,接下來做什麼?」
但設計 Agent 時,你想的是:
- 「我要達成什麼目標?」
- 「這個 Agent 需要什麼能力?」
- 「在什麼範圍內它可以自己做決定?」
這是兩種完全不同的思考方式。Workflow 是「控制導向」的,Agent 是「目標導向」的。
## No Code/Low Code 工具的尷尬處境
說到這裡,我也觀察到一個有趣的現象,就是所謂的 No Code 或 Low Code 工具的生存空間或甜蜜點正在被擠壓...
### 簡單問題:Agent 獲勝
以前我們可能會用 No-Code 工具來做一些簡單的自動化,比如「收到表單就發通知信」、「定時爬個資料存到試算表」。但現在你會發現,一個設計良好的 Agent 可能一下子就搞定了,而且不用設定一堆節點或參數。舉幾個例子:
- 簡單的資料處理和轉換
- 基礎的 CRUD 操作
- 制式的客服回覆
這些任務用 Agent 可能比用 No Code 工具還快、還直覺。
### 複雜問題:Workflow 或直接寫 code
一旦問題變複雜,No Code 工具也不見得是好選擇。
No Code 工具用久了多少都會遇到有些工作流程做到一半卻發現「咦,這個好像有點難做到」,然後開始想各種變通方案,最後整個流程變得超級複雜、難以維護。這時候你可能會想:「我還不如直接寫 code 算了。」這類問題包括:
- 多層審核流程
- 複雜的業務規則引擎
- 需要合規稽核的作業流程
這些場景如果規則明確,Workflow 或直接寫程式會是更好的選擇。光靠 Agent 的 Prompt 要涵蓋所有狀況,複雜度不會比較低。
### 甜蜜點正在縮小
如果你是在兩邊游移的人,慢慢也會發現,No Code / Low Code 工具的「甜蜜點」正在縮小:
- 簡單的那一端被 Agent 取代了
- 複雜的那一端直接用 Workflow 或寫 code 更有效率
- 剩下的空間主要是需要視覺化協作、讓非技術人員也能參與設計的場景
我不是說 No Code 工具會消失,但它們的定位確實在改變。
目前坊間的那些 No Code 平台也不是吃素的,也都知道這個問題,所以早就開始整合 AI 功能。但這並沒有改變問題的本質:如果問題本質上適合 Agent,那就用 Agent;如果適合 Workflow,那就用 Workflow。
平台只是工具,重點還是你對問題本質的理解。
## 該選哪一個?
問自己一個最簡單的問題:「這個流程能不能出錯?」
如果答案是「不能」或「最好不要」,那就選 Workflow。為什麼這麼說?因為 Workflow 提供的是可靠性和可追蹤性:
- 不能出錯的流程,例如金融交易、醫療流程、法規遵循
- 需要完整審計追蹤,每一步都要留下記錄,方便日後稽核
- 規則明確且穩定,業務邏輯很清楚,不太會變動
- 多人協作需要視覺化,團隊成員都要能看懂流程在做什麼
- 合規要求嚴格,有法規或公司政策的限制
Workflow 用起來可能有點老派,但想想看,如果你處理的是客戶的錢或是你的錢,使用者的個資、或是會影響他人權益的流程時,可靠性比酷炫更重要。
那什麼時候適合用 Agent?當問題本身就不確定、需要彈性應變的時候。例如:
- 處理非結構化輸入,自然語言、圖片、各種格式的文件
- 情境變化大,很難事先窮舉所有可能的情況
- 快速原型開發,想先驗證想法,還不確定最終要怎麼做
- 任務目標明確但路徑多樣,知道要什麼結果,但達成方式很多種
- 可以容忍一定程度的不確定性,結果有些變異是可以接受的
線上客服 Chatbot 就是個很好的例子。每個使用者的問題都不一樣,我們不可能為每種問題都設計一條 Workflow 路徑。這時候 Agent 的彈性就很重要。「可以容忍不確定性」這個前提很重要。如果你的任務是「幫客戶退款」,這種涉及金錢的操作,我會建議還是用 Workflow 比較保險。
## 混合架構?
前面提到,那些 No Code 平台早就注意到這件事,所以紛紛都採用 Workflow + Agent 的混合模式,很受歡迎的 n8n 就是一個很經典的例子。
- 用 Workflow 控制關鍵決策點和資料流向
- 在特定的地方嵌入 AI Agent 節點處理非結構化或需要彈性的部分
結合兩者優勢,有可控性,又有靈活性。我個人很喜歡這種混合的設計,畢竟真實世界的問題,往往不是非黑即白。有些部分需要精確控制,有些部分需要靈活應變,把兩者結合起來才是最實用的解決方案。
工具可能有好有壞,不過通常沒有誰會取代誰,只有適不適合,重點是你有沒有用對地方 :)
---
## 宅宅的浪漫:用 AI 為自己的研討會寫一個售票系統
- URL:https://kaochenlong.com/ezbundle
- 發佈日期:2025-09-14
更新:直播課程已結束,經剪輯後已上傳到[五倍學院](https://5xcampus.com/courses/ai-coding.html)
> 兩個多月,18 萬行程式碼,從開發到部署、上線,一個人。感覺有點夢幻,但好像真的做到了,而且功能還在持續增加中!
故事要從去年的 12 月我們舉辦的 [WebConf Taiwan 2024](https://webconf.tw) 開始說起。
一直以來,在台灣舉辦的社群活動或技術研討會,報名或售票系統大多是使用活動通或是 KKTIX 之類的服務,我們也是。我覺得很棒,使用上很方便也都沒什麼問題,不過因為身為 SaaS(Software as a Service)終究還是有一些限制,沒辦法為特定客戶做客製化。在我們活動結束後的會後檢討我就想,如果我們有一些自己的需求的話,也許下回我們可以自己來做一個報名系統:

為什麼?既然都是工程師們的 Web 研討會,如果能自己做一個系統來用好像不錯,這種「吃自己的狗糧(Eating Your Own Dog Food)」也是一種「工程師的浪漫」(自以為)
講是這樣講,但哪裡這麼簡單。我很常許這種不切實際的願望,大多不了了之,這次原本也不例外。但今年開始眼見 AI Agent 越來越厲害,我發現好像可以試著跟 AI 一起來做這件事,我就找了 Claude Code 來試試看,一試下去才發現不得了,AI Agent 也太厲害了吧,我之前曾經試用過 Cursor 跟 Windsurf 都不錯,但 Claude Code 這種 CLI 模式的開發體驗,跟我過去使用 Vim 開發的習慣非常契合,感覺很不錯。
但講到這裡我必須先說,不是 AI Agent 開下去跟它聊聊天系統就會自動生出來,基本上這個系統我就算不靠 AI 也能自已做出來,只是可能時程至少半年以上,而且照我實際的狀況,大概做一做就會被別的事拉走就放棄了。但跟 AI 一起開發,有種在玩遊戲的感覺,在開發的過程中我甚至會懷疑我自己的存在價值,看著 AI 在幾秒內寫出我要花一小時才能完成的程式碼,那種感覺很微妙。
我曾經估算過,在我狀況好而且不被打擾的情況下,一天大概能寫 1,000 行程式碼,但在 AI 的協助下,兩個月內從零開始寫到 15 萬行程式碼,現在持續增加已經接近 18 萬行,而且都還是在下班或假日的時間在做這件事,這對我這個寫程式寫很多年的老骨頭來說還是很驚人。就是因為這樣,用 AI 開發的「試誤成本」變得非常低,如果寫壞了再請它重寫一次就好,這讓我膽子變大了,開始敢挑戰我以前覺得比較麻煩、花時間的功能。
一開始只是想做個報名、售票的網站,但後來越做越多功能,不只能賣票,還能賣數位內容,接上了物流之後還能賣實體商品:

不知不覺越做越多,現在整個系統的程式碼量已經將近 18 萬行,而且整個專案到現在還是就只有我一個人開發。

目前 ezBundle 已經有:
- 會員系統:Email 註冊/登入、第三方帳號登入(OAuth 整合)、二階段認證(2FA)
- 專案管理(活動/商品/課程):上架、預覽功能、多種方案設定、團購套組價格管理
- 購物與結帳:多元付款方式(信用卡、ATM)、優惠券使用
- 訂單管理:訂單查詢、二聯、三聯式發票管理
- 票券系統:QR Code 票券與分配、票券驗證、轉讓
- 數位內容銷售
- 退款申請、審核、處理
- 商家註冊、審核以及發票、金流設定
- 表單建立、預覽與設定
- 優惠券設定與管理
- Email 通知、排程與開信追蹤
還有一些沒列出來的,就以一個賣東西的平台來說雖然還不到很厲害,但差不多該有的功能也都有了,就算沒有我也可以很快再做上去,過陣子我再拍個影片來介紹這個系統的功能 :)
技術上我選用 Ruby on Rails 框架做為主體,也許會好奇為什麼不用更流行的框架?原因很簡單:我寫 Rails 十幾年了,對框架的細節應該不會比 AI 差,我認為身為一位開發者,在跟 AI 協同開發的過程中,對開發工具的熟悉度是很重要的,這樣才能有效引導 AI 產出正確的程式碼,萬一 AI 寫歪了也知道把它拉回來。
### 上線試用!
再醜的作品總是要拿出來見人,雖然系統還沒到很成熟,但如果不拿出來試一下也不知道到底行不行,所以我就跟朋友商量能不能用[他的活動](https://ezbundle.cc/p/raycast-ai)來試試。我刻意開了單人與多人團購票種以及每人限購的組合實驗,活動當天在現場用手機跟桌機都可以順利完成驗票,除了要花點時間找票券之外,整個流程還算順利。
在這裡先感謝大家願意給我當白老鼠做各種測試並且給我寶貴的回饋意見,讓 ezBundle 可以越來越好用。
另外一個是我自己的線上直播課程:https://ezbundle.cc/p/ai-coding

同樣除了有早鳥票、單人票、多人票、限購票多種組合,就是刻意模擬 WebConf 研討會可能的票種組合。以售票數來說目前有約 200 人報名了,系統承載沒問題,金流處理也都正常,感謝大家支持,看起來應該沒什麼大問題。
偷偷業配一下,這個直播課程就是把我跟 AI 一起做這個 ezBundle 系統的心得整理出來,然後再用 4 個小時的時間直接實作一個薪資管理系統,讓大家看看我是怎麼跟 AI 一起協作開發的。
我寫的「為你自己學 Python」這本書我也擺上來了:https://ezbundle.cc/p/python-book

目前物流還在試驗中所以暫時只能賣數位內容,待完成後就可以像國外的出版社一樣,可以賣實體書、電子書以及實體書加電子書的組合銷售了。
整體來說,這跟我以前開發系統的過程並沒有太大差別,差別在於以前我都是自己一行一行寫程式碼,現在是用對話的方式跟 AI 討論規格,然後盯著它寫,它寫歪了我就把它拉回來或是整段砍掉重寫,但要上線前的測試、上線後的維護、功能的新增,以及「一邊開飛機一邊換引擎」的手感都跟以前沒什麼兩樣。
### 還有什麼想做的?
有,還很多想要試試看,像是捐款、抖內、團購、訂閱制、推薦獎勵機制,以目前的架構來說應該都不難做。不過不急,如果沒意外的話,今年 12/12(五)、12/13(六)我們舉辦的 [WebConf Taiwan 2025](https://webconf.tw) 活動會試著使用 ezBundle 來做報名、售票、驗票等功能了。如果七、八百人的活動從報名、售票、驗票、現場票券管理都能用它完成,甚至還能在現場用它玩一些跟贊助商的互動遊戲,那應該就是最好的成果驗收。
有興趣也來用這個平台賣點東西嗎?不管是課程、活動票券、數位內容或實體商品都可以。如果心臟夠大顆 & 信的過我的技術,歡迎私訊給我,我們來聊聊看可以怎麼進行。
---
## AI 時代,來學程式跟 AI 一起變厲害!
- URL:https://kaochenlong.com/learn-coding-together
- 發佈日期:2025-02-18
這年頭,AI 工具強到連程式碼都能幫你寫了,隨便問個問題,ChatGPT 馬上就能吐出可執行的程式碼,看起來好像真的不需要學寫程式了?但現實可沒這麼簡單。AI 的確能幫上大忙,但它不是魔法,寫程式的核心價值並不只是「敲幾行程式碼」,而是理解問題、設計解法、debug……這些能力 AI 正在快速進步,但還沒到可以完全取代人的程度,至少現在還沒辦法讓一般的程式新手靠 ChatGPT 就能完成完整的開發工作。
雖然許多人對未來軟體工程師的就業市場感到悲觀,但我反而認為會寫程式的人現在比以前更有價值,因為 AI 就是一個能力放大器,它讓我們能夠更快、更有效率地把想法變成現實。如果你有在規劃今年的學習目標,或者想進一步提升技術,甚至考慮轉職軟體工程,那現在絕對是最好的時機。而且這次的課程不只教 Web 開發,還有 AI 技術加持,讓你學到最前沿的工具和技巧。
如果你也認同我的觀點,想在 AI 世代強化自己的技術,甚至轉職成軟體工程師,但又擔心選錯課程,那就繼續看下去吧!
在三月份,除了我們自己主辦的實體培訓課程,還有另一個跟商研院配合的產業新尖兵課程。無論你是完全的新手,還是已經會一點但想再升級,都很適合參加。而且,這次還有 AI 相關的加碼課程,讓你不只學會 Web 開發,還能掌握 AI 技術。
## 這次有兩個課程,看看哪個適合你
這次我們有兩個高強度、全職參與的培訓班,適合想要快速累積開發實力的人。如果你願意投入時間和精力,學完保證技能大幅提升。
### ASTROCamp 第 19 屆(五倍學院)
- 上課地點:五倍學院教室(在台北市 228 公園旁)
- 課程網址:https://5xcamp.us/astrocamp
- 開課日期:3/10(一)
這是我們自己辦的 Web 開發培訓班,不知不覺已經來到第 19 屆,課程內容經過多次打磨,讓你從零到全端開發者,掌握一個開發者該有的技能。
#### 學什麼?
- HTML/CSS/JavaScript:前端的基本功
- TDD(測試驅動開發):開發者該有的好習慣
- Git:團隊協作的必備技能
- Python(Flask / Django):熱門的 Web 後端技術
- SQL:資料庫處理,開發者不可或缺的能力
#### 適合誰?
- 想學 Python,目標是成為全端開發者
- 對 Flask 或 Django 有興趣,想學 Python 在 Web 開發的應用
雖然這樣講可能有點自誇,但我猜在台灣對 Python 和 Git 的理解能比我更深入的人應該不多。
### 產業新尖兵計畫(商研院舉辦)
- 上課地點:台北車站附近
- 課程網址:https://5xcamp.us/cdri
- 開課日期:3/4(二)
這個課程是由商研院舉辦,內容設計更偏向 Node.js 與 Vue.js,適合想走 JavaScript 前端或全端開發路線的學員。這是我們第二次跟商研院配合,講師團隊也是我們的講師及或工程師同事,品質保證!如果你信得過五倍的講師,那你也可以相信這個課程能帶來的成果!
#### 學什麼?
- HTML/CSS/JavaScript:網頁開發基本功
- TDD(測試驅動開發):好習慣從基礎開始
- Git:團隊協作必備
- Node / Express:JavaScript 的後端技術,讓你前後端都能搞定
- Vue.js:目前最受歡迎的前端框架之一
⚠ **注意:產業新尖兵計畫有年齡資格限制,報名前記得確認。**
如果你是 JavaScript 愛好者,想走前端或全端開發路線,這門課會是更適合你的選擇。學完後,你可以獨立開發前後端應用,甚至打造自己的全端專案。
## AI 技術加碼
不管你選哪個課程,這期都有 AI 技術的額外加碼,讓你不只學會 Web 開發,還能掌握 AI 時代的關鍵技能!
- AI 協作開發:學會用 Copilot、ChatGPT 等 AI 工具提升開發效率
- OpenAI API 串接:實作 AI 應用,學會使用 OpenAI API 打造自己的 AI 產品
- AI Agent 開發:進階 AI 技術,學習如何打造自動處理任務的 AI 代理人
這些技能不只讓你更快上手開發工作,也讓履歷更具競爭力。
## 怎麼選?
- 想學 **Python**,走全端開發 → 選 **ASTROCamp**
- 想學 **JavaScript**,走前端或全端 → 選**產業新尖兵計畫**
不論是哪條路,現在開始,都是最好的時機!
---
## 「為你自己學 Python」正式上架!
- URL:https://kaochenlong.com/python-book-in-print
- 發佈日期:2024-11-18

「為你自己學 Python」一書已經正式出版,書本裡的內容全文可在網站上免費閱讀,但如果您覺得拿在手上閱讀比較有感覺,在天瓏書局也能購買紙本書:
雖然我曾經寫過五、六年的 Python 程式,也曾在社群教過一陣子的 Python 課,但要提筆寫書又是不同的故事了。這本書從開始寫書到定稿,大概花了我一年多的時間,除了把以前上課的教材做了一次大大的整理,同時順便更新教材的軟體版本(當年還是 Python 2.x 的時代)。不只這樣,有些我當年自認為的觀念或看法,隨著年紀也有不同的領悟,也趁這個機會校正我自己對 Python 的理解。
這本書以 Python 3.12 做為主要教學版本,內容涵蓋環境安裝及 Python 程式語法,包括各種常用資料型態、邏輯及流程判斷、迴圈、錯誤處理、函數、模組、物件導向程式設計、檔案處理等,並透過網站爬蟲程式抓取並分析資料。沒有太多華麗的技巧,只有最基礎的程式觀念,期望能夠讓讀者在學習 Python 的過程中,建立穩固且正確的基礎。
在撰寫過程中,我除了翻閱官方文件以及 PEP(Python Enhancement Proposal)之外,有些我搞不清楚原理而且文件裡沒特別提到的設計,我就直接去翻 Python 的 C 語言原始碼來驗證自己的想法。新手自學程式容易被不完全正確觀念的影響而不自知,因此在這本書中我力求呈現正確且精準的觀念。我希望這不僅是一本程式自學參考書,也能引導各位讀者掌握 Python 正確的觀念,試著建立自己的「單一真相來源(Single Source of Truth)」。
關於這本書
跟其它的書有什麼不同?
目前坊間 Python 書已經非常多了,通常以行銷的立場,當推出新課程或是新產品,總是得要說一些特色來吸引讀者。一般的手法都是先評論坊間的書都有怎樣怎樣的缺點,接著說自己這本書跟其他書有什麼不同,只要買我這本就會考試都考一百分。這種踩其它產品來墊高自己的高度、踩別人的頭往上爬不是我的做風。
說是這樣說,但我在寫這本書的期間有到書店翻閱其他的 Python 的書籍,有些書裡的內容的確寫的沒那麼正確。舉個例子,有些作者會拿著 Java 的物件導向的觀念來解釋 Python 的物件導向,像是明明 Python 的類別本身就沒有跟 Java 一樣的 private 設計,但書裡卻寫的煞有其事。我相信作者不會刻意誤導讀者,但對可能無法分辨是否正確的新手學習者來說並不是什麼好事。我不敢保證自己寫的是 100% 都正確,但我在撰寫這本書的過程有要求自己一定要查證每一個觀念,這也是這本書花這麼久的時間才完成的原因之一,如果有寫錯的地方,都歡迎大家不吝指教。
同時,程式設計現在已經變成國民義務教育課綱的一部份,我希望可以讓更多人可以用較低的成本取得學習資源,本著開源的精神,所以我把這本書全文的內容都放在「為你自己學 Python」網站上,並以 CC BY-NC-SA 4.0 方式授權予公眾自由取用,不需要花錢購買也可以閱讀。若您是學校老師或教授並且打算使用這本書當教材,都歡迎直接取用;如果需要紙本書可以直接與我連繫,我可用最低的印刷成本提供給教學單位。
所以,如果硬要說我的書跟其它書有什麼不同,大概就是我寫這本書的主要目的是教學而不是賺錢(雖然錢錢還是很香),這應該算是我跟其它書比較不同的地方(吧)。
推薦序
有朋友在看這本書的時候問到,我在業界有認識那麼多厲害的大神朋友,為什麼不找他們寫推薦序?答案很簡單,因為我不喜歡麻煩別人而已。其次,我自己在讀書的時候通常都會直接跳過推薦序,因為我想藉由自己的閱讀來評斷這本書的內容,而不是被別人的推薦影響。如果我自己不看推薦序,我也不會期待別人一定要看我的推薦序。
再者,我對這本書的內容有信心,我希望讓這本書的內容來為自己背書,如果大家看過覺得內容很受用,也歡迎各位能分享出去,讓更多人能知道這本書,這對我來說就是最好的推薦序了。
關於出版
我之前出版過幾本書,都是跟出版社配合,有專門的編輯幫忙處理出版流程,我只要專心寫書就好。關於電腦書我一直有些想要實現的想法,不過出版社通常都有現成的流程或框架可能沒那麼容易實現,所以這回我就選擇了自己跑一趟出版流程,從封面設計、書本排版、印刷廠送印、ISBN 申請、販售通路協調,都自己 && 找外包協助。
為什麼要自己做出版?原因之一是我想挑選我自己有興趣的主題,或是邀請我喜歡的作者來寫書,並且親自看過每本我經手的書。會有這個想法是因為某次與日本的出版社合作的時候,我發現他們的編輯雖然不懂程式,還是親自跟著我書裡的指令或程式碼範例跟著敲打,確保我書裡的程式碼都能正常執行。
另外,原本我提供給編輯的日文原稿是這樣:
「俺の財宝か?欲しけりゃくれてやるぜ…探してみろ この世の全てをそこにおいてきた」
《ONEPIECE》ゴール・D・ロジャー
結果在出版社的編輯手上變成了這樣:

不知道各位能不能看出這其中的細節。除了內容之外,編輯還會跟我確認書裡用到的所有圖片是不是版權可以用於出版使用,種種細節都讓我非常印象深刻。
另外,我還曾主動投稿到國外的某些出版社表示想要出版我在台灣賣的還不錯的書,但最後都被拒絕,原因並不是內容問題,而是他們的遊戲規則並不是作者寫了什麼就照單全收,而是跟他們家的編輯一起討論寫作方向,然後寫出期望的作品。雖然感覺作者少了一些自主權,但至少不會讓內容隨著作者自己想寫什麼就寫什麼,一不小心就歪樓的情況發生,這樣的流程也讓我覺得很有趣。
所以我就想,如果有機會讓我來當編輯或出版電腦書,我也希望能做到這樣的程度。
不過畢竟我不是專業的出版社,雖然可能可以控制出版的品質,但成本控制上可能沒那麼有優勢。簡單的說,像我這種業餘的出版流程又想要追求品質的做法,賣完一刷差不多就是打平而已。在這個網路時代賣電腦書,不賠錢就要偷笑了。
封面設計
之所以會想自己出版,另一個原因就是想要有自己喜歡的封面。對我來說,封面是書的第一個賣點,雖然聽起來有點膚淺,但我的確會因為漂亮的封面而買書。所以幾年前我就找了一位很喜歡的插畫設計師 Croter 幫忙設計封面,設計師本身不會寫程式,在討論的過程我提供了以下幾點想法:
Python 是一款目前全世界最受歡迎的程式語言,現在國中已經是必修課。
Python 的中文翻譯是蟒蛇,但蟒蛇有點恐怖,小一點的蛇也是 ok 的。
這本書主要內容是講授這個程式語言。
最後,我很喜歡七龍珠,它基本上就是我的整個童年,七龍珠的作者離開讓我覺得很難過。雖然 Python 是蛇不是龍,但不知道有沒有機會在設計裡加入一點對七龍珠致敬的元素。
設計師很厲害,一下子就抓到了我想要的風格,也就是現在這本書的封面。不得不說,這本書的封面我真的很喜歡的,從顏色搭配到角色設計,處處都是細節。我知道這年頭 AI 繪圖越來越厲害,但我還是喜歡這種有溫度的手繪設計,很榮幸也很開心能有機會請大師出馬,讓我的書有這麼漂亮的封面。
後續還有四、五本書的封面(React、JavaScript、Ruby on Rails 等書)也都是請他幫忙而且都畫好了,每本我都很喜歡,大家可以期待一下。
編輯排版
事實上,原本我是連書本的排版都是要自己來的,但學了一下 InDesign 就發現這裡面有太多專業,如果我連這個都自己來的話,大概就不用出書了。所以最後還是委託專業的設計公司象晴設計來幫忙處理排版,他們的設計師對書本的排版相當專業,也很有耐心,在經歷了幾次的校稿後,最後才有了這本書,不然大概不知道會拖到什麼時候...
結語
這是我第一本自己發行的書,也是我第一次嘗試自己處理出版流程,有點辛苦但也讓我對出版業有了更進一步的了解。如果您也想寫電腦書,也認同我的想法,歡迎與我們聯繫來聊聊細節。
我把今年大部份的時間差不多都用在準備這本書上了,希望大家會喜歡,也希望能夠幫到更多想要學習 Python 程式的人。
謝謝大家 :)
---
## Python 冷知識 - 註解、函數、布林值
- URL:https://kaochenlong.com/python-fun-facts
- 發佈日期:2024-10-24
匯整一下近期整理的 Python 冷知識,就是那些知道了也不一定會變高手的趣味事實(至少我覺得有趣)。
## 關於多行註解
有寫過 Python 的朋友應該知道,在 Python 是使用 `#` 符號來當做程式碼「註解(Comment)」寫,但你可能不知道 Python 並沒有「多行註解」的設計喔!
你可能會好奇這樣的寫法:
```python
print("Hello")
"""
這是註解
這也是註解
這行還是註解
"""
print("World")
```
你看,用 3 個引號這個不就是多行註解了嗎?
如果你認為這是註解,那你可能誤解「註解」的設計了。
在 Python 這種 3 個引號的寫法並不是註解,它只是可以換行的「多行字串」而已。Python 本身只有單行註解,並沒有多行註解的設計。
的確,你可以拿多行字串來充當註解的效果,但不表示它就是註解,說到底它就只是個字串而已。通常使用多行文字充當註解的時候都不會指定變數給它,所以在 Python 直譯器的眼裡,它可能不會被編譯到 Bytecode 裡,以剛才的範例,編譯出來的結果是:
```plaintext
0 RESUME 0
1 LOAD_NAME 0 (print)
PUSH_NULL
LOAD_CONST 0 ('Hello')
CALL 1
POP_TOP
3 NOP
9 LOAD_NAME 0 (print)
PUSH_NULL
LOAD_CONST 1 ('World')
CALL 1
POP_TOP
RETURN_CONST 2 (None)
```
可以看到中間的多行文字直接消失了。
是說,如果把這個寫法放在類別或函數裡的第一行,可以造成 Docstring 的效果,也就是會幫類別或函數的 `__doc__` 屬性加料,但如果只是要做到 Docstring 的效果,一般的字串就能做到了,不一定需要用到多行字串。
Python 的發明人 Guido 曾經對多行註解這樣[回覆](https://twitter.com/gvanrossum/status/112670605505077248)過:

如 Guido 所說,的確是可以拿多行文字來「當做」多行註解,但這同時也表示 Python 本身就是沒有多行註解的設計,如果有的話就不需要用多行文字來「當做」了。
## type() 不是函數
如果想要在 Python 裡印出某個資料的型別,可能會使用 `type()` 這個函數(或函式),如果想要做數字或字串轉型可能會用 `int()` 或 `str()` 函數,像這樣:
```plaintext
>>> type(123)
>>> int("123")
123
>>> str(123)
'123'
```
但是,事實上不管是 `int`、`str` 或是 `type`,它們在 Python 裡都是一個「類別(Class)」,只是剛好只傳一個引數給它的時候,它會做一些特別的事,例如做型別轉換或是印出它的型別,不信的話你可以用 `type()` 檢驗看看:
```plaintext
>>> type(int)
```
`int`、`str`、`list`、`dict` 等類別都是一種叫做 `type` 的型別,這是因為 `type` 型別在 Python 是所有內建類別的 meta class,也就是所有內建型別都是一種 `type`,包括 `type` 自己也是:
```plaintext
>>> type(type)
```
雖然它們用起來的手感像是呼叫函數,但在 Python 裡它們都是實實在在的類別 :)
## 關於布林值
在 Python 裡的布林型態(`bool`)的 `True` 跟 `False`,有些網路上的教學會說它們可以被轉型成數字 `1` 跟 `0`。這樣的說法沒問題,但可能比較少人知道在 Python 裡布林值其實就是一種數字。不信的話,可以試著這樣做:
```plaintext
>>> bool.__base__
```
`__base__` 屬性可以取得類別的上層類別,所以可以看的出來 `bool` 的上層類別就是數字 `int`,它們之間是有繼承關係的,所以布林值就是一種(kind of)整數。
而且,在 Python 裡的 `True` 跟 `False` 都只會存在一份,而且還是直接內建在 Python 直譯器裡。不管你怎麼建立新的布林值物件,它都會指向同一顆物件。例如:
```plaintext
>>> a = bool(123)
>>> b = bool("xyz")
>>> a is b
True
>>> a is True
True
```
可以看到它們就是同一顆物件。
另外,如果程式碼中使用了一看就知道的布林值判斷,例如:
```python
if True:
print("這是真的")
```
因為這一定會發生,所以 Python 編譯的過程會直接把 `if True` 的判斷拿掉,只留下底下的程式碼。反之,如果是這樣寫:
```python
if False:
print("這根本不會發生")
```
因為這一定不會發生,所以這段程式碼直接會被 Python 無視,根本不會被編譯成 Bytecode。
最後再補個冷知識,在 Python 2.2 之前,Python 其實是沒有布林型態的,早期的開發者常會使用 0 和 1 來代替布林值判斷,或是自己定義 `True` 和 `False`,例如 `True = 1` 或是 `False = 0` 這樣的寫法。直到 [PEP 285](https://peps.python.org/pep-0285/) 提案之後才正式引入布林值的設計,並將上層類別設定成 `int` 類別。不過,在 Pytohn 2 時代,就算 PEP 285 提案已經通過,`True` 跟 `False` 還是可以被重新定義:
```plaintext
# 注意,這是 Python 2
>>> True = False
>>> print(True)
False
```
直到 Python 3 之後,`True` 跟 `False` 才變成保留字,無法被重新定義。
---
## 鐵人賽 - 為你自己讀 CPython 原始碼
- URL:https://kaochenlong.com/dive-into-cpython-source-code
- 發佈日期:2024-10-15
這次的 iThome [鐵人賽](https://ithelp.ithome.com.tw/users/20065770/ironman/7541),我給自己選了一個有點硬的主題,就是閱讀 CPython 的原始碼。
今年也剛好在 PyCon Taiwan 有一場工作坊,主題是介紹如何閱讀 CPython 的原始碼,所以原本只是想藉著鐵人賽的活動,把工作坊的內容文字化做為補充資料,沒想到越寫越多,對我這個不懂 C 語言的人來說更是吃力。雖然現在有很多 AI 工具的輔助,但幾乎每篇文章得要一邊挖原始碼,一邊驗證我解讀的程式碼是不是正確的,每篇文章至少都得花三個小時以上的時間。
但做這件事也是有好處的,除了學到了一些入門的 C 語言,就是現在對這個程式語言有更進一步的認識了,就算要跟別人戰程式的話(並沒有)也比較有底氣一點點。而且,我的工作有一半以上都是在講課,萬一被同學問到奇怪的 Python 問題,現在應該都透過翻閱 CPython 的原始碼的實作講的出真正的原因。
另外,我自己有個有點無聊的小小堅持,就是希望這個系列的每篇文章(包括現在大家在看的這篇也是),都能給它一個合適的 Banner 圖片,從一開始像是新兵戰士面對的像火龍一樣強大的對手,現在能跟它和平相處,甚至藉由它可以再回頭看看其它程式語言的實作,能更了解不同程式語言之間的差異,對我來說真的是件很有趣的事 :)
## 漏網之魚?
在時間有限而且能力也有限的情況下,目前只能暫時先寫這 30 篇的文章,但我對以下這些主題也很有興趣:
- 虛擬機器(VM)的實作
- 記憶體管理與資源回收
- GIL,特別是在 3.13 之後 GIL 解禁之後有什麼不一樣
希望在不久的將來,在時間比較寬裕以及能力提昇之後,可以再回來挑戰這些主題 :)
## 主題
[第 1 章:來讀 CPython 原始碼](https://pythonbook.cc/chapters/cpython/introduction)
CPython 是最常見的 Python 實作品,先把專案原始碼下載一份到本機,順便寫一個簡單的 C 語言 Hello World 程式,為後續的 CPython 原始碼學習做準備。
[第 2 章:CPython 專案簡介](https://pythonbook.cc/chapters/cpython/project-structure)
介紹如何編譯並修改 CPython 原始碼以及專案目錄結構,順便範例展示了如何修改 CPython 原始碼,例如在進入 REPL 時顯示「Hello CPython」,離開時顯示「Bye」讓 Python REPL 禮貌地打招呼和道別。
[第 3 章:全部都是物件(上)](https://pythonbook.cc/chapters/cpython/pyobject-part-1)
在 CPython 裡,所有物件都由 PyObject 結構來表示,內含兩個主要成員:ob_refcnt 管理物件的引用計數,當計數降為 0 時,物件會被回收;ob_type 則指向 PyTypeObject,決定物件的型別及行為...
[第 4 章:物件生成全紀錄](https://pythonbook.cc/chapters/cpython/creating-an-object)
介紹從 Python 執行簡單的類別定義與實體化的過程,首先,程式碼經過 Tokenization 和 AST 解析,接著編譯為 Bytecode,再由 Python 虛擬機器執行...
[第 5 章:全部都是物件(下)](https://pythonbook.cc/chapters/cpython/pyobject-part-2)
在 CPython 中,PyTypeObject 是負責定義所有 Python 型別的關鍵結構,裡面包含了物件的名稱、大小、屬性和行為,並使用串列做為範例,展示了這個結構是如何運作。
[第 6 章:我的 Python 會後空翻!](https://pythonbook.cc/chapters/cpython/add-custom-builtin-type)
介紹如何在 CPython 裡面自製一個叫做 PyKitty_Type 的型別,並讓這個型別可以做出打招呼和後空翻的動作。
[第 7 章:匯入模組的時候...](https://pythonbook.cc/chapters/cpython/what-happened-when-importing-module)
Python 的模組匯入機制其實比我們平常看到的複雜許多。當我們使用 import 指令時,Python 會先從 sys.modules 查找已經匯入過的模組,如果沒找到,則透過內建的 importlib 模組來加載模組。import 和 from ... import ... 這兩種寫法在底層執行時略有不同,前者會直接匯入整個模組,而後者則會提取模組中的特定屬性。
[第 8 章:整數的前世今生](https://pythonbook.cc/chapters/cpython/numbers)
當在 Python 裡執行 n = 9527 這樣的程式碼時,背後發生了不少事。首先,數字 9527 會被編譯成 Bytecode,並存放在常數表中,等到執行時再從這裡取出。Python 的設計使它能處理任意大小的整數,因為可以用多個「digit」來分段儲存大數字,打破了其他程式語言的數字上限問題。此外,為了提升效能,Python 預先建立了從 -5 到 256 的小整數,這些整數會重複使用,而不是每次都新建物件。這樣的設計讓 Python 既靈活又高效。
[第 9 章:浮點數之小數點漂流記](https://pythonbook.cc/chapters/cpython/floating-point)
浮點數,顧名思義,它的「小數點」位置是可以浮動的,這樣的設計讓電腦能有效處理非常大的或非常小的數字。不同於日常生活中的小數,電腦內的浮點數是以類似科學記號的方式儲存,使用「有效數字」和「指數」來表示。Python 的浮點數其實就是 C 語言的 double,這是依照 IEEE 754 標準來處理的,因此會有精度不足的問題,這就是為什麼計算浮點數時會出現些許誤差。
[第 10 章:字串的秘密生活(上)](https://pythonbook.cc/chapters/cpython/strings)
當你在 Python 裡寫下 message = "Hello, World!",背後其實發生了很多事。這段程式碼會將字串「Hello, World!」編譯成 Bytecode 並建立一個字串物件。如果字串是空的,CPython 會從直譯器內部拿出同一個空字串;若字串包含 ASCII 字元,則會使用更節省記憶體的 ASCII 結構。不過,一旦字串含有像 Emoji 這樣的 Unicode 字元,Python 會自動轉換成較大的 Unicode 結構來處理。最重要的是,Python 的字串是不可變的,這表示每次修改字串,實際上都是產生一個新的字串物件。
[第 11 章:字串的秘密生活(下)](https://pythonbook.cc/chapters/cpython/more-about-strings)
在 Python 中,字串的操作其實不像表面看起來那麼簡單。比如字串串接時,雖然像 a = a + "😊" 這樣的操作看似直接,但實際上會產生一個新的字串物件,並透過內部的函數來進行字元的複製與串接。過程中,如果兩個字串的編碼相同,會直接進行記憶體層級的快速拷貝。如果編碼不同,則會轉換編碼並逐字複製,速度會慢一些。
另外,Python 也會利用字串內部化技術來節省記憶體,將常用的字串存入字串池,讓相同內容的字串共用同一塊記憶體,進一步提升效能。
[第 12 章:從準備到起飛!](https://pythonbook.cc/chapters/cpython/what-happens-when-python-starts)
當執行 python hello.py 時,Python 直譯器先讀取程式碼,然後經過一系列的處理步驟,最終執行並顯示結果。在這個過程中,程式碼會先被讀取並轉換成一種可以被理解的結構,接著編譯成可執行的形式,最後由 Python 的執行器執行並輸出「Hello Kitty」...
[第 13 章:參觀 Bytecode 工廠](https://pythonbook.cc/chapters/cpython/from-py-to-pyc)
當執行 Python 程式時,程式碼會被編譯成 Bytecode,並且生成 .pyc 檔案。這個檔案主要是為了加快執行速度,特別是被重複匯入的模組。但執行主程式時,Python 並不會自動生成 .pyc,因為多數情況下主程式只會執行一次,生成 .pyc 的必要性不大。 .pyc 檔案裡面除了存有魔術數字,還包含編譯後的 Bytecode,這些 Bytecode 是由一連串的 opcode 組成,Python 的虛擬機器會根據這些 opcode 來執行程式。
[第 14 章:串列的排隊人生](https://pythonbook.cc/chapters/cpython/lists)
使用 Python 的串列(List)的時候會發現它可以存放不同型態的資料,還可以動態增加或減少元素,而且還不用事先定義大小。這麼方便的設計背後,CPython 是如何實現的?串列的內部結構其實是透過一個指向元素的指針來運作,而不是直接存放資料,因此它能裝下各種型態的物件。而當需要更多記憶體時,CPython 會採取「過度分配」的策略,也就是一次多要一些空間,以減少頻繁重新分配的次數,提升效能。
[第 15 章:字典的整理收納課(上)](https://pythonbook.cc/chapters/cpython/dictionaries)
字典是 Python 中常用的資料結構,Key & Vaule 的組合存取資料,在 CPython 裡,字典的內部結構由 PyDictObject 定義,Key 跟 Value 分別存放在不同的物件中,這樣設計是為了效能與記憶體使用的平衡。
新增元素時,Python 會先透過計算鍵的雜湊值來決定其在字典中的位置,如果發生雜湊碰撞,系統會利用特殊的計算公式來找到下個可用的空位,並將鍵值對存放於對應的記憶體空間內。查找時則會根據計算出的索引快速定位到對應的鍵值。雖然這個流程聽起來有些複雜,但實際運行速度非常快,讓大部分情況下字典的查找效率接近 O(1)。
[第 16 章:字典的整理收納課(下)](https://pythonbook.cc/chapters/cpython/more-about-dictionaries)
在 Python 中,字典的記憶體空間會隨著加入的元素數量自動調整,這是為了在效能和記憶體使用之間取得平衡。當字典裡的空間不足時,系統會根據「負載因子」來決定何時需要擴展容量,通常當使用率超過 2/3 時會自動擴充空間。每次擴展的量約為原本的 2 倍,以減少碰撞機率並確保效能不受影響。然而,即使刪除元素後,已擴展的空間不會自動回收,除非特意清空字典或重新建立...
[第 17 章:不動如山的 Tuple](https://pythonbook.cc/chapters/cpython/tuples)
在 Python 裡 Tuple 是一種不可變的資料結構,意思是它一旦建立後,裡面的元素就不能更改、刪除或新增。建立 Tuple 時,Python 會透過特殊的函數來處理,無論是空的 Tuple 還是有資料的 Tuple,背後都有不同的流程處理。Python 還會使用「空閒列表」來有效管理記憶體回收,像是當元素數量小於 20 時,Python 不會馬上釋放記憶體,而是暫時保留以供未來使用。此外,Tuple 的不可變性意味著你無法直接修改它的內容,這個特性跟字串相似。Python 甚至對某些特定數量元素的 Tuple 做了些有趣的效能最佳化來提升運行效率。
[第 18 章:虛擬機器五部曲(一)](https://pythonbook.cc/chapters/cpython/pvm-code-objects)
在 Python 裡,函數也是物件,當我們定義一個函數時,Python 會建立一個 PyFunctionObject,裡面包含了函數的名字、參數、文件字串等資訊。函數背後最關鍵的部分是一個叫做 Code Object 的東西,這個物件儲存了函數的程式碼。在編譯階段,Python 將我們寫的程式轉換 Bytecode,然後將 Bytecode 包裝成 Code Object,最後由虛擬機器執行。
[第 19 章:虛擬機器五部曲(二)](https://pythonbook.cc/chapters/cpython/pvm-function-objects)
函數物件是透過 MAKE_FUNCTION 指令建立的,裡面包含了 Code Object、函數名稱、預設值等屬性。oparg 參數則用來決定函數物件具備哪些屬性...
[第 20 章:虛擬機器五部曲(三)](https://pythonbook.cc/chapters/cpython/pvm-frame-objects)
當呼叫一個 Python 函數,背後會建立一個叫 Frame 的物件。這個 Frame 會儲存像是區域變數、全域變數等資料,並形成一個堆疊結構,每次呼叫函數就會加上一個新的 Frame。當函數執行完畢後,這個 Frame 就會被清理掉。在 CPython 裡,Frame 包含許多重要的資訊,比如指向函數的程式碼、變數、甚至前一個 Frame,確保執行流程順暢,然後在適當時機被銷毀,讓記憶體資源得以回收。
[第 21 章:虛擬機器五部曲(四)](https://pythonbook.cc/chapters/cpython/pvm-namespaces-and-scopes)
當 Python 執行程式碼時,它會根據 LEGB(Local、Enclosing、Global、Built-in)的規則來查找變數,大家可能不知道的是,其實 Python 的編譯器會在程式執行前,就已經決定該如何查找變數了...
[第 22 章:虛擬機器五部曲(五)](https://pythonbook.cc/chapters/cpython/pvm-cell-and-closure)
Python 中的閉包(Closure)是怎麼運作的?閉包允許內層函數存取外層函數的變數,而這些變數會被包裝成所謂的 Cell Object。當我們定義一個包含閉包的函數時,Python 會自動建立這些 Cell,並將外層的變數放入其中,讓內層函數可以在之後使用。
[第 23 章:類別與它們的產地](https://pythonbook.cc/chapters/cpython/class-and-metaclass)
物件導向設計中,類別是用來建立實體(物件)的,但在 Python 世界裡,類別本身也是物件。那麼類別本身又是誰創造的?其實所有的類別都是透過它們的 metaclass 來建立。Python 裡的 class 語法背後其實是使用內建函數 __build_class__() 來完成這個過程,而 type 類別本身的 metaclass 也還是 type,這樣就形成了類別的遞迴關係。
[第 24 章:繼承與家族紛爭(上)](https://pythonbook.cc/chapters/cpython/class-and-inheritance)
在大多數程式語言裡,繼承可能沒那麼複雜,基本上就是把共用的功能寫在上層類別,然後讓下層類別繼承它就好了。但 Python 的繼承多了一個「多重繼承」,也就是一個類別可以同時繼承多個上層類別,讓整個結構複雜不少。
當 Python 要找到一個物件的方法時,會依據「MRO」(方法解析順序)來決定要先去哪一個上層類別找,這個順序是用一套規則計算出來的。當然,平常只用單一繼承時,你不會覺得有什麼不同,但多重繼承的情況下,MRO 就會幫忙決定方法的查找順序。
[第 25 章:繼承與家族紛爭(中)](https://pythonbook.cc/chapters/cpython/mro-and-c3-linearization)
Python 採用了 C3 線性演算法來解決方法繼承順序的問題。這個演算法能找出一個符合規則的單調次序,確保在多重繼承下方法的呼叫順序是合理的。
透過 MRO 可以看到 Python 是依據 C3 線性演算法決定方法查找的順序,先看上層,再看平行層,最後會到最上層類別 object。不過當遇到無法確定順序的繼承關係時,Python 會拋出錯誤。
[第 26 章:繼承與家族紛爭(下)](https://pythonbook.cc/chapters/cpython/mro-and-super)
MRO 是透過 C3 線性演算法來解決多重繼承的順序問題,而 super() 則是根據這個順序從 MRO 中的下一個類別開始查找方法。特別的是,Python 的 super() 並不是單純呼叫上層類別的方法,而是透過一個代理物件來動態解析方法,確保不會跳過任何一個繼承的類別,從而避免了多重繼承時的「鑽石問題」。
[第 27 章:產生一個產生器](https://pythonbook.cc/chapters/cpython/yield-and-generator)
產生器在 Python 裡的運作很好玩。簡單來說,透過 yield 關鍵字,可以讓函數暫停並回傳值,這對處理大量資料或無限集合特別實用。產生器的內部狀態會記錄在 PyGenObject 裡,包含名稱、例外狀態、以及執行狀態。雖然產生器能省下記憶體,但它的實作有點複雜,特別是在管理狀態和例外處理的部分。
[第 28 章:轉呀轉呀七彩迭代器](https://pythonbook.cc/chapters/cpython/iterators)
迭代器在 Python 裡非常常見,它讓我們能很簡單的遍歷不同類型的「容器」,像是字串、串列、Tuple 或是字典等結構。Python 中的迭代器只要有遵循「迭代器協議」就能被稱為迭代器。Python 的 iter() 函數背後有幾個有趣的實作細節,例如它支援一種「哨兵」機制,可以用來指定當某個條件達成時停止迭代。不同的資料型態,如串列、字典和範圍,都有各自的迭代器實作方式。
[第 29 章:無所不在的描述器](https://pythonbook.cc/chapters/cpython/descriptor)
描述器(Descriptor)其實是 Python 中很常見的東西,只是你可能不知道自己已經在用了。它可以控制屬性的存取,讓我們在讀取或設定屬性時,背後做一些額外的事。描述器有兩種:資料描述器和非資料描述器,區別在於是否實作了特定的方法...
[第 30 章:例外處理的幕後真相](https://pythonbook.cc/chapters/cpython/exceptions)
Python 處理例外的方式很直觀,使用 try...except 就能應付各種可能的錯誤。當然,這背後在 CPython 裡可是有一整套機制運作。當發生例外時,Python 會把錯誤資訊堆疊起來,方便之後的處理。除此之外,Bytecode 也會包含一些用來追蹤例外的表格(ExceptionTable),確保我們能跳到正確的處理位置。最後,finally 區塊更是保證不論發生什麼事都會被執行,讓程式有條理地結束...
---
## 你是數字,我是數字,我們不一樣!
- URL:https://kaochenlong.com/same-value-but-different-object-in-python
- 發佈日期:2024-10-13
在 Python 裡的 `id()` 函數可以算出某個值或物件在 Python 世界裡的「編號」,如果有兩個值算出來的編號是一樣的,表示這兩個值不只相等,而且還是一顆物件。
那麼問題來了,我寫了一個程式叫做 `hello.py`,內容如下:
```python
a = 100
b = 200
def hey():
return 300
res1 = 300
res2 = 100 + 200
res3 = a + b
res4 = hey()
print(id(res1))
print(id(res2))
print(id(res3))
print(id(res4))
```
當我執行 `python hello.py` 的時候,最後四行印出來的 id,有一個跟其它三個是不一樣的,猜猜看是哪一個。
答案是 `res3`,它算出來的數字跟其它三個不同。
先聲明,我使用的 Python 版本是 `3.12`,也許在不同的版本會有不同的結果。
為什麼這些看起來答案都是一樣的數值,卻有其中一個不一樣?在 Python 為了讓效能變的更好有做了不少調整,像是針對數字的效能最佳化,在 -5 ~ 256 之間的「小整數」,因為這些數字很常被使用,所以這些數值都直接編譯到 Python 直譯器本體,每次需要用到的時候就不用重新產生一個數字物件,就是因為這樣才會有以下這個看起來有點奇妙的現象:
```
>>> a = 256
>>> b = 256
>>> a is b
True
>>> a = 257
>>> b = 257
>>> a is b
False
```
這應該也不是什麼秘密了,如果想知道更多細節,可參考[整數的前世今生](https://pythonbook.cc/chapters/cpython/numbers#%E5%B0%8F%E6%95%B4%E6%95%B8)文章介紹。
不過這題我有刻意避開了小整數的範圍,因為重點不在這裡。如果想要知道 Python 是怎麼執行我們寫的程式,就得看看 `hello.py` 編譯成 Bytecode 變成什麼樣子。因為內容有點長而且混在一起,所以我一段一段整理給大家看看。首先是 `res1 = 300` 這行:
```plaintext
7 16 LOAD_CONST 3 (300)
18 STORE_NAME 3 (res1)
```
這相當簡單,就是載入一個常數 300,然後指定給 `res1` 變數。再看看 `res2 = 100 + 200`:
```plaintext
8 20 LOAD_CONST 3 (300)
22 STORE_NAME 4 (res2)
```
咦?有發現奇怪的地方嗎?原本我們的程式碼是寫 `100 + 200`,但 Python 直接幫我們算好,然後把結果指定給 `res2`,看起來跟 `res1` 是一樣的。
Python 為了讓效能更好一點,在編譯成 Bytecode 之前會先進行「常數折疊」(Constant Folding) 的操作,試著計算那些可以提前得知結果的運算。例如這裡的 `100 + 200` 被先計算成 `300`,然後直接作為常數編譯到 Bytecode 中,這樣可以減少在執行階段的不必要計算量。
這裡還有個小細節,在 `LOAD_CONST 3` 後面的 `3`,這跟剛才 `res1 = 300` 的 `LOAD_CONST 3` 是一樣的,表示這是從同一格堆疊上取得並載入的值,所以這會是同一個常數 300。
我們暫時跳過第三個,先看最後一個 `res4 = hey()`:
```plaintext
4 10 LOAD_CONST 2 ()
12 MAKE_FUNCTION 0
14 STORE_NAME 2 (hey)
10 34 PUSH_NULL
36 LOAD_NAME 2 (hey)
38 CALL 0
46 STORE_NAME 6 (res4)
Disassembly of :
4 0 RESUME 0
5 2 RETURN_CONST 1 (300)
```
上半段是定義 `hey()` 函數,中段的 `CALL` 也很好猜,就是在呼叫 `hey()` 這個函數。最後一段的 `RETURN_CONST` 回傳一個常數 300。但是,這裡的 300 跟前面的 300 是同一個數字物件嗎?這裡就是細節了...
Python 在建立函數的時候,更精準的說是在建立函數裡面的 Code Object 的時候,裡面有個叫做 `const_cache` 的東西,這東西從名字看起來大概可以猜到是常數的暫存。簡單的說,就是在函數裡需要用到常數的時候,會先檢查是不是已經有人用過同樣的常數,如果有,就會直接取用同一個值。對這部份實作有興趣的話,可搜尋原始碼 `makecode()` 函數的實作。
好,到這裡,`res1`、`res2` 跟 `res4` 都是同一個常數 300,所以使用 `id()` 函數計算出來的結果都是一樣的。
那 `res3 = a + b` 呢?來看看它的操作:
```plaintext
1 2 LOAD_CONST 0 (100)
4 STORE_NAME 0 (a)
2 6 LOAD_CONST 1 (200)
8 STORE_NAME 1 (b)
9 24 LOAD_NAME 0 (a)
26 LOAD_NAME 1 (b)
28 BINARY_OP 0 (+)
32 STORE_NAME 5 (res3)
```
這個行為明顯跟 `res1` 或 `res2` 不一樣了,這個 `BINARY_OP` 會拿前面堆疊上的 `a` 跟 `b` 以及加號 `+` 進行運算。如果再深入追原始碼的話(有興趣可搜尋原始碼的 `_PyLong_Add()` 函數),會發現 `a + b` 的過程會產生並回傳一顆新的數字物件,雖然它也是數字 300,但這個 300 跟前面用 `LOAD_CONST` 載入的 300 就不是同一個物件了。
當然,如果把這裡的數字改成 -5 ~ 256 之間的小整數的話,就會發現這四行的答案都是一樣的了。
另外,不知道大家有沒發現我刻意而且還有強調把這些程式碼寫在一個 `.py` 檔裡然後執行它,雖然 300 這個數字並不在小整數的範圍內,所以沒辦法享受當伸手牌拿現成數字來用的待遇,但因為執行整個檔案的時候,這個 300 就是一個被定義過的常數,在同一次的編譯裡可以被重複使用。
不過,如果你進到 Python 的 REPL 環境就不一樣了:
```
>>> a = 100
>>> b = 200
>>> def hey():
... return 300
...
>>> res1 = 300
>>> res2 = 100 + 200
>>> res3 = a + b
>>> res4 = hey()
>>> id(res1)
4298043664
>>> id(res2)
4296275504
>>> id(res3)
4298043920
>>> id(res4)
4296275632
```
在 REPL 不會有「常數折疊」的機制,而且每次的執行,都是一個全新的開始,每次的執行都會進行一次編譯,所以除非是 -5 ~ 256 之間的小整數,否則這四個 300 可能都會是不同的 300。
---
## 空虛的真
- URL:https://kaochenlong.com/vacuous-truth
- 發佈日期:2024-10-09
來一個我自己覺得滿有趣的 Python 程式問答題。
在 Python 裡,`all()` 這個函數可以接一個串列,串列裡的每一顆元素都成立的時候就會回傳 `True`,反之只要有一顆不成立,就會是 `False`,例如:
```python
>>> all([True, True, True])
True
>>> all([True, False, True])
False
```
題目來了:
```python
>>> all([]) # A
>>> all([[]]) # B
>>> all([[[]]]) # C
```
一堆中括號、小括號,眼睛都快看花了,在正式的專案千萬不要這樣整自己(或是你的同事)!大家猜猜看 A、B、C 的結果分別是什麼?
答案是 `True` / `False` / `True`
首先,我們得先看看 `all()` 這個函數的設計,當如果裡面所有的元素都是 `True` 的時候會得到 `True`,但文件上跟原始碼裡都有一行但書:
> If the iterable is empty, return True.
也就是說如果是 `all([])` 的話,會得到 `True`,至於為什麼是這樣的結果我們待會再看。
第二題,因為在 Python 裡空的串列 `[]` 會被當做 `False` 看待,所以 `all([[]])` 相當於 `all([False])`,所以答案是 `False`。
最後一題,`[[]]` 是一個串列,它是一個包含空串列的串列,但它本身不是一個空的串列。有點像是箱子裡面裝了一個箱子,就算內層的箱子是空的,對外層的箱子來說它也是有裝東西的,就是裝了你這個空箱子!
對於非空串列,在 Python 會被當做 `True`,所以 `all([[[]]])` 等於是 `all([True])` 的效果,答案會得到 `True`。繼續延伸下去,`all([[[[[[[[]]]]]]]])` 就算裡面包再多層次,答案也是 `True`。
### 真空?空虛?
這裡比較難理解的可能是為什麼 `all([])` 是 `True`。這不是腦筋急轉彎,也不是設計者故意惡搞,其實這是刻意的設計。在數學或邏輯上稱為「Vacuous Truth」。我覺得中文翻譯成「空虛的真」滿酷的,聽起來有點像領域展開之類的中二招式。它的意思是想要表達在這個空間裡沒有任何一個條件是不成立的,用一般路人也聽的懂的話來舉個例子:
> 如果房間裡一隻手機都沒有,那麼「房間裡所有的手機都已經關機」這句話就是成立的。
事實上,你要說「房間裡所有的手機都已經開機」或是「房間裡每一隻手機都是 iPhone」也都是對的。也就是說,對於一個空的陣列或集合來說,透過 `all()` 函數問它是否每個元素都成立(或都不成立),基本上沒什麼太大的意義,因為都會成立。
如果用幾個希臘字母來寫的話:
∀𝑥(𝑥∈𝐴 → 𝑃(𝑥))
這幾個字母組成一起的意思是「對所有的 𝑥,如果 𝑥 屬於集合 𝐴,那麼 𝑥 具有性質 𝑃」。在邏輯中,當一個條件語句(若 p 則 q)的前提(p)永遠不成立時,無論結論(q)是真是假,整個條件語句都被認為是成立的。因為「 𝑥 屬於集合 𝐴」這個前提永遠不會成立(因為集合 𝐴 是空集合),所以無論 𝑃(𝑥) 是什麼,整個陳述都成立。
如果有興趣翻 CPython 的原始碼,就會發現它的實作滿簡單的,為了簡化,我把一些判斷錯誤、處理記憶體跟相對比較不重要的語句拿掉,看起來像這樣:
```c
// 檔案:Python/bltinmodule.c
static PyObject *
builtin_all(PyObject *module, PyObject *iterable)
{
PyObject *it, *item;
int cmp;
for (;;) {
item = iternext();
if (item == NULL)
break;
cmp = PyObject_IsTrue(item);
if (cmp == 0) {
Py_RETURN_FALSE;
}
}
Py_RETURN_TRUE;
}
```
簡單的說,就是用 `for` 跑個無窮迴圈,把元素一個一個拿出來,如果有任何一個值是 `False` 就結束迴圈,不然最後就回傳 `True`。簡化成 Python 的寫法,大概就是:
```python
def all(iterable):
for element in iterable:
if not element:
return False
return True
```
在 Python 跟 `all()` 類似邏輯的還有 `any()`,大家可想想看,如果把原本題目裡的 `all()` 換成 `any()`,答案會變成什麼。
不是只有 Python,Ruby 的`.all?` 方法也是這樣的設計。如果你是前端工程師,你去看看 JavaScript 陣列的 `.every()` 方法也一樣:
```javascript
// 每個元素都大於 1000
> [1450, 9527, 1314].every((x) => x > 1000)
true
// 有一個沒有滿足條件
> [1450, 9527, 520].every((x) => x > 1000)
false
// 但是沒有人不滿足條件!
> [].every((x) => x > 1000)
true
```
對於空的集合體來說,基努李維也說過(並沒有),就算你說 1 加 1 等於 5,也是對的:
```javascript
> [].every(() => 1 + 1 == 5)
true
```
是說,當你跟朋友經過一家餐廳門口,餐廳裡一個客人都沒有,但你朋友突然說「喂,這家餐廳裡的所有客人都在低頭安靜的吃飯耶!」,這句話雖然是符合邏輯,但放在七月份就會覺得有點嚇人了。
---
## 為你自己學 Python
- URL:https://kaochenlong.com/pythonbook
- 發佈日期:2024-09-02
為你自己學 Python
網址:
TL;DR, 先說結論:
這是我最近寫的書「為你自己學 Python」,實體書 & 電子書正在編輯中,網站上的內容除另有標示外,將會以 CC BY-NC-SA 4.0 方式授權予公眾自由取用。
希望對想要學習 Python 程式語言的朋友有些幫助 :)
其實最一開始的故事,是因為家裡讀國三的小朋友跟我說說學校好像要開始教 Python,所以我就雞婆的想來寫一本 Python 入門書給他(我猜這個中二年紀的小朋友大概不會領情)。剛好我們家今年開始調整課程方向,所以也就變成我們家培訓班的教材。
近幾年我都會參加 iThome 的鐵人賽,但我看了一下這本書的 Git Log,第一次 commit 是 10 個月前的事,這寫的比鐵人賽還認真!其實原本今年農曆年前就已經寫的差不多了,但因為對自己寫的文字不太滿意所以整個大改一次,再加上有些部份不確定運作原理,所以花了一些時間翻 CPython 的原始碼才比較知道是怎麼回事,拖到現在才丟出來。
正因為如此,沒意外的話,CPython 的原始碼大概會變成我今年鐵人賽的題目了,內容也一樣會更新一份在這個網站。
我這人比較囉嗦,所以一不小心就越寫越多,簡單算了一下,好像光是基礎應用篇就寫了超過 500 頁,再加上我猜學 Python 想做網站的人也可能沒那麼多,所以我會把 Flask、FastAPI 以及 Django 的內容放在另一本書。
目前進度:
- 基礎應用篇:已完成
- 網站開發篇:
- Flask 約完成 85 %
- Django 約完成 70 %
- FastAPI 完成度最低,大概只有 20 %
預計應該會於九月底或十月初完成 & 同樣以 CC 授權釋出。
有朋友問到我本來不是寫 Ruby 的嗎?為什麼改成 Python 了?大家別誤會,我現在還在寫 Ruby 喔!但可能知道的人不多,在我開始寫 Ruby 之前就是寫 Python 的,在當年 Python 3 才剛推出沒多久而且 Django 都還沒有內建 Migration 的時代就用它幫客戶做活動網站,也曾經在中研院 OSSF 開過好幾次的工作坊。後來有次參加了某位 Ruby 圈大大的課(現在變成 AI 圈的大大了)發現 Ruby 很有趣就轉去寫 Ruby 寫了十幾年直到現在,只是因應小朋友以及市場需求,才又再次把 Python 撿回來。
雖然說這是我們家培訓課程的教材,但老實說,我並不認為我所學的這些內容有什麼特別之處,很多知識也是從網路上的開源資源學來的。秉持著吃果子拜樹頭的精神,本書內容將全部公開於「為你自己學 Python」網站上,網站上內容除另有標示外將以 CC BY-NC-SA 4.0 方式授權。希望可以藉由這樣免費開源的方式釋出,讓大家可以用更低的成本習得寫程式的技能。
另外,我是個買書會看封面顏值的人,內容先不說,光是封面設計的漂亮我大概就會下單了。所以雖然書還沒送印,封面也畫好了。不得不說,這本書的封面我真的超喜歡的,從顏色搭配到角色設計,處處都是細節,我知道這年頭 AI 繪圖越來越厲害,但我還是喜歡這種有溫度的手繪設計。很榮幸也很開心能有機會請大師出馬,讓我的書有這麼漂亮的封面。
Python 是個看起來簡單但到處充滿細節的程式語言,我盡力把這本書寫得盡可能的完整與正確,但我就只是個普通人,而且還是不務正業的非資訊本科系畢業,技術跟經驗都有限,也都還在學習的過程中,難免有不小心寫錯或是觀念理解錯誤的地方,還請先進們多多包涵並不吝指正。
以上,希望對想要學習 Python 程式語言的朋友有些幫助 :)
---
## 為什麼索引值從 0 開始算?
- URL:https://kaochenlong.com/why-array-index-begin-with-zero
- 發佈日期:2024-03-03
有沒有想過為什麼我們學的數學是從 1 開始算,但好像大部份比較主流的程式語言的陣列包括 Python 的串列,索引值卻是從 0 開始的?因為這是因為陣列的索引值並不是指向元素本身,比較像是記憶體位置的「偏移量」。
假設有一個陣列像這樣:
```py
chars = ['a', 'b', 'c', 'd']
```
當我們說「變數 `chars` 指向 `['a', 'b', 'c', 'd']` 陣列」的時候,並不是 `chars` 這個變數直接指向這一整個陣列,而是 `chars` 變數指向這個陣列的「起始位置」,更精準的說是記憶體位置。陣列在記憶體裡的位置跟門牌號碼一樣都是連號的,假設這個陣列的起始記憶體位置是 `1450`,然後每個元素所佔的位置或說格子的大小我就先隨便假設是 8(先不管單位跟以及為什麼是 8)。大概示意圖如下:

這樣的話,你認為要怎麼取得第一個元素的 `a` 字元?不就是指向 `1450` 的位置就拿到了嗎?(你也可以說是從 `1450` 到 `1458` 這一塊空間)。
我們現在知道怎麼拿第一個元素了,接著要怎麼拿第二個?其實只要知道第一個元素的位置再搭配簡單的數學加法跟乘法就能算出來了:
> 起始位置 + (1 x 8) = 1458
同理可證,第三個、第四個直到第 N 個元素就能推導出計算公式:
> 起始位置 + (N x 8)
到這裡,大家有看出來上面公式裡的 `N`,剛好就是等於我們平常在講的索引值嗎?所以索引值其實就是記憶體位置的「偏移量」,因此才會從 0 開始算。
所以陣列索引值從 0 開始的概念從滿早期的程式語言開始就有了,大家用著用著也習慣了,Python 以及其它程式語言也都受到影響,所以 Python 的串列索引值也跟是從 0 開始算。
事實上,也不是所有程式語言的索引值都是從 0 開始算的,不同的程式語言對這件事有不同的觀點,像程式語言 [Lua](https://www.lua.org/)、[Fortran](https://fortran-lang.org/)、[COBOL](https://zh.wikipedia.org/zh-tw/COBOL)、[Julia](https://julialang.org/) 跟 [R 語言](https://www.r-project.org/)等都是從 1 開始算的,也許在他們的觀點來看,`chars[1]` 就是第一個或第一欄,看起來更簡單、直覺。
---
## Python 好用的 F 字串!
- URL:https://kaochenlong.com/f-strings-in-python
- 發佈日期:2024-02-29
Python 裡的 F 字串(F-String)不只是可以直接把變數塞到字串裡、看起來更好看、更好寫而已,它還有一些有趣的用法,像是我們常會遇到需要幫錢錢加上千位數字的逗點,在其它程式語言可能需要透過其它函數或是另外自己寫函數來處理,用 F 字串可以輕鬆做到這件事,例如:
```py
my_money = 1000000
print(f"{my_money:,}") # 印出 1,000,000
print(f"{my_money:.2f}") # 印出 1000000.00
print(f"{my_money:,.2f}") # 印出 1,000,000.00
```
在 F 字串裡的變數後面加上 `:` 可以幫我們做格式化,這裡的 `,` 就是在千位數字的地方加上逗點,`.2f` 是指可以讓原本的整數以小數點以下 2 位數的方式呈現,如果組合在一起 `,.2f` 就會變成整數部份加上千位數逗號,小數部份則呈現到小數點以下 2 位數。
有時候我們在做一些數學計算之後得到的結果想要用百分比的形式,你可能得先把它乘以 100 再後面加個 `%` 符號,像這樣:
```py
ratio = 0.315
print(f"{ratio * 100}%") # 印出 31.5%
```
這沒什麼問題,在別的程式語言也差不多是這麼做,但 Python 的 F 字串可以更簡單幫我們做到這件事:
```py
ratio = 0.315
print(f"{ratio:.1%}") # 印出 31.5%
```
`.1` 表示顯示到小數點以下第 1 位,後面的 `%` 就是直接幫我們換成百分比的形式,至於這樣寫有沒有比較簡單或直觀可能因人而異了,因為有些人少了乘以 100 可能會覺得哪裡怪怪的。
如果有需要的話,我們同樣也可以透過 `:` 來設定字串的「寬度」,例如:
```py
pi = 3.1415926
print(f"|{pi:5}|") # 印出 |3.1415926|
print(f"|{pi:20}|") # 印出 | 3.1415926|
```
我刻意在字串的兩邊加上 `|` 是為了讓大家看得更清楚一點,`:` 後面接的數字就是這個 `{}` 字串的寬度,如果字串本身的字數超過寬度(例如 `5`),字串還是會把設定的寬度給撐開,如果我設定寬度大一點,例如設定成 `20`,這樣會在字串的左邊補上空白,呈現出來的效果就會變成靠右對齊。
有靠右對齊,自然也有靠左對齊跟置中對齊,這時候可以在寬度的數字前面加上一個 `-` 符號,例如:
```py
pi = 3.1415926
print(f"|{pi:<20}|") # 靠左對齊
print(f"|{pi:>20}|") # 靠右對齊
print(f"|{pi:^20}|") # 置中對齊
```
印出來的結果會是:
```
|3.1415926 |
| 3.1415926|
| 3.1415926 |
```
以做網站來說,因為瀏覽器不太會理會 HTML 裡多餘的空白字元,所以這種格式化對齊大概沒什麼機會派上用場,但如果是用在做一些資料統計或分析的小程式,並打算在終端機畫面呈現或是列印報表的時候,這種對齊就挺方便的了,例如:
```py
print(f"1. {'為你自己學 Python':<20} {'NTD 120'}")
print(f"2. {'為你自己學 Git':<20} {'NTD 200'}")
print(f"3. {'為你自己學 Ruby':<20} {'NTD 180'}")
print(f"4. {'為你自己學 Rust':<20} {'NTD 250'}")
```
印出來的結果會變這樣:
```
1. 為你自己學 Python NTD 120
2. 為你自己學 Git NTD 200
3. 為你自己學 Ruby NTD 180
4. 為你自己學 Rust NTD 250
```
填補的字串預設是使用空白字元,但如果你喜歡也可以換成別的字,例如:
```py
pi = 3.1415926
print(f"|{pi:x<20}|") # 靠左對齊
print(f"|{pi:x>20}|") # 靠右對齊
print(f"|{pi:x^20}|") # 置中對
```
我用了 `x` 字元來填補空白,印出來的結果會變成:
```
|3.1415926xxxxxxxxxxx|
|xxxxxxxxxxx3.1415926|
|xxxxx3.1415926xxxxxx|
```
F 字串也能幫我們把整數轉換成二進位、八進位或十六進位,例如:
```py
# 格式化成二進位
print(f"{10:b}") # 印出 1010
# 格式化成八進位
print(f"{10:o}") # 印出 12
# 格式化成十六進位
print(f"{255:x}") # 印出 ff
```
還記得以前學過的「科學記號表示」嗎?F 字串也能幫我們搞定,例如:
```py
speed_of_light = 299792458 # 光速
print(f"{speed_of_light:.3e}") # 印出 2.998e+08
```
`.3` 代表小數點後面要顯示幾位數,`e` 代表科學記號,這樣就能把數字格式化成科學記號表示法。最後,F 字串還能順便幫我們搞定日期格式:
```py
from datetime import date
the_day = date(1947, 2, 28) # 228
print(f"{the_day:%Y/%m/%d}") # 印出 1947/02/28
```
其中 `%Y` 跟 `%m` 跟 `%d` 是分別代表代表年份、代表月份以及日期,這些格式化參數的用法跟其它程式語言的格式化差不多,應該不難查到分別代表哪些意思。雖然在其它程式語言也有類似這種把變數塞進字串裡的「字串安插(String Interpolation)」功能,但 Python 的 F 字串還能加上格式化工具,能玩的把戲就更多了
---
## Python 的 String Interning 機制
- URL:https://kaochenlong.com/string-interning-in-python
- 發佈日期:2024-02-27
許多程式語言會使用 2 個等號 `==` 比較兩個值是否相等,在 Python 也是一樣,但 Python 還有個特別的比較方法 `is`,它可以用來比較這兩個值是不是同一個物件:
```py
>>> a = [1, 2, 3]
>>> b = [1, 2, 3]
>>> a == b
True
>>> a is b
False
```
雖然變數 `a` 跟 `b` 看起來都是串列 `[1, 2, 3]`,但實際上它們在 Python 是兩個不同的東西,你可以想像成我們從網路上買了兩盒一樣的玩具,如果沒有特別的碰撞或破損的話,這兩盒玩具的包裝盒看起來都一樣,裡面的玩具看起來也是一樣,但這兩個玩具本質上就是兩個不同的玩具。在 Python 有個叫做 `id()` 的內建函數,可以用來查看某個東西在 Python 世界裡的「編號」:
```py
>>> a = [1, 2, 3]
>>> b = [1, 2, 3]
>>> id(a)
4303554880
>>> id(b)
4303556672
```
可以看的出來這兩個串列雖然看起來一樣,但內部的編號是不同的。再來給大家看一個例子:
```py
>>> a = [1, 2, 3]
>>> b = a
>>> a == b
True
>>> a is b
True
```
第二行的 `b = a` 的意思,就是把 `a` 的值指派給 `b`,所以這時候不管是 `a` 或 `b` 都是指向同一個地方,它們的 `id()` 函數的執行結果也會一樣,所以 `a is b` 會是 `True`。到這裡應該還算容易理解,接著來看比較難一點的:
```py
a = "hellokitty"
b = "hellokitty"
print(a == b) # 印出什麼?
print(a is b) # 印出什麼?
```
猜猜看,你覺得會印出什麼結果?`==` 的比較容易猜,就是比對這兩個變數的值是不是一樣,所以會得到 `True`。而 `is` 會比較這兩個 `"hellokitty"` 字串是不是同一個,這在其它的程式語言可能會有不同的結果,我用 Ruby 舉個例子:
```rb
>> a = "hellokitty"
>> b = "hellokitty"
>> a.object_id
=> 47540
>> b.object_id
=> 56940
```
這裡的 `.object_id` 方法跟剛剛提到的 `id()` 函數的功能差不多,在 Ruby 裡的字串雖然看起來一樣,但它們實際上在記憶體裡就是兩個不同的東西。這在 Python 裡會有不同的結果,還記得我們在上個章節才講到 Python 的字串本身的設計是不可變(Immutable)的嗎?既然不能變,所以 Python 內部會建立一個「表格」用來存放一些字串,試著盡可能的把同樣的字串建放同一個地方,以節省一些記憶體空間,這個機制叫做 String Interning(有興趣去翻 Python 的原始碼就會發現這個「表格」本質上是一個字典)。當你試著把一個字串被被命名為某個變數的時候,Python 會決定要不要使用之前曾經使用過的字串。
```py
>>> id("hellokitty")
4305320112
>>> id("hellokitty")
4305320112
>>> id("hellokitty")
4305320112
```
但也不是所有的字串都會走這個機制,例如這個例子:
```py
>>> id("hello kitty")
4305197104
>>> id("hello kitty")
4305197424
>>> id("hello kitty")
4305195568
```
String Interning 機制主要會發生在幾個情況,只有字數比較少而且只包含英文字母、數字、底線的字串才會幫我們處理,在上面這個範例中的字串有空白字元,Python 就沒有幫我們保留下來,而是每次都會產生一個新的字串。如果你想要強制讓 Python 發動 String Interning 機制的話,可使用內建模組 `sys` 的 `intern()` 函數:
```py
>>> import sys
>>> a = sys.intern("hello kitty")
>>> b = sys.intern("hello kitty")
>>> a is b
True
```
不只字串,數字也有類似的機制,但這個機制不會無限上綱到所有的數字,只有在 `-5` 到 `256` 之間的整數才會發生:
```py
>>> a = 256
>>> b = 256
>>> a is b
True
# 超過範圍了
>>> a = 257
>>> b = 257
>>> a is b
False
```
因為 -5 到 256 之間的數字很常用到,所以 Python 在啟動的時候有先幫我們就把這些數字先準備好,如果在程式裡用到的話就不用另外重新產生一份。
---
## Python 的 Pattern Matching
- URL:https://kaochenlong.com/pattern-matching-in-python
- 發佈日期:2024-02-27
假設你曾經寫過其它的程式語言,遇到這種一大串的 `else if` 組合的時候,通常會想到的是 `switch` 或 `case...when` 之類的語法。在 Python 並沒有這樣的設計,真的遇到一堆判斷就是用 `if...elif...else...` 寫就是了。這樣的設計是因為 Python 的作者認為 `switch` 語法並不是那麼的必要,而且 `if...elif...else...` 也可以達到相同的效果,所以就沒有加入 `switch` 語法。沒有 `switch` 或 `case` 的設計並沒什麼大不了的,事實上有些程式語言就算有 `switch` 或 `case` 的寫法,本質上也只是 `if..else` 而已。
## match 比對
其實 `switch` 語法一堆人在敲碗,但碗都敲破了也沒有用,提議一直被否決,就覺得[不太需要](https://docs.python.org/3/faq/design.html#why-isn-t-there-a-switch-or-case-statement-in-python)。不過在許多人的努力,在 PEP 上討論了好陣子,終於在 [PEP 634](PEP 634 https://peps.python.org/pep-0634/) 定案,並且 Python 3.10 實作了新的語法 `match`,寫起來的手感跟其它程式語言的 `switch` 語法有點像。`match` 語法的正式名稱叫做「結構化模式比對(Structural Pattern Matching)」,由於是在 3.10 版之後才有,所以太舊版本的 Python 是沒有支援這種寫法。假設我有原本個簡單流程判斷像這樣:
```py
weather = "rain"
if weather == "rain":
print("下雨天")
elif weather == "sunny":
print("出太陽")
elif weather == "cloudy":
print("多雲")
else:
print("不知道")
```
使用 `match` 語法的話,可以寫成這樣:
```py
weather = "rain"
match weather:
case "rain":
print("下雨天")
case "sunny":
print("出太陽")
case "cloudy":
print("多雲")
case something:
print(f"我不知道 {something} 是什麼天氣")
```
前面三個 `case` 分別是在比對字串(Python 會使用 `==` 來進行比對),如果比對成功的話就會結束整個比對的過程,而最後一個 `case` 是指如果上面所有的路線都比對失敗的話會走的路,差不多就是 `else` 的概念。在後面加上變數名稱的話表示比對到的值會被指派給這個變數。至於要用什麼名字倒是無所謂,但如果發現在這個 `case` 裡根本沒打算使用它的話,可以給它一個底線 `_`:
```py
weather = "rain"
match weather:
case "rain":
print("下雨天")
case "sunny":
print("出太陽")
case "cloudy":
print("多雲")
case _:
print("我根本不在乎是什麼天氣")
```
那個 `_` 代表「I don't care」的意思。看到這裡,大家有覺得這樣語法看起來有比較清楚嗎?老實說我覺得並沒有差多少。
如果只是這樣的簡單比對的話,我也認同用 `if..else..` 還比較直覺一點。但如果你以為 `match` 語法就只有這樣,那你就太小看 Pattern Matching 了,Pattern Matching 在其它程式語言可是大家搶著抄但又不一定抄的起來的好用設計。再舉個例子,假設我想判斷某個值是數字還是字串,用 `if..else..` 寫起來大概像這樣:
```py
value = True
if type(value) in [int, float]:
print("數字")
elif type(value) == str:
print("字串")
else:
print("我不知道你是誰")
```
`match` 不只能比對簡單的值,還能比對型別,所以改用 `match` 寫的話:
```py
data = 123
match data:
case int() | float():
print("整數")
case str():
print("字串")
case _:
print("其他型別")
```
在第一個 `case` 中間的那個 `|` 表示是「或者」的意思,雖然整個行數沒有變比較少,但程式碼的可讀性有比較好一點點。`match` 還能用來比對串列、字典、集合等等資料結構,在處理比較複雜的資料結構時挺方便的:
```py
user_data = [1, "悟空", 18]
match user_data:
case [id, name, age]:
print(f"{id} - 我是 {name},我今年 {age} 歲")
case _:
print("你好")
```
在 `user_data` 裡面有三個值,透過 Pattern Matching 如果有「比對」到一樣的模式,被比對到的部份可以直接變成變數。也許你並不需要所有的變數,如果你只是想要得到資料裡的第一個值,剩下的不需要,可以用底線 `_` 來代表不需要的部份:
```py
user_data = [1, "悟空", 18]
match user_data:
case [id, _, _]:
print(f"會員編號:{id}")
case _:
print("hi")
```
這應該就不是一般的 `if..else..` 或其它程式語言的 `switch` 能夠輕鬆做到的事了。
除了串列、Tuple 能比對,字典也能比對:
```py
user_data = {"name": "Kitty", "gender": "female"}
match user_data:
case {"gender": "male", "name": name}:
print(f"{name} 先生您好!")
case {"gender": "female", "name": name}:
print(f"{name} 女士您好!")
case _:
print("您好!")
```
原本一般大概會使用 `if..else..` 來判斷 `gender` 的值再決定怎麼打招呼,但透過 Pattern Matching 可以直接比對到字典裡 `gender` 的值,而且順序還不需要一樣,這樣的寫法看起來更直覺。Python 的 `match` 不只這樣,還能在 `case` 後面加上條件,像這樣:
```py
number = [10, 0]
match number:
case x, y if y != 0:
print(x / y)
case _:
print("第二個數字不能為零!")
```
另外,`match` 不只一次比對一個,還能使用 `*` 符號一次比對多個,像這樣:
```py
numbers = [1, 2, 3, 4, 5]
match numbers:
case _, *others:
print(others)
case _:
print("Hello Python")
```
第一個元素我刻意使用 `_` 表示我不需要,但後面的元素可以使用 `*` 符號把它全部收下來,這個應該也是其它程式語言的 `switch` 辦不到的事。講了這麼多關於 `match` 的介紹,最後用一個簡單但算經驗的例子來做個收尾,大家不知道有沒有聽過「費氏數列(Fibonacci)」?這個數列的規則是:
- 第 1 項是 0
- 第 2 項是 1
- 第 3 項開始每一項都是前兩項的和,所以這個數列 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ... 以此類推。
這東西跟我們的年夜飯的概念有點像,大年初三吃的就是大年初一加上初二的總合。這個數列是很經典的「遞迴函數」的實作,用 `match` 可以寫的很簡潔易懂:
```py
def fib(n):
match n:
case n if n < 2:
return n
case _:
return fib(n - 1) + fib(n - 2)
print(fib(6)) # 第 6 項 = 8
print(fib(8)) # 第 8 項 = 21
```
Python 的 不像其它程式語言的 `switch` 只是 `if..else..` 的變形或替代品,它還能用來比對或拆解更複雜的資料結構,在適當的地方使用 `match` 可以讓程式碼看起來更直覺、更易懂。
---
## Vite 的 HMR 機制
- URL:https://kaochenlong.com/hmr-in-vite
- 發佈日期:2023-10-26
這是在 Vue.js Taiwan 社群小聚分享的主題「你是 Live Reload,我是 Hot Reload,我們不一樣!」,用一個簡單的 Vanilla JS 專案示範平常在用的 Live Server 所提供的頁面自動重新整理,跟 Vite 裡面的 HMR(Hot Module Replacement)有什麼不同,希望對大家有些幫助。
但我必須老實說,今天這個主題是我在今年 LaravelxVue Conf 研討會上講錯的內容,當時我以為我看到的是 HMR,但後來細追原始碼才發現原來不是這麼一回事。
想要跟著影片自己動手做看看的話,可以到我的 GitHub 取得檔案
Repo:https://github.com/kaochenlong/vite-hmr-demo
場地錄影畫質只有 1080p 所以可能有些程式碼不一定會那麼清楚 :)
想要了解更多關於 Vite 的原始碼或運作原理的話,可參考[Vite 原始碼解讀](https://kaochenlong.com/2023/10/16/vite-source-code.html)的介紹。
---
## Vite 原始碼解讀
- URL:https://kaochenlong.com/vite-source-code
- 發佈日期:2023-10-16
今年的 [iTHome 鐵人賽](https://ithelp.ithome.com.tw/users/20065770/ironman/6302)我挑選的主題之一,是應該沒辦法用來開課賺錢的「Vite 原始碼解讀」。
選擇這個題目的原因是今年八月份我在 [{LaravelxVue} Conf](https://laravelconf.tw/) 研討會上分享的主題「被 Vue 框架所耽誤的建置工具:Vite」,可惜只有短短的 40 分鐘的時間,沒辦法介紹太細節的內容。所以我決定在這次鐵人賽活動,30 天分期付款地慢慢跟大家一起研究 Vite 的原始碼,看看能不能發現什麼有趣的東西。
我大部分時間都是在 Ruby/Rails 的專案,但 Rails 每一代的前端打包策略都在不斷調整,從早期的 Assets Pipeline 到 Webpack,再到 Rails 7 的 Importmap。對於前端生態,每種全棧型的網站框架都有自己的做法,但似乎沒有真正完美的整合方式。直到我聽說了 Vite,最初我以為它只是 Vue 的附屬品,但後來發現它可以適用於各種 JavaScript 專案,甚至是隔壁棚的 React。這讓我對 Vite 產生了更大的興趣。
不同於傳統的打包工具,Vite 不需要在開發階段就打包所有資源,而是根據需要動態載入,難怪速度可以那麼快。Vite 也巧妙地將打包的工作分給了 [esbuild](https://esbuild.github.io/) 和 [Rollup](https://rollupjs.org/) 這兩個優秀的工具,Vite 自身則充當開發伺服器的角色做它該做的事。
影片從 `npm run dev` 指令發生什麼事開始看,接著是 middleware 的介紹,然後花了不少時間在介紹 Pre-Bundling,後續還有 Module Graph、Plugin 跟 HMR 的介紹,最後花了一點點時間動手作了個功能超弱的陽春復刻版的 Vite - [Vivvi](https://github.com/kaochenlong/vivvi)
影片播放清單
翻原始碼其實好處挺多的,除了能鍛鍊自己的程式能力外,還能看看前端的生態圈或是有哪些有趣的套件,重點是我在研究原始碼的過程看到一些不必要的程式碼,趁機賺了一次 [PR](https://github.com/vitejs/vite/pull/14557)。
一開始我還以為中間會因為工作或其它因素斷賽,沒想到竟然完賽了!這回一邊拍影片,一邊在修正我的 OBS 設定跟流程,所以過程中會發現我可能會換版型或換背景底圖,其實都是在做一些實驗。算了一下,這 30 天的影片總時數是 8 小時 22 分又 9 秒,我也順手把這 30 天的影片整理成[播放清單](https://www.youtube.com/playlist?list=PLBd8JGCAcUAEFJzjAQC6DbWTeDICcanFF),有需要的朋友請自行取用,喜歡我的影片的話也別忘了順手訂閱一下喔 :)
---
## 資深工程師身上的 N 項特質,N = 12
- URL:https://kaochenlong.com/from-junior-to-senior-developer
- 發佈日期:2023-10-04
我們身處在軟體工程師的大海、河川、小溪或水溝中,總是常聽到大家問一個古老的問題:「Junior 工程師要怎麼變成 Senior 工程師?」
老實說我沒辦法跟你說你怎麼做就馬上能讓你從 junior 工程師變成 senior 工程師,因為 senior 工程師不像超級賽亞人一樣可以說變就變的,還是有什麼開關或植入晶片就馬上有效果,如果有這種開關或晶片也請幫我安裝一打。
我沒辦法也不太喜歡給那些常在農場文上看到像是「要訓練獨立思考能力」、「要有解決問題的能力」之類的抽象建議,除非有附上實際的訓練及成果檢核方式,不然這樣的建議有說跟沒說可能差不多。不過,我倒是可以跟各位分享我從那些優秀工程師朋友們身上觀察到的 N 項神奇特質,N = 12。我不知道到底是這些特質讓他們變的優秀,還是因為他們優秀才有這些特質,但既然他們能站在那個位置,表示一定有值得學習、致敬的地方。
這篇文章的文字可能有點長,如果大家懶得看文字,也可以點開影片配雞排,不方便看用聽的也行。
另外,如果你對於學習程式或是轉職軟體工程師有興趣,可參考另一篇文章「[程式為什麼這麼難學?](/2023/10/04/how-to-be-a-developer.html)」。
## N 項特質
### 1. 技能熟練
首先第一個,也是最最最基本的,就是他們對自己專業領域的技能是熟練的,例如不管是前端、不管是後端、不管是資料庫或是 Operation。
我想大家以前小時候都在國文課本裡讀過一個故事,講說有一個賣油的老翁,他可以用非常高超的那個技巧,可以把油滴進那個非常小的銅幣裡面,他說其實沒什麼,就是熟練而已。一個優秀工程師,不管是打字速度或是他使用的工具,可以很熟練的去做到想做的事情,這很重要。不然在旁邊看他用一陽指在慢慢打字或是不熟練的慢慢操作開發工具,你大概怎麼樣也不會覺得他有多厲害。
### 2. 匠人精神
當然不是只有熟練,還要把事情做好,而且做到最好。有個我很喜歡的名詞叫「匠人精神」,我也很希望我自己可以做到這種程度。事情也不用多,就是把事情做到最好就好。我們可能會在日本的美食節目上會看到一些老師傅,他這輩子就只捏壽司,捏到年紀已經七八十歲身體還是很硬朗,然後你會發現門口排隊的客人總是一大串還不一定排的到。為什麼?這已經不是錢的問題了,因為大家就喜歡專業。我在那些優秀的工程師、大神朋友身上都有看到類似的特質。
不知道大家有沒有配合過厲害的設計師,他們有時候會跟你刁畫面上少一個 pixel、間距不對、按鈕位置不對,或是怎麼跟設計稿不一樣,他們並不是故意為難你,因為這就是他們的專業跟堅持,這很重要。
有些人會覺得,算了啦,隨便啦,這哪有關係,這個老闆也不管、客人不會在意這個啦...
這個「隨便啦」的說法,我比較少聽到優秀的工程師們在他們的專業上會這樣說,這也是要從 Junior 變成 Senior 的一個很重要的關鍵,要對一些事情開始認真,不要就是隨便的給放水流。
匠人精神是超棒工程師的神秘武器,對每個細節都超級認真,追求卓越。或許這些小細節無足輕重,但這些出色的工程師會不停地追求卓越,我認為這就是為什麼他們可以跟 junior 工程師拉開距離的最主要原因。
### 3. 跟風
我觀察到這些工程師朋友們他們還有另外一個特質,就是他們會跟風。我這個跟風不是負面的形容詞,我這裡跟風的意思,像最近的這半年來 AI 比較紅,他們會花時間去學這個新技術而且願意應用在他們手邊的專案上。
這是一個很重要的特質,有些人會覺得說「算了,那個是年輕人在學的東西,我們不要學」,或是「那個東西現在還不穩定,再看看」,但是我看到比較優秀的工程師朋友們會試著去跟這些新的技術,最後用不用是其次,但至少會願意去學習,不會排斥。
願意學習新技術,這也很重要。
### 4. !隨波逐流
雖然優秀工程師們會跟風、隨風而動,但不會盲目。學習歸學習,他們會審慎評估新技術的價值,看它是否適合自己的專案,然後才考慮應用。我不知道他們是怎麼訓練的,但他們都有自己的判斷力,不會被短期潮流牽著鼻子走。
### 5. 動手實作
除了主動學習之外,他們也不是只有出一張嘴巴,還會動手去做。他們的實作能力都很強,有一些想法或 idea,說不定一個晚上就把東西拼出來了,非常有行動力。
這也是我自己很喜歡的一個特質,就是實際動手做。Linus 也講過「show me the code」,你講那麼多幹嘛,就給我看你的程式碼,剩下的不用講。
跟 Nike 運動鞋的那個勾勾一樣,不要講那麼多,做就對了!
### 6. 不怕踩坑
實作的過程中一定會踩坑,但這些人他們也沒在怕,就踩下去,多踩多健康,反正踩坑踩久了就習慣了。在踩的過程中,不論是新技術還是舊技術,在踩坑的過程可能會發現「咦...我原本這樣做好像不太順,我是不是應該弄一些自動化的方法?」
然後他們就會想辦法去改善流程、增加 DX(Developer Experience),讓他們自己或其它同事在用這個工具或者流程的時候會有一種「哇\~」的舒暢感。
不斷的踩坑,然後因為覺得不舒服、覺得痛了,就改善它,這也是我看到這些人身上的特質。
### 7. 喜歡閱讀
我有一次在一本書上面看到一段話「在禮拜一到禮拜五讀書是為了維持不敗,禮拜六讀書是為了超越別人,禮拜天讀書是為了超越自己」。
閱讀是個很重要的習慣,對軟體工程師來說更是如此,不管是紙本書還是網路技術文章都一樣,閱讀是保持競爭力或是超車的關鍵。我在這些優秀的工程師朋友們身上都有看到這個特質,沒有例外。
### 8. 愛現、樂於分享
不只這樣,他們看完書或學到新技術後,他們還很愛現(稱讚意味),他們會在社群網站上面說我最近看了什麼書或分享閱讀心得。樂於分享、愛現這件事情,那些優秀的工程師朋友們幾乎每一個都有。
他們愛現,有些會寫書、寫文章,有些會拍影片,然後去投稿研討會,上台分享,讓更多人知道。因為其實我們都深信,在分享的過程中,不管是技術上還是非技術上,會得到的回報遠比想像的還要多很多,正是那句「施比受更有福」,有做過這件事的人就知道我在講什麼。
我非常相信,分享跟交流才會讓這個世界進步,越是藏招、越是不想跟別人分享,這件事情不健康也不會進步,對個人或對這個世界都是。
### 9. 培養後進
優秀的工程師不只懂技術,還知道怎麼培養新人。新人可能是你的同事、下屬,但目標都一樣:培養他們超越你。這或許聽起來怪怪的,但在技術圈,這樣的分享和交流是很正面的。而且在教導技術同時,你也會發現自己也在不斷成長,然後整個團隊也會進步。
當愛現、樂於分享、培養後進的這個風氣一但在公司或團隊做起來之後,通常就會吸引到更多優秀的人一起加入團隊,進入正向循環後便一發不可收拾,到時候你想擋都擋不住。
### 10. 會說故事
因為愛分享、培訓新人,所以通常這些人的說故事能力都蠻厲害的。說故事能力是指可以把複雜技術用白話的方式,講到連阿嫲都聽得懂。愛因斯坦曾經說過「如果說你沒有辦法用簡單的方式解釋它的話,就代表你對這個東西其實認知是不夠的」。
這個我非常認同,所以我在面試人的時候,我常會故意問一些看起來簡單但背後原理有點複雜的問題,例如在前端的面試題中,常會在 for 迴圈裡面跑個 `setTimeout`,故意印個 i 出來,然後問你這會印出什麼來?如果面試者有背過這個題目可能答的出來,但我通常會繼續再問說你覺得為什麼是這個結果,如果答案是背來的,很可能就答不出來或是亂講一通了。
### 11. 善於溝通
另外,這些優秀的工程師通常擁有很不錯的情緒管理及溝通技巧。這不僅包括同事、老闆、下屬溝通,還要能夠把複雜的技術以簡單明瞭並且有耐心的傳達給非技術人士。
### 12. 友善
最後一點,也是我覺得很重要的一點,就是這些人對自己可能很嚴格、很要求,但對其它人(特別對地位相對比較低的人)通常都蠻友善的,他們不會去臭別人的工具說「什麼?你還在寫 PHP 喔」或是「蛤?你在寫 Ruby,這年頭誰還在寫 Ruby」
有啊,我,我還在寫 Ruby!(舉手)
Be nice,友善這個特質很重要,不然就算技術再厲害也不容易受到其它人的敬重。我認識那些厲害的工程師朋友們大部份都很友善,樂於助人,嗯...但說不定那些不友善的人我也不會想去認識所以因此有取樣偏差也說不定 :)
## 教練,我想變強...
以下我列出一些可以練習、自我要求的項目,希望對各位在前進 senior 工程師的這條路會有幫助。但再次提醒,就算每項都做到了也不表示會成功,不過持續做下去,雖不中亦不遠矣。
### 1. 對技術的熟練度
第一點,不論是前端、後端還是全部端都一樣,就是要對自己專業領域的技術的熟練度一定要夠,這有點廢話,對技術不熟就什麼都不用講了。
### 2. 深度 > 廣度
需要學習的知識越來越廣,當面臨深度跟廣度的選擇的時候,如果可以的話,選擇深度大於廣度的效果會比較好。大家其實心裡都知道,樣樣通就樣樣鬆,但身體總是忍不住會分心或貪心都碰一些。
大部份的人沒有精神時光屋或是妙麗的時光器,每個人的時間跟精力都是有限的,盡量把有限的技能點數往深度點,長遠來說對往資深工程師這條路的幫助是比較大的。
### 3. 團隊 > 個人
我不會告訴你說你該學什麼語言或框架,這個不是我們這篇文章要討論的重點,但在做技術選型的時候,要先考慮的絕對不會是你個人自己爽,覺得這個好酷喔就用下去了,不是這樣子的。
你要考慮到現在目前的這個團隊裡面,他們對於這個技術的接受度以及能不能 handle 它,現在已經是團體戰,如果你要往更資深的方向前進的話,要考慮的不是你只有自己一個人而已。
### 4. 領導技能
領導技能指的不是帶人,大家可能聽過一個名詞叫一個職稱叫 Tech Lead,這個名字是 Tech 加一個 Lead,所以其實是帶領技術方向。例如說公司想要用某一個框架或架構,為什麼用它一定其原因在。
但要做到這點,要做到滿多功課:
- 花時間研究該技術的優、缺點、適用的地方
- 提出想法,說服長官或同事接受這個新技術
- 提出實際的作法,跟著大家一起做
### 5. 時間、專案管理
如果你只有一個人,也許你只要做好你自己個人時間管理就好,但如果在一個團隊裡的話,要做的事情就是這個專案的管理,不要覺得這是長官或 PM 的事,管好手邊每項工作的進度,這對你的長官、工作夥伴都是好事,對你要往資深工程師這條路更是重要的練習。
### 6. 輸出
你可能學了很多的課程、看了很多的書,不要只有看進去而已,你也可以試著「輸出」。輸出是指說你可以寫文章、拍影片或者是說在一些社群的小聚上面主動報名跟大家分享最近學到的東西。
相信我,學會「輸出」之後,你會學到更多。
### 7. 自我評估
要知道自己的價值在哪裡很重要,但要怎麼做?如果你不知道怎麼評估,有一個比較簡單比較務實也相對客觀的評估方式,就是面試。
對,就是找工作的那個面試,我並不是要你常常換工作,而是你可以把你的履歷開著,或是大概每一、兩年就去外面投個履歷,去找面試官聊聊天,試試看你現在的等級在什麼地方。
如果你感受不到你自己有沒有變厲害、感受不到自己的價值的話,就讓市場來決定你的價值,試一下市場的水溫你就會知道了。
## 不一定需要的...
以下列出兩點我個人覺得對於成為資深工程師這條路不一定有直接幫助的 `nice-to-have` 的項目。
### 1. 工作年資
有些人認為資深工程師就是一定要工作年資滿 5 年、10 年、20 年之類的,但我認為對資深工程師來說,年資是條件之一,但並不是必要條件。
有些公司可能會因為年紀到了或者是年資到了,在公司待了 8 年、10 年了,所以公司幫你加薪變成一個管理職,帶一些新人,職稱順便就變成資深工程師了。這個跟我心目中的資深工程師不太一樣
雖然這樣講可能有點傷人,但有些在業界工作號稱有 10 年經驗但事實上是只有 1 年經驗,然後這10 年在做一樣事情,希望我們都不是這樣的人。
雖說年資不是必要條件,但也不可能就是三個月就掛資深工程師,你又沒有精神時光屋。通常在業界做 2 \~ 3 年左右,只要不要太混,通常都會做出不錯的成績出來。
### 2. 擔任管理職
在台灣有些公司做久了就會被拉起來變管理職,但管程式、管技術跟管人是完全不一樣的技能。
我自己其實很不喜歡管理職,我覺得管人好複雜,管程式簡單多了。如果你擔任管理職可以讓整個團隊更進步是蠻好的,但以一個資深工程師來說,帶不帶人倒不一定是個一定要有的經歷。
更何況不少工程師被昇級成管理職就不寫程式了...嗯,這又是另一個故事了。
## 「你想要寫程式寫到幾歲?」
最後,這個問題我不知道大家有沒有想過,也許各位還很年輕,但這個問題我從 20 幾歲就被問到現在,現在已經 40 多歲了還在問,我的答案就是:我根本沒有想過這個問題。
為什麼要想這個問題?我喜歡寫程式、我喜歡教學、我喜歡做這件事情,我做這件事情是快樂的,為什麼我需要設定一個期限?如果是在做開心的事,不就是做到累、做到到自己不能做為止嗎?
所以對我來說,我大概會寫程式寫到不能寫為止吧 :)
成長是一條漫長、甜蜜但偶爾帶點痛苦的一條路,變強這件事不會在一夜之間發生。以上內容大家可以當做是在這條路上的幾項 checklist,最終能不能成功還是得取決於各位的實際行動和努力。
享受這段旅程,不要讓自己太疲憊,保持快樂,一起變成更優秀的工程師吧!
---
## 程式為什麼這麼難學?
- URL:https://kaochenlong.com/how-to-be-a-developer
- 發佈日期:2023-10-04
在[前篇文章](/2023/10/04/from-junior-to-senior-developer.html)我跟大家介紹了 senior 工程師的共通特質以及 junior 工程師的自我修練的建議,希望對大家有些幫助。
但如果你不是軟體工程師,可能不知道在哪裡看到招生廣告點進來的,或是剛好正在考慮想要轉職軟體工程師的話,有一些過去以及我們自己帶學生的經驗可供你參考。
同樣,如果懶的看文字,也可以看影片配雞排:
## 你為什麼想轉職軟體工程師?
如果你正在考慮轉職軟體工程師這件事,請你要認真的想想這個問題,為什麼?
有些朋友可能是目前的工作崗位的薪資天花板到頂了,或者在前幾年疫情比較嚴重的時候迫沒工作,這都是常見的轉職的原因。
我不知道大家想像中軟體工程師的生活是怎樣,會不會有人是覺得最近在刷抖音、刷 YouTube 影片都看到好像那些軟體工程師感覺一個過得比一個爽,看看那些矽谷工程師,收入又高,上班好像也都沒在做事...這是一定的啦,因為拍影片的人總不能把苦的一面拍出來給你看吧。
那會不會有這種年薪百萬、事少錢多離家近的工作?我相信一定有,只是以比例上來說相對比較少。以台灣來說比例會更少一些些,所以你一定、一定、一定(要講三遍)要想想看,你為什麼想要轉職軟體工程師。假設你已經想清楚了,想再了解更多細節,就再往下看吧
## 軟體工程師入門
軟體工程師種類很多,有做開發網站的,有做手機應用程式的,也有做硬體、韌體的,很多種類。如果你是資訊本科系應該沒有轉職的問題,所以我假設看到這裡的你是非本科系,如果想要進到這個產業但又不知道該選哪個角色的話,我的建議是**網站工程師**。
為什麼?首先,網站工程師的入門門檻相對比較低一點,並不是說這個比較簡單,我指的是入門門檻,要踏進去這一道門是相對比較簡單的。你不需要有什麼厲害的設備或實驗室或是多高的學歷,你只要有一台電腦,然後買幾本書或看幾個線上課程跟著做,你就有機會踏進這道門,說不定可以找到工作、拿到一張軟體工程師的入場券了,我是說真的。
但我還是必須很囉嗦的提醒大家,就是門檻低歸低,裡面水是很深的,不要以為說進來之後就是可以在這個行業就穩穩的吃了,不是這樣子的。
另外一個也比較推薦的原因是職缺相對比較多一些,然後薪資也還行,軟體工程師現在的薪資行情,以剛入門 junior 工程師來說,在台北來說低一點的月薪大概 35 \~ 38K 左右,狀況比較好的大概可以到 45 \~ 50K 之間。
學程式一直都不是一件很簡單的事情,不要去聽那些補習班跟你說只要學這個東西就可以找到工作。是啦,是找的到工作,但是如果要學的更好、想往更高的地方爬,如前一篇文章提到的,怎麼樣從 junior 變成 senior 工程師,過程其實很漫長的。
我先不提我們家的[培訓課程](https://5xcampus.com/camp/)怎樣怎樣,我想先跟大家聊聊為什麼學程式這麼難。
## 為什麼程式這麼難學?
有一張圖我想秀給大家一下:
出處:[為什麼學習寫程式這麼難?](https://www.thinkful.com/blog/why-learning-to-code-is-so-damn-hard/)
Y 軸 Confidence 是指信心指數,X 軸 Competence 是能力指數。
在一開始學習的時候都有所謂的蜜月期,非常開心。我就以網站工程師舉例,通常一開始我們可能會先學 HTML、學 CSS、學切版,因為你都做的出來,這時候應該會覺得「哇!這個蠻簡單的嘛」
但是開始進到 JavaScript 程式之後,不少同學就會「欸?怎麼好像難度突然變高很多?」
我們很多學生,包括我自己以前也是一樣,到了開始寫程式的時候的難度會爬坡一下,在這時期的信心指數就開始往下跌,雖然你感覺你有在學東西,你的能力指數也確實有在增加,可是你的信心指數卻持續下降,開始懷疑人生。
這邊有個名詞叫「絕望沙漠」,而且這段時間有點長,我看到很多新手都在這個階段就放棄了。為什麼?因為在這個階段會發現好像再怎麼學都沒有進步的感覺。或因為過程進步的幅度非常緩慢,結果連帶自信指數開始往下跌。這是新手學習程式的第一關大魔王關卡,真的很多人會在這關放棄。
但這並不一定是你的問題。為什麼?因為以現在的開發工具或框架為了方便讓你很快把東西做出來,會做了適度的抽象甚至過度簡化了,簡化這些流程容易讓學習者可以很快做出成果來,會有進步的幻覺,一開始的時候進步會覺得很 OK,但要開始客製化的時候就發現改不動了。
接下來你得補齊更多的基礎知識才有辦法再進一步,因為在補齊基礎建設的過程不容易感受到自己的進步(或是沒有人跟你說你有在進步),相對的沒成就感,明明在變強但卻變的沒自信了,這是「絕望沙漠」造成的原因。
所以知道這概念之後,只要能夠撐過這段沙漠期,應該就可以繼續往上爬,信心指教跟能力指數都會慢慢隨之增加。只要你不是天生神力、智商 200+,這應該都是個是新手學習程式必須要經歷過的階段,差別只在於這個沙漠期會多久了,這也是為什麼程式難學的原因。
這很正常,我自己也是曾經在沙漠裡懷疑人生,只要能夠撐過去就可以了。問題是要怎麼經過這個沙漠呢,待會我們有一些建議可以跟大家分享。
## 自學 vs 上課
如果要學程式的話,應該要自學還是去上課給老師帶?
其實都好,就看自己個人規劃。以我開培訓班的立場,我一定是跟你說來來來,報名連結在這裡,趕快來這裡上課。但以我個人立場的話,我會建議你可以先從自學開始,真的,因為我自己也是這樣過來的。
### 設定期限與目標
自學很重要的點,就是一定要設定期限跟目標。很多新手學習者沒先做這件事情,就覺得說反正我現在沒有什麼壓力,就慢慢學。沒有設定期限或目標只要遇到長一點的休假或是最近有個遊戲剛出來,學習就很容易中斷,而中斷通常就得再重來一次。
所以自學的時候,你一定要給自己設定一個期限跟目標,甚至可以設定嚴格一點,像是如果沒有做到就怎麼怎麼樣之類的罰則。
### 挑選程式語言
設定完目標之後,再來要選一款程式語言,這個聽起來像廢話,但是如果說去各大論壇問新手要學程式語言該學哪一種,你會得到各式各樣的答案。
有些人會告訴你現在 AI 盛行的時代你應該就要學 Python,有些人會告訴你現在做網站就應該要學 JavaScript,它現在可以前後端通吃,有些比較硬派的會強調你就是應該學 C 語言打好你的基礎。
這些建議每條單獨看都對,但適不適合你?我不知道,我這篇文章也沒有打算告訴你說你應該學什麼,這不是這篇文章的重點。你應該先決定自己想要做什麼東西,例如說你想要做 iPhone 的應用程式,那基本上沒有什麼選擇,就是只能選 Apple 他們家的程式語言;如果說你想要寫 Android 手機應用程式,選擇稍微多一點點了;如果想要做網站,那個選擇就更多了,PHP、Ruby、Python 這些都可以,所以你要先想想看你想要做是什麼,決定方向,程式語言的挑選就不難了。
### 看書、看手冊
選定技術之後,再來就是先翻一下官方網站,這很重要,如果可以的話先跟著官方手冊順一次大概流程。看一下基本的語法,跟著教學做。
你也可以去買一本書來看,如果說假設你覺得逛手冊太無聊,然後書也不知道該怎麼看,因為畢竟程式是需要敲鍵盤的,你可以跟著某些線上課程一起做。雖然說 YouTube 上面有很多教學,但是品質好壞差很多,你也不知道這些人是誰、評價如何。
如果你要挑選線上課程的話,有一個網站叫 [Udemy](https://www.udemy.com/) 是一個蠻好的開始,上面每一門課大概都是10 塊美金左右,然後就挑購買數夠多而且評價數也夠好的,基本上課程的內容都不會差到哪裡去。同時,我會建議先跟著一個講師或一門課程做就好,不要一次看好幾門課,也不要到處看文章、看影片,這年頭不是資訊不足,而是資訊是太多不知道該聽誰的,所以我會建議就是你就先找一門課,然後練完之後你可以再換另外一個講師或另外一門課。在學習過程中跟著同一門課的同一個脈絡,學習效果會好一些。
### 刻意練習
刻意練習的意思是指,假設你想要做的是一個購物車或電商網站,你就要往那個方向刻意去練習寫購物車,用你現在學到的東西去寫一個成品出來。做的很差、很卡關也沒關係,這很正常,就盡量刻意去做這些練習。練的過程你會發現越來越熟練,經常刻意練習比較難、比較複雜的功能,自己的腦袋就會慢慢習慣比較複雜的思考了。
### 練習輸出
學習的過程中,盡量就是有一些產出,也許是筆記,也許是文章。因為上課的時候資訊流是從電腦螢幕或從台上老師那邊流到我們腦袋裡面去,這些東西它會不會留在我們腦袋裡面?不知道。就算可以,它能留多久?只要學習過刻的不夠深,很快就會忘記。
對於把學習的內容刻的深一點,輸出是一件很有幫助的練習。我們自己帶學生的一個做法,我覺得蠻有效,我自己也是這樣訓練我自己,大家不妨試試看。就是我在出作業給同學們寫的時候,一般的作業繳交方式大概就是把檔案丟回來,但我會請同學們說把寫作業的過程錄下來,我想知道同學們是怎麼寫的,包括找答案的過程也是。不只這樣,我還請同學們一邊寫、一邊講解,你們就把我當學生,講解給我聽你為什麼這樣寫,為什麼要寫這一行、為什麼這邊要用這個變數。如果都能順暢的講講出來,代表你真的知道你在做什麼。這個練習很多業界現役的工程師都還不一定做的到,他們會寫,但不一定解釋的出來,因為那一段 code 可能是從網路上 copy 過來就貼在這裡剛好會動而已。
這是個很有效的訓練,大家可以自己練習看看,錄完之後可以再把影片重播一次看看,效果會更好,你可以看看你剛剛講了什麼,自己能不能聽的懂自己講的東西。
## 找老師、找教練
前面介紹的是自學,如果你今天想要去上課的話,同樣還是要多問「為什麼」,問自己為什麼需要去上課?是學不會,還是不確定學的正不正確,還是沒有人盯著就會放鬆?
知道原因之後再去找老師或培訓單位,看看他們的課程、師資,聽一下說明會適不適合自己。
其實這個概念跟為什麼去健身房要找教練一樣,自己練當然可以,但如果你不知道自己這樣練是不是有效,還是會不會受傷,一開始找個教練旁邊指導是很有幫助的,通常專業的教練會依據你的個人的身體狀況、體力等,給你最有效的建議,縮短你的學習時間。
學程式也一樣,如果旁邊有個老師帶著練,這個我想也不用我特別講,大家也知道效果通常會比自學來的好。但還是要提醒大家,先想想自己是不是需要再說,不要只聽信廣告。
## 常見問題
我們做軟體工程師轉職這些年下來,不少來聽說明會的朋友常會問到這些問題...
### Q: 不是本科系畢業可以嗎?
科學、學歷的問題很常被拿出來講,我的學歷可能只有高中畢業沒有很漂亮,有機會嗎?學歷很重要、科系很重要,但它不會決定你能不能當軟體工程師,我在業界看到很多那些所謂的前輩、大神們很多也都不是本科系畢業的。
軟體工程師並不像醫生、律師需要的所謂的國家考試資格,很多人就是真的去上一門課然後就說自己是工程師也有。如果你是資訊本科系,很好,之前在學校學的就剛好學以致用,如果不是其實也不用太擔心,因為科班生的四年所學的東西並沒有到追不上的程度。
### Q: AI 這麼厲害,還要學程式嗎?
這個也是最近我們常被問到的問題,AI 時代了,幹嘛學程式,現在都叫 AI 寫就好啦。是沒有錯啦,但你怎麼知道 AI 寫的程式對不對?如果你今天是個新手學習者,你根本沒能力判斷。
AI 可以是一個非常好的教練,你對它再兇它也不會跟你抱怨,但你也是得學會程式才能判斷它給的資訊到底是不是正確的。AI 很厲害,但以目前(2023 年 10 月)來說,倒還沒有完全真的威脅到軟體工程師的工作。
### Q: 每個人都適合寫程式嗎?
我必須認真的告訴各位,不是,真的不是,不是所有的人都適合寫程式的,我們不會因為想要你趕快來報名就跟你說只要有心每個人都可以當軟體工程師。
有些人是真的不適合,我指的不適合不是說特別笨,我指的是指對寫程式這件事提不起興趣,就是沒有感覺,不知道做這個可以幹嘛。
要怎麼樣知道自己適不適合,有比較簡單而且低成本的檢查方法,就是你可以在 YouTube 上跟一些有在教程式的人跟著做做看,或者是說花一點小錢去 Udemy 買一門課,然後跟著做做看。
每做完一個章節你就問自己,喜不喜歡這種感覺,喜不喜歡無中生有把東西做出來的感覺?如果真的覺得喜歡再說,不喜歡不要勉強,不要因為別人說什麼轉職軟體工程師的收入比較好、比較不會被疫情影響什麼的,你自己做的喜歡、做的開心最重要。
### Q: 大概要學多久?
如果自己學,平常白天有工作只能晚上學習的話大概半年左右可以學到一定的程度。可以的話盡量自己用學到的東西做點跟課程裡不一樣的,不要只會做課程教的東西,這樣效果有限。
如果是參加培訓單位的話,通常快的話三個月,慢的話大概也是半年到一年左右。如果說不知道說這期期間到底有沒有學會或學到什麼程度,我會建議你參加一些社群活動,認識一些工程師,跟他們聊聊,一來可以知道大概知道自己在什麼等級,二來也比較不會孤單。學習這條路一個人走是可以走的比較快,但一群人可以走比較遠。
我們每個禮拜二晚上都會有一個叫[默默會](https://www.facebook.com/rubymokumokukai/)的活動,沒有意外的話會每週二會固定舉辦,地點就在我們的辦公室,在台北市 228 公園旁(台北市衡陽路 7 號 5 樓),不用特別報名,人來就好。
你可以帶著你的問題來參加這個活動,或是來認識一些,或是就坐在裡面發呆也沒關係,不用有壓力,放心,我們不會在現場推銷你任何東西。
學習程式語言是一個挑戰,但也是一個令人滿足的過程。無論你選擇哪一種語言,關鍵是保持耐心,不斷學習和實踐。選擇適合自己的學習方式,你將能夠逐步建立起堅實的程式基礎,並將自己引向成為一名優秀的軟體工程師的道路。
有任何問題,都歡迎來我們每週辦的社群活動,或是我們的[粉絲專頁](https://www.facebook.com/5xruby)跟線上跟我們聊聊,先預祝學習順利 :)
---
## [為你自己學 Rust] 泛型(Generics)
- URL:https://kaochenlong.com/generics-in-rust
- 發佈日期:2023-10-01
從前面的「所有權(Ownership)」章節開始,接著生命週期(Lifetime)、特徵(Trait)、列舉(Enum),對平常只有撰寫前端或是只有寫 CRUD 的工程師來說(例如我就是),應該開始有一點學習上爬坡感了,這個章節會再繼續的增加一點坡度。
假設我定義了一個長方型的 Struct:
```rust
struct Rectangle {
width: u32,
height: u32,
}
```
裡面有 `width` 跟 `height` 這兩個欄位,都是 `u32` 型別。Rust 對型別的要求是很嚴格的,說好的 `u32` 就是只能放整數,給它小數的話馬上 Rust 編譯器就會跳出來抱怨了。
```rust
let rect_a = Rectangle{ width: 100, height: 50 }; // 沒問題
let rect_b = Rectangle{ width: 38.5, height: 19.5 }; // 不行!
```
假設我希望這個 `Rectangle` 的欄位也可以使用小數的話,你可能想說可以改成寫成這樣:
```rust
struct RectangleU32 {
width: u32,
height: u32,
}
struct RectangleF32 {
width: f32,
height: f32,
}
```
這樣就沒問題了:
```rust
let rect_a = RectangleI32{ width: 100, height: 50 };
let rect_b = RectangleF32{ width: 38.5, height: 19.5 };
```
但如果這樣寫,後續如果想要寫一個可以計算面積的函數,這兩個不同的 Struct 要怎麼傳進去當參數?難道也要寫兩份?
不同的程式語言在面臨這樣的情境,都有類似的設計,有些程式語言稱之「模板(Template)」,有些則稱之「泛型(Generics)」,就字面上的解釋來說泛型並不是特定指某一種型別,而是用來「代表」某個型別。如果還是覺得有點抽象,我們來看看程式碼:
```rust
struct Rectangle {
width: T,
height: T,
}
```
首先,在 Struct 名字的最後面的 `` 就是 Rust 裡泛型的標記寫法,表示在這個 Struct 裡會有一種 `T` 型別。
蛤?什麼是 `T` 型別?這你不用管,反正它就是一種型別,或說它就是個佔位置的 placeholder 也行,它可以是整數、浮點數、布林值或其它型別都可以,沒有限定。以上面的範例來說,唯一確定的就是 `width` 跟 `height` 這兩個欄位是「相同的 `T` 型別」。正是因為它可以廣「泛」的代表某種「型」別,才稱之「泛型」。
如果你喜歡的話,你可以把這個 `T` 換成任意的大寫開頭的字,像這樣:
```rust
struct Rectangle {
width: RRRRRR,
height: RRRRRR,
}
```
Rust 編譯器不會抱怨,但通常會看到大家使用 `T` 是因為它比較短而且可以用來代表「型別(Type)」,甚至在其它有支援泛型的程式語言也都會用 `T`。把原本固定寫死的型別抽換成 `T` 型別之後,使用起來就更有彈性了:
```rust
let rect_a = Rectangle{ width: 100, height: 50 };
let rect_b = Rectangle{ width: 38.5, height: 19.5 };
```
現在 `Rectangle` 的欄位可以用整數,也可以用小數了。這樣的泛型寫法可以讓原本 Struct 更有彈性了,不需要一樣的東西寫很多次。
有趣的是,雖然這裡並沒有指定特定型別,但當 Rust 在編譯過程中發現你要 `u32` 型別,那它就在背後幫你做一個 `u32` 的 Struct 出來,你帶 `f32` 給它,它就幫你做一個欄位是 `f32` 的 Struct 給你,這個行為有個專有名詞叫「單型化 (Monomorphization)」:
```rust
struct Rectangle_i32 {
width: i32,
height: i32,
}
struct Rectangle_f32 {
width: f32,
height: f32,
}
```
實際產生的名字應該更複雜,但這些都是在編譯時期 Rust 編譯器幫你做掉的,所以雖然在編譯的階段會多花一點點的時間,但執行的時候不會有額外的成本,效能不會因為這樣而有所折扣,也就是 Rust 官方手冊裡所提到「零成本抽象(Zero-Cost Abstractions)」的概念,你不用手動寫出這些資料結構的實際型別。
### 多個不同型別
承上,如果 `width` 跟 `height` 這兩個欄位我想要給它們不同的型別呢?沒問題,你可以這樣寫:
```rust
struct Rectangle {
width: T,
height: U,
}
```
那個 `U` 就是表示另一種型別。同樣的,你想用什麼名字都行,不過慣例上會使用 `U`,因為它正好是英文字母 `T` 的下一個字,如果還有更多型別參數,可以繼續往下使用 `V`、`W` 之類的字母。(就跟為什麼迴圈裡常會用 `i` 變數,然後迴圈裡的迴圈會用 `j` 變數一樣的道理)
泛型不是只能用在 Struct 上,Enum 跟 Trait 也都能用。現在各位再回想一下在上個章節介章提到的 `Option` 跟 `Result`:
```rust
pub enum Option {
None,
Some(T),
}
pub enum Result {
Ok(T),
Err(E),
}
```
它們本身也是用了泛型的寫法,另外我們在介紹生命週期的時候也出現過 `boss_cat<'a>` 這樣的寫法,其中的 `<>` 也是泛型。現在是不是比較能看的懂了呢?
### 參數也能泛型
不只如此,泛型還能用在函數上。我從前面的章節借個範例來用:
```rust
fn add_number(a: i32, b: i32) -> i32 {
a + b
}
```
因為這裡的 `i32` 都一樣,如果改用泛型的寫法可以變成這樣:
```rust
fn add_number(a: T, b: T) -> T {
a + b
}
```
這麼一來這個 `add_number()` 函數可以處理整數也能處理浮點數,好像不錯。但編譯的時候會出現錯誤訊息:
```
$ cargo run
error[E0369]: cannot add `T` to `T`
23 | a + b
| - ^ - T
| |
| T
```
Rust 告訴我們它沒辦法把 `T` 跟 `T` 加在一起。咦?我本來以為 Rust 編譯器這麼聰明,應該能夠猜的出來 `T` 是什麼型別,看來我們得跟它講的更清楚一點。但其實再仔細想想也合理,想想看,因為這個 `T` 可以是任意型別,如果我寫成 `add_number(true, false)` 硬是給它 2 個 `bool` 型別的值給它,你是要它們兩個怎麼相加?
所以我們要告訴 Rust,這個 `T` 型別不能是隨便阿貓阿狗型別都可以,得給它一些「限制(Constraints)」,在 Rust 裡我們可以給這個 `T` 型別某些特徵(Trait)。以上面的這個加法的範例來說,在 Rust 的標準函式庫裡有一個 `Add` 特徵可以借來用:
```rust
fn add_number>(a: T, b: T) -> T {
a + b
}
```
我知道這個語法寫起來有點複雜了,`std::ops::Add` 是指在 `std` 模組裡的 `ops` 模組裡有個 `Add` 特徵,把它掛在 `T` 後面表示這個 `T` 型別現在就有 `Add` 的特徵了,而且 Rust 內建就已經幫 `Add` 特徵實作了可以相加的功能。
如果覺得 `std::ops::Add` 寫起來有點囉嗦,可透過 `use` 關鍵字稍微來簡化它:
```rust
use std::ops::Add;
fn add_number>(a: T, b: T) -> T {
a + b
}
```
讓我們來追一點點 Rust 的原始碼,驗證一下我們剛剛介紹的這段內容。追進原始碼就會發現,`Add` 就真的只是一個 Trait:
```rust
pub trait Add {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
```
裡面需要實作 `add` 這個方法,這裡也可以看到這個 Trait 本身也有用到泛型,剛剛寫到的 `Output` 也可以在這裡看到它。再往下看一點會看到 Rust 幫我們寫好的巨集(Macro):
```rust
macro_rules! add_impl {
($($t:ty)*) => ($(
impl Add for $t {
type Output = $t;
fn add(self, other: $t) -> $t { self + other }
}
forward_ref_binop! { impl Add, add for $t, $t }
)*)
}
add_impl! { usize u8 u16 u32 u64 u128 isize i8 i16 i32 i64 i128 f32 f64 }
```
這裡不需要每個語法都看懂,但大概能猜的出來 `add_impl!` 這個巨集會幫所有的整數型別以及浮點數型別都實作 `Add` 這個特徵所需的功能。另外,從原始碼應該也看的出來在這個 `add()` 方法的參數是 `self` 並不是 `&self` 參照,所以執行了加法之後就會轉移所有權。
回到原本的程式,因為現在我們的 `add_number()` 函數已經加上限制了,所以這樣執行:
```rust
println!("{}", add_number(1, 2));
println!("{}", add_number(3.1, 9.8));
```
就都能正常運作了。如果這時候故意帶個 `bool` 型別給它呢?
```
$ cargo run
error[E0277]: cannot add `bool` to `bool`
|
7 | println!("{}", add_number(true, true));
| ^^^^^^^^^^ no implementation for `bool + bool`
```
因為 `bool` 型別並沒有實作 `Add` 特徵(只有數字們有),所以無法通過編譯。我們再試著讓它複雜一點,如果在函數裡不只做加法,還要做減法呢?寫起來可能就會變這樣:
```rust
fn calc + std::ops::Sub