最近用 GPUI 框架實作了一個圖形化計算機,過程中學到不少 Rust 的實用技巧。這篇文章整理了從這個專案中可以學到的 Rust 程式設計概念。
專案概述
這個計算機支援基本四則運算、指數運算 (^)、取餘數 (%)、變數賦值,以及內建常數 pi 和 e。UI 使用 Zed 編輯器團隊開發的 GPUI 框架,這是一個 GPU 加速的 Rust UI 框架。
1. 使用 Enum 定義 Token
Rust 的 enum 非常適合用來定義詞法分析器 (lexer) 的 token 類型:
#[derive(Debug, Clone, PartialEq)]
pub enum Token {
Name(String), // 變數名稱
Number(f64), // 數字
Plus, // +
Minus, // -
Mul, // *
Div, // /
Mod, // %
Pow, // ^
Print, // ;
Assign, // =
LP, // (
RP, // )
}這種設計的好處:
- 類型安全:編譯器確保你處理所有可能的 token 類型
- 資料攜帶:
Name(String)和Number(f64)可以攜帶額外資料 - 模式匹配:配合
match表達式,程式碼清晰易讀
2. 實作 Iterator Trait
將 lexer 實作為 Iterator,讓程式碼更符合 Rust 慣例:
pub struct TokenStream {
input: Vec<char>,
offset: usize,
}
impl Iterator for TokenStream {
type Item = Token;
fn next(&mut self) -> Option<Self::Item> {
loop {
if self.offset >= self.input.len() {
return None;
}
let ch = self.input[self.offset];
self.offset += 1;
match ch {
'+' => return Some(Token::Plus),
'-' => return Some(Token::Minus),
'*' => return Some(Token::Mul),
// ... 其他 token
x if x.is_whitespace() => continue,
_ => return None,
}
}
}
}實作 Iterator trait 的好處:
- 可以使用
.collect()收集所有 token - 可以搭配其他 iterator 方法如
.map()、.filter()等 - 延遲求值 (lazy evaluation),不會一次解析完整個輸入
3. 遞迴下降解析器
計算機的核心是一個遞迴下降解析器 (recursive descent parser),這是編譯器課程中的經典技術:
impl Calculator {
// 加減法 (最低優先級)
fn expr(&mut self, token: Option<Token>) -> Result<f64, String> {
let mut left = self.term(token)?;
loop {
match self.current_token {
Some(Token::Plus) => left += self.term(None)?,
Some(Token::Minus) => left -= self.term(None)?,
_ => return Ok(left),
}
}
}
// 乘除法、取餘數 (中等優先級)
fn term(&mut self, token: Option<Token>) -> Result<f64, String> {
let mut left = self.power(token)?;
loop {
match self.current_token {
Some(Token::Mul) => left *= self.power(None)?,
Some(Token::Div) => left /= self.power(None)?,
Some(Token::Mod) => left %= self.power(None)?,
_ => return Ok(left),
}
}
}
// 指數運算 (高優先級,右結合)
fn power(&mut self, token: Option<Token>) -> Result<f64, String> {
let base = self.prim(token)?;
if let Some(Token::Pow) = self.current_token {
let exp = self.power(None)?; // 遞迴實現右結合
Ok(base.powf(exp))
} else {
Ok(base)
}
}
// 基本元素 (最高優先級)
fn prim(&mut self, token: Option<Token>) -> Result<f64, String> {
// 處理數字、變數、括號、一元負號
}
}這個設計的關鍵:
- 運算子優先級:透過函數呼叫順序自然實現(
expr→term→power→prim) - 結合性:左結合用迴圈,右結合用遞迴
- 錯誤處理:使用
Result<f64, String>和?運算子傳遞錯誤
4. GPUI 的 Render Trait
GPUI 使用 trait 來定義 UI 元件的渲染邏輯:
impl Render for CalculatorApp {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.flex()
.flex_col()
.size_full()
.bg(rgb(0x1e1e1e))
.p_4()
.gap_3()
.child(self.render_display())
.child(self.render_keypad(cx))
}
}GPUI 的特色:
- 宣告式 UI:類似 SwiftUI 或 Flutter 的風格
- Tailwind 風格的樣式:
.flex()、.p_4()、.gap_3()等方法 - GPU 加速:比 Electron 少 60-80% 記憶體使用量
5. 事件處理與閉包
按鈕點擊事件使用閉包 (closure) 和 cx.listener() 來處理:
div()
.id(SharedString::from(format!("btn_{}", label)))
.on_click(cx.listener(move |this, _event, _window, cx| {
match label_owned.as_str() {
"C" => this.clear(),
"⌫" => this.backspace(),
"=" => this.evaluate(),
"±" => this.toggle_sign(),
"π" => this.append("pi"),
other => this.append(other),
}
cx.notify(); // 通知 UI 更新
}))這裡展示了 Rust 閉包的幾個重點:
move關鍵字將變數所有權移入閉包- 閉包可以捕獲外部變數(
label_owned) cx.notify()觸發 UI 重新渲染
6. 模組化設計
專案採用清晰的模組結構:
src/
├── main.rs # 進入點 + UI 元件
└── calculator/
├── mod.rs # 模組宣告
├── token.rs # Token 定義
├── lexer.rs # 詞法分析器
└── parser.rs # 語法分析器 + 測試
這種結構的好處:
- 關注點分離:UI 和計算邏輯分開
- 可測試性:parser 可以獨立測試
- 可重用性:calculator 模組可以用於其他專案
7. 單元測試
Rust 內建強大的測試框架,測試直接寫在同一個檔案:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_exponentiation() {
let mut calc = Calculator::new();
assert_eq!(calc.evaluate("2^3").unwrap(), 8.0);
assert_eq!(calc.evaluate("2^3^2").unwrap(), 512.0); // 右結合
assert_eq!(calc.evaluate("2 + 3^2").unwrap(), 11.0); // 優先級
}
#[test]
fn test_modulo() {
let mut calc = Calculator::new();
assert_eq!(calc.evaluate("10 % 3").unwrap(), 1.0);
assert_eq!(calc.evaluate("17 % 5").unwrap(), 2.0);
}
}Rust 測試的特點:
#[cfg(test)]確保測試碼不會編譯進正式版本#[test]標記測試函數- 執行
cargo test即可運行所有測試
8. 錯誤處理
使用 Result 類型和 ? 運算子進行錯誤處理:
pub fn evaluate(&mut self, input: &str) -> Result<f64, String> {
self.tokens = TokenStream::new(input).collect();
if self.tokens.is_empty() {
return Err("empty expression".to_owned());
}
let first_token = self.next_token();
self.expr(first_token)
}
// 使用 ? 運算子傳遞錯誤
fn expr(&mut self, token: Option<Token>) -> Result<f64, String> {
let mut left = self.term(token)?; // 如果 term 失敗,錯誤會被傳遞
// ...
}這種模式的好處:
- 明確標示可能失敗的操作
- 錯誤類型清楚(這裡用
String,正式專案建議用自定義錯誤類型) ?運算子讓錯誤處理程式碼簡潔
總結
這個計算機專案雖然簡單,但涵蓋了多個重要的 Rust 概念:
| 概念 | 應用場景 |
|---|---|
| Enum + Pattern Matching | Token 定義與解析 |
| Iterator Trait | 詞法分析器 |
| Recursive Descent | 語法分析器 |
| Trait (Render) | UI 元件定義 |
| Closure | 事件處理 |
| Module System | 程式碼組織 |
| Unit Testing | 驗證解析邏輯 |
| Result + ? | 錯誤處理 |
如果你想深入學習 Rust,動手實作一個計算機是很好的練習。從簡單的四則運算開始,逐步加入變數、函數、甚至自定義語法,你會學到很多編譯器和語言設計的知識。
參考資源
- gpui-calculator 原始碼 - 本文範例的完整程式碼
- GPUI 官方網站
- gpui-component - GPUI 的 UI 元件庫
- The C++ Programming Language, 4th edition - 計算機範例的原始出處
- Crafting Interpreters - 編譯器入門好書