Home
Explore Uedu
Student Console
Register as Member/Login
(2) In future presentations of the research findings, in addition to the course project website and public presentations, your real name and personal information will not appear in this research report. If you are interested in the research results, we can provide you with an executive summary after the study is completed.
問卷中心
Teacher Console
Course Setup
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

0

Local AI

--

CISOSE26 本地 AI UG26
政治大學 AQI 18 29°C

AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

變數與資料型別

變數與資料型別

從「0.1 + 0.2 不等於 0.3」出發,看懂變數、型別系統與型別安全的全貌

為什麼 0.1 + 0.2 不等於 0.3

打開任何一個程式語言的互動式介面,輸入 0.1 + 0.2,你很可能會看到一個令人困惑的答案:0.30000000000000004。這不是電腦壞了,也不是語言設計失誤,而是「資料型別」這個看似平凡的概念,正在悄悄影響著每一行程式的行為。當我們在程式裡寫下一個名字、給它一個值,背後其實牽動著記憶體配置、型別判定、運算規則與安全保證等一整套機制。理解變數與型別,等於拿到了理解所有程式語言的第一把鑰匙。

程式設計概念示意圖

變數:給記憶體中的一塊空間取個名字

電腦的記憶體(memory)可以想像成一條長長的儲物櫃,每一格都有編號(也就是記憶體位址(address))。程式要記住一個數值、一段文字或一個結果,就得佔用其中某些格子。但是要程式設計師記住「我的成績存在第 0x7ffe3a 格」實在太不人道了,於是有了變數(variable)——它是一個我們取的名字,背後對應到某塊記憶體空間。

score = 95

這一行做了三件事:在記憶體中準備一塊空間、把整數 95 放進去、讓名字 score 指向這塊空間。之後我們只要寫 score,語言就會幫我們去那個位址把值取回來。這就是「具名儲存」的核心價值:用人類友善的名字,取代冷冰冰的位址。

不同語言對「變數到底是什麼」有不同的觀點。在 C 這類語言裡,變數比較像「一個盒子」,score 直接就是那塊裝著 95 的記憶體。在 Python 這類語言裡,變數更像「一張貼紙(label)」,score 是貼在某個物件上的標籤,物件本身存活在別處。這個差異會在後面討論可變性與賦值時浮現出來。

資料型別:值的「種類」決定了能對它做什麼

如果記憶體只是一堆 0 與 1,那麼同樣一段位元,要怎麼知道它代表整數 65、字元 'A'、還是某個顏色?答案是資料型別(data type)。型別替一段位元賦予意義,同時規定了「可以對它做哪些運算」。

常見的基本型別(primitive type)包括:

型別 說明 範例
整數(integer) 沒有小數的數 42-7
浮點數(float) 帶小數的數,以 IEEE 754 表示 3.140.1
布林(boolean) 真或假 TrueFalse
字元/字串(string) 文字 'A'"hello"

型別不只是分類標籤,它決定了運算的語意。5 + 3 是數值相加得到 8;但 "5" + "3" 在許多語言裡是字串串接,得到 "53"。同一個 + 符號,遇到不同型別就有不同行為——這正是為什麼型別系統如此重要。

回到開頭那個 0.1 + 0.2 的謎題。浮點數採用 IEEE 754 標準,用有限的二進位位元去近似一個十進位小數。問題是 0.1 在二進位裡是無限循環小數(就像 $1/3$ 在十進位裡是 $0.333\ldots$),電腦只能存一個非常接近但不完全等於 0.1 的值。兩個近似值相加,誤差就浮現了。這提醒我們:浮點數是近似值,不該用 == 直接比較,而要檢查兩數差距是否小於某個極小容忍值 $\varepsilon$。

靜態型別 vs 動態型別:什麼時候檢查型別?

程式語言對型別的「檢查時機」分成兩大陣營。

靜態型別(static typing)在程式編譯或執行前就確定每個變數的型別,例如 C、Java、Go、Rust。型別錯誤在編譯階段就會被攔下來:

int score = 95;
score = "hello";   // 編譯錯誤:不能把字串塞進整數變數

動態型別(dynamic typing)則在執行(runtime)時才檢查型別,例如 Python、JavaScript、Ruby。同一個變數可以先裝整數、後裝字串:

score = 95        # 此刻 score 是整數
score = "hello"   # 完全合法,現在 score 是字串

兩者各有取捨。靜態型別能在程式上線前就抓出許多錯誤,IDE 也能據此提供精準的自動補全與重構,代價是程式碼較囉嗦、開發節奏較慢。動態型別寫起來輕快靈活、適合快速原型,代價是某些型別錯誤要等到程式跑到那一行才爆炸。值得注意的是,「靜態/動態」說的是檢查時機,和接下來要談的「強/弱」是兩個獨立的維度。

強型別 vs 弱型別:型別規則有多嚴格?

強型別(strong typing)弱型別(weak typing)講的是:語言願意幫你做多少「悄悄的型別轉換」。

弱型別語言會在你混用型別時自作主張地轉換。JavaScript 是著名例子:

"5" + 3      // 得到 "53"(數字被轉成字串)
"5" - 3      // 得到 2  (字串被轉成數字)
true + 1     // 得到 2  (true 被當成 1)

同一批值,遇到 +- 竟有截然不同的結果,這種「隱式轉換(implicit coercion)」常是 bug 的溫床。

強型別語言則拒絕這種曖昧。Python 雖然是動態型別,卻是強型別:

"5" + 3   # TypeError:不能把字串和整數相加

它寧可直接報錯,逼你明確表態到底要數值相加還是字串串接。所以我們可以把四種組合畫成一個 2×2 的座標:Python 是「動態 + 強」,C 是「靜態 + 弱(允許不少隱式轉換)」,Java 是「靜態 + 偏強」,JavaScript 是「動態 + 弱」。這兩個維度交織出每個語言獨特的「型別個性」。

運算子與優先序:先乘除後加減的程式版本

當運算式裡有多個運算子(operator)時,誰先算?這由優先序(precedence)決定,規則和數學課的「先乘除後加減」一脈相承,但範圍更廣。

result = 2 + 3 * 4      # 等於 14,不是 20,因為 * 先算
result = (2 + 3) * 4    # 用括號改變順序,等於 20

除了優先序,還有結合性(associativity)處理「同優先序時從哪邊開始算」。多數二元運算子是左結合,例如 8 - 3 - 2 解讀為 (8 - 3) - 2 = 3;但指數運算通常右結合,2 ** 3 ** 22 ** (3 ** 2) = 512。常見優先序由高到低大致是:括號 → 指數 → 乘除取餘 → 加減 → 比較 → 邏輯運算。當你不確定時,最好的策略不是去背完整的優先序表,而是主動加括號讓意圖清晰——對讀程式的人也更友善。

動手看一個例子

讓我們追蹤一段混合了型別與運算子的程式,一步步看型別如何影響結果。

a = 7
b = 2
print(a / b)    # 3.5
print(a // b)   # 3
print(a % b)    # 1
print(a > b and b > 0)  # True

逐步拆解:

  1. a / b:在 Python 3 中,/ 是「真除法」,即使兩個運算元都是整數,結果也會是浮點數 3.5。注意這裡發生了型別提升:整數運算的結果變成了浮點型別。
  2. a // b// 是「整數除法」,捨去小數部分得到 3
  3. a % b% 是取餘數,$7 = 2 \times 3 + 1$,所以餘 1
  4. a > b and b > 0:先算兩個比較運算(TrueTrue),因為比較的優先序高於 and,最後邏輯 and 得到 True

光是除法就有 /// 兩種,且回傳型別不同——這正凸顯了「運算子的行為深受型別與語言設計左右」。同樣一行 7 / 2,在某些舊語言或 C 的整數運算裡會直接得到 3,差異足以造成難以察覺的 bug。

型別轉換:把一種型別變成另一種

實務上我們常需要在型別之間轉換,分成兩類。

隱式轉換(implicit / coercion)由語言自動完成,例如整數參與浮點運算時自動升級為浮點數(前述 7 / 2 即是)。方便,但若發生在意料之外就是 bug 來源。

顯式轉換(explicit / casting)由程式設計師明確要求:

age_text = "25"          # 字串
age = int(age_text)      # 顯式轉成整數 25
age_next = age + 1       # 26,合法

price = float("19.9")    # 字串轉浮點 19.9
label = str(100)         # 整數轉字串 "100"

從使用者輸入、檔案或網路讀進來的資料通常是字串,要做數值運算前一定得先轉型。轉型也可能失敗:int("hello") 會丟出錯誤,所以穩健的程式會搭配例外處理。另外要警覺窄化轉換(narrowing)的資訊遺失:把浮點數 3.9 轉成整數會變 3(直接捨去小數而非四捨五入),這種隱藏的精度損失若沒注意,會在帳務或計量場景釀成問題。

重點回顧

  • 變數是記憶體空間的具名標籤;C 視之為「盒子」,Python 視之為「貼在物件上的標籤」,這個心智模型差異會影響賦值與可變性的理解。
  • 型別替位元賦予意義並規定可用運算;同一個 + 遇到數字是相加、遇到字串是串接。浮點數是 IEEE 754 近似值,不可用 == 直接比較。
  • 靜態/動態講的是「何時檢查型別」,強/弱講的是「容不容許隱式轉換」,兩者正交,組合出語言的型別個性(如 Python 是動態強型別)。
  • 運算子優先序與結合性決定運算順序;不確定時主動加括號勝過硬背規則。
  • 轉型分隱式與顯式;外部輸入多為字串需顯式轉型,並提防窄化轉換造成的精度遺失與轉型失敗例外。

深入探討(研究所視角)

從工程概念往上走,型別系統其實是一個有深厚理論根基的領域。

型別系統作為輕量級形式驗證。 在程式語言理論中,型別系統可視為一套證明系統。著名的型別安全定理「Well-typed programs cannot go wrong」(Milner, 1978)由兩個性質撐起:Progress(型別正確的程式不會卡在無意義的中間狀態)與 Preservation(求值一步後型別不變)。兩者合起來即 Soundness(健全性)——只要通過型別檢查,就保證執行期不會出現「把整數當函式呼叫」這類型別錯誤。這呼應 Curry–Howard 同構:型別即命題,程式即證明,編譯通過等於完成了一個機械化的證明。型別安全因此是一道實打實的安全防線,能在編譯期排除一整類記憶體與行為錯誤——這也是 Rust 透過所有權(ownership)型別系統,在沒有垃圾回收的前提下達成記憶體安全的理論依據。

型別推論:要安全也要不囉嗦。 靜態型別最常被詬病的是要寫一堆型別標註。型別推論(type inference)讓編譯器自己推導出型別,兼顧安全與簡潔。其經典演算法是 Hindley–Milner(Algorithm W),運作核心是統一(unification):把運算式中的未知型別當成變數,依使用方式建立一組約束方程,再解聯立。例如看到 f x = x + 1,從 +1 推出 x 必為數值,於是 f 的型別被推定為 數值 -> 數值,全程不需人工標註。Algorithm W 在多數情況下接近線性,理論最差複雜度雖達指數,但實際少見。今日 Haskell、ML、Rust、TypeScript、乃至 C++ 的 auto、Java 的 var,都是這套思想的後代。

undefined:型別系統的破口與設計教材。 JavaScript 的 undefined 是觀察型別安全如何被打破的絕佳案例。它代表「這個位置從未被賦值」——存取未宣告變數、讀取物件不存在的屬性、函式沒回傳值,結果都是 undefined。問題在於 undefined 會悄悄參與運算而不立刻報錯:undefined + 1 得到 NaN(Not a Number),錯誤被掩蓋並向後傳播,直到很遠的地方才以難以追溯的形式爆發。再加上它和 null 兩個「空值」並存,造就了無數 bug。Tony Hoare 把 null 的發明稱為「十億美元的錯誤」,正是這類問題的縮影。現代語言的回應是把「可能為空」這件事搬進型別系統強制處理:Haskell 的 Maybe、Rust 的 Option<T>、Kotlin 的可空型別 T?、TypeScript 的 strictNullChecks 與聯合型別 T | undefined,都逼迫程式設計師在編譯期就明確處理「沒有值」的分支,把執行期的隱性崩潰提前轉化為編譯期的顯性檢查。這正是型別推論與型別安全在真實語言演化中的交會點:一個好的型別系統,不只是替值分類,更是用最低的標註成本,把整類錯誤擋在程式上線之前。

AI 共讀助教正在陪你讀:變數與資料型別
嗨!我是這篇文章的共讀助教,只根據〈變數與資料型別〉的內容回答。可以問我「解釋某段」「舉個例子」「出題考我」,或反白文中段落後點下方「解釋選取段落」。