曼德博集合是一種複雜且精美的分形圖案,本文將逐步講解如何使用 Rust 程式語言實作其計算與視覺化。首先,我們會利用複數結構體和相關運算方法模擬複數的迭代計算過程,接著將計算結果對映至畫素,最後使用 ASCII 字元將影像輸出至終端。過程中,我們會使用 Rust 的泛型函式和特徵繫結來提高程式碼的彈性和可重用性,同時也會探討 Rust 的字串處理方法,例如如何將字串分割成行並進行逐行處理。最後,我們將提供完整的程式碼範例,讓讀者可以輕鬆地理解和執行程式,並進一步探索曼德博集合的奧妙。
示例程式碼
以下是計算曼德博集合的一個簡單示例:
import numpy as np
def mandelbrot(c, max_iter):
z = c
for n in range(max_iter):
if abs(z) > 2:
return n
z = z*z + c
return max_iter
def draw_mandelbrot(xmin,xmax,ymin,ymax,width,height,max_iter):
r1 = np.linspace(xmin, xmax, width)
r2 = np.linspace(ymin, ymax, height)
return (r1,r2,np.array([[mandelbrot(complex(r, i),max_iter) for r in r1] for i in r2]))
def draw_image(xmin,xmax,ymin,ymax,width,height,max_iter):
d = draw_mandelbrot(xmin,xmax,ymin,ymax,width,height,max_iter)
import matplotlib.pyplot as plt
plt.imshow(d[2], extent=(xmin, xmax, ymin, ymax))
plt.show()
draw_image(-2.0,1.0,-1.5,1.5,1000,1000,256)
內容解密:
mandelbrot
函式計算給定複數c
是否屬於曼德博集合,並傳回其迭代次數。draw_mandelbrot
函式生成曼德博集合的畫素資料。draw_image
函式使用 Matplotlib 將曼德博集合繪製為影像。
圖表翻譯:
以下是使用 Mermaid 語法繪製的曼德博集合計算流程圖:
flowchart TD A[開始] --> B[初始化複數] B --> C[計算曼德博集合] C --> D[判斷是否逃逸] D -->|是| E[傳回迭代次數] D -->|否| C E --> F[繪製影像] F --> G[顯示影像]
這個流程圖展示了計算曼德博集合的步驟,從初始化複數開始,到計算曼德博集合、判斷是否逃逸,最終繪製和顯示影像。
瞭解複數和曼德博集合
曼德博集合是一個著名的數學集合,定義為所有在迭代函式 $f(z) = z^2 + c$ 下不發散的複數 $c$。這裡,我們將探討如何使用 Rust 程式語言來繪製曼德博集合。
複數的初始化和運算
複數可以用其實部和虛部的座標來表示。給定兩個座標,$x$ 和 $y$,我們可以初始化一個複數 $z = x + yi$,其中 $i$ 是虛數單位,滿足 $i^2 = -1$。
在 Rust 中,我們可以定義一個結構體 Complex
來代表複數,並實作相關的運算方法。
struct Complex {
real: f64,
imag: f64,
}
impl Complex {
fn new(real: f64, imag: f64) -> Self {
Complex { real, imag }
}
fn magnitude(&self) -> f64 {
(self.real.powi(2) + self.imag.powi(2)).sqrt()
}
}
曼德博集合的渲染
曼德博集合的渲染涉及到對每個複數 $c$ 進行迭代,計算其是否屬於曼德博集合。這個過程可以用 Rust 的迴圈和條件陳述式來實作。
fn render_mandelbrot(width: usize, height: usize, max_iters: usize) -> Vec<Vec<usize>> {
let mut escape_vals = vec![vec![0; width]; height];
for y in 0..height {
for x in 0..width {
let cx = map(x, 0, width, -2.5, 1.5);
let cy = map(y, 0, height, -1.5, 1.5);
let mut z = Complex::new(0.0, 0.0);
let mut c = Complex::new(cx, cy);
let mut i = 0;
while i < max_iters && z.magnitude() <= 2.0 {
z = Complex::new(z.real * z.real - z.imag * z.imag + c.real, 2.0 * z.real * z.imag + c.imag);
i += 1;
}
escape_vals[y][x] = i;
}
}
escape_vals
}
fn map(value: usize, in_min: usize, in_max: usize, out_min: f64, out_max: f64) -> f64 {
let scale = (out_max - out_min) / (in_max as f64 - in_min as f64);
(value as f64 - in_min as f64) * scale + out_min
}
顏色對映和輸出
最後,根據曼德博集合的逃逸次數,我們可以對應不同的顏色進行對映和輸出。
fn print_mandelbrot(escape_vals: Vec<Vec<usize>>) {
for row in escape_vals {
let mut line = String::new();
for column in row {
let val = match column {
0..=2 => ' ',
2..=5 => '.',
5..=10 => '•',
_ => '*',
};
line.push(val);
}
println!("{}", line);
}
}
執行和結果
執行上述程式碼,可以得到曼德博集合的渲染結果。這個結果以文字形式呈現,每個字元代表了曼德博集合中的一個點的逃逸次數。
fn main() {
let escape_vals = render_mandelbrot(80, 24, 100);
print_mandelbrot(escape_vals);
}
這個簡單的實作展示瞭如何使用 Rust 程式語言來渲染曼德博集合。透過對複數的迭代和計算,可以得到曼德博集合的影像,並以文字形式呈現。
Mandelbrot 集合計算與視覺化
Mandelbrot 集合是一個著名的分形,具有自相似性和無限複雜的結構。以下是使用 Rust 程式語言計算和視覺化 Mandelbrot 集合的範例。
Mandelbrot 集合計算
Mandelbrot 集合的計算公式為:z = z^2 + c
,其中 z
和 c
是複數。集合中的每個點都對應一個複數 c
,如果該點的序列 z
不會發散到無窮大,則該點屬於 Mandelbrot 集合。
fn calculate_mandelbrot(width: u32, height: u32, xmin: f64, xmax: f64, ymin: f64, ymax: f64) -> Vec<Vec<u8>> {
let mut pixels = vec![vec![0; width as usize]; height as usize];
for y in 0..height {
for x in 0..width {
let cx = xmin + (x as f64 / width as f64) * (xmax - xmin);
let cy = ymin + (y as f64 / height as f64) * (ymax - ymin);
let mut zx = 0.0;
let mut zy = 0.0;
let mut i = 0;
while zx * zx + zy * zy < 4.0 && i < 255 {
let temp = zx * zx - zy * zy + cx;
zy = 2.0 * zx * zy + cy;
zx = temp;
i += 1;
}
pixels[y as usize][x as usize] = if i == 255 { 0 } else { i as u8 };
}
}
pixels
}
Mandelbrot 集合視覺化
計算出 Mandelbrot 集合的畫素後,可以使用 ASCII 字元來視覺化。以下是使用 ASCII 字元來繪製 Mandelbrot 集合的範例。
fn print_mandelbrot(pixels: Vec<Vec<u8>>) {
for row in pixels {
for pixel in row {
print!("{}", match pixel {
0..=30 => '*',
31..=100 => '+',
101..=200 => 'x',
201..=400 => '$',
401..=700 => '#',
_ => '%',
});
}
println!();
}
}
主函式
最後,可以在主函式中呼叫 calculate_mandelbrot
和 print_mandelbrot
函式來計算和視覺化 Mandelbrot 集合。
fn main() {
let mandelbrot = calculate_mandelbrot(80, 24, -2.0, 1.0, -1.5, 1.5);
print_mandelbrot(mandelbrot);
}
圖表翻譯:
以下是使用 Mermaid 語法繪製的 Mandelbrot 集合流程圖。
graph LR A[開始] -->|計算 Mandelbrot 集合|> B[計算畫素] B -->|視覺化|> C[輸出 ASCII 字元] C -->|繪製圖形|> D[完成]
這個流程圖展示了計算和視覺化 Mandelbrot 集合的步驟。首先,計算 Mandelbrot 集合的畫素,然後視覺化這些畫素,最後輸出 ASCII 字元並繪製圖形。
進階函式定義
Rust 的函式可以比之前的 add(i: i32, j: i32) -> i32
更加複雜。為了幫助讀者更好地理解 Rust 的原始碼,以下幾節將提供一些額外的內容。
2.8.1 顯式生命週期註解
首先,讓我們介紹一些比較複雜的語法。當你閱讀 Rust 的原始碼時,你可能會遇到一些難以理解的定義,因為它們看起來像古代文明的象形文字。以下是 listing 2.13 中的一個例子,展示了一個函式簽名中顯式生命週期註解的使用。
fn add_with_lifetimes<'a, 'b>(i: &'a i32, j: &'b i32) -> i32 {
*i + *j
}
這種語法一開始可能很難理解,但隨著時間的推移,你會越來越熟悉它。讓我們分解一下這個函式簽名的各個部分:
fn add_with_lifetimes(...)
是函式定義的開始,-> i32
表示該函式傳回一個i32
值。<'a, 'b>
宣告了兩個生命週期變數'a
和'b
,它們的作用域是add_with_lifetimes
函式。i: &'a i32
將生命週期變數'a
繫結到引數i
的生命週期,表示i
是一個參照,指向一個具有生命週期'a
的i32
值。j: &'b i32
將生命週期變數'b
繫結到引數j
的生命週期,表示j
是一個參照,指向一個具有生命週期'b
的i32
值。
繫結生命週期變數到值的意義可能不是很明顯。Rust 的安全性檢查根據一個生命週期系統,該系統驗證所有存取資料的嘗試都是有效的。生命週期註解允許程式設計師宣告他們的意圖。所有繫結到給定生命週期的值必須至少存活到最後一次存取任何繫結到該生命週期的值。
通常,生命週期系統會自動工作。雖然每個引數都有一個生命週期,但這些檢查通常是不可見的,因為編譯器可以推斷出大多數生命週期。然而,在某些情況下,編譯器需要幫助,例如函式接受多個參照作為引數或傳回一個參照。在這些情況下,編譯器會透過錯誤訊息要求生命週期註解的協助。
當呼叫函式時,不需要生命週期註解。以下面的例子為例,你可以看到生命週期註解在函式定義(第 1 行)中,但在呼叫時(第 8 行)則沒有。
fn add_with_lifetimes<'a, 'b>(i: &'a i32, j: &'b i32) -> i32 {
*i + *j
}
fn main() {
//...
}
這個例子的原始碼位於 ch2-add-with-lifetimes.rs
中。
內容解密:
上述程式碼定義了一個名為 add_with_lifetimes
的函式,它接受兩個參照作為引數,分別是 i
和 j
,並傳回一個 i32
值。這個函式使用了顯式生命週期註解來指定引數的生命週期。透過這種方式,Rust 可以確保程式碼的安全性和正確性。
圖表翻譯:
flowchart TD A[開始] --> B[定義 add_with_lifetimes 函式] B --> C[指定引數 i 和 j 的生命週期] C --> D[傳回 i32 值] D --> E[結束]
這個流程圖展示了 add_with_lifetimes
函式的執行流程,從定義函式開始,指定引數的生命週期,然後傳回結果,最終結束。
泛型函式與生命週期引數
在 Rust 中,當我們需要處理多種不同型別的輸入時,泛型函式是一個非常有用的工具。泛型函式允許我們定義一個函式,可以適用於多種型別的資料。
生命週期引數
在使用參考(reference)時,Rust 需要知道參考的生命週期,以確保記憶體安全。生命週期引數是一種方法,讓程式設計師可以指定參考的生命週期。
以下是一個範例:
fn add_with_lifetimes<'a, 'b>(i: &'a i32, j: &'b i32) -> i32 {
*i + *j
}
在這個範例中,'a
和 'b
是生命週期引數,它們指定了 i
和 j
參考的生命週期。
泛型函式
泛型函式是一種函式,可以適用於多種型別的資料。以下是一個範例:
fn add<T>(i: T, j: T) -> T {
i + j
}
在這個範例中,T
是一個泛型型別引數,它可以代表任何型別的資料。
但是,Rust 編譯器會抱怨說不能將兩個 T
型別的值相加。這是因為 T
可以是任何型別的資料,而不是所有型別的資料都可以相加。
解決方案
要解決這個問題,我們需要指定 T
型別必須實作 Add
特徵(trait)。以下是一個範例:
use std::ops::Add;
fn add<T: Add<Output = T>>(i: T, j: T) -> T {
i + j
}
在這個範例中,T: Add<Output = T>
指定了 T
型別必須實作 Add
特徵,並且 Add
特徵的輸出型別必須是 T
。
圖表翻譯:
graph LR A[泛型函式] -->|實作|> B[Add 特徵] B -->|輸出|> C[T 型別] C -->|相加|> D[結果]
在這個圖表中,泛型函式 add
實作了 Add
特徵,並且 Add
特徵的輸出型別是 T
型別。然後,T
型別的值可以相加,得到結果。
泛型函式與特徵繫結
在 Rust 中,泛型函式可以定義為能夠處理多種型別的函式。然而,在定義這樣的函式時,我們需要確保這些型別支援特定的操作,例如加法。在本文中,我們將探討如何使用特徵繫結(trait bound)來限制泛型型別,從而實作加法操作。
問題:無法進行加法
當我們嘗試定義一個泛型函式 add
來對兩個相同型別的變數進行加法時,編譯器會報錯。這是因為 Rust 的泛型型別 T
可以是任何型別,而不是所有型別都支援加法操作。
fn add<T>(i: T, j: T) -> T {
i + j
}
解決方案:特徵繫結
為瞭解決這個問題,我們需要使用特徵繫結(trait bound)來限制泛型型別 T
。我們可以指定 T
必須實作 std::ops::Add
特徵,這樣編譯器就知道 T
支援加法操作。
fn add<T: std::ops::Add<Output = T>>(i: T, j: T) -> T {
i + j
}
在這個例子中,<T: std::ops::Add<Output = T>>
是特徵繫結,它指定 T
必須實作 std::ops::Add
特徵,並且加法操作的結果必須也是 T
型別。
特徵(Trait)
在 Rust 中,特徵(trait)是一種語言特性,類別似於其他語言中的介面(interface)或協定(protocol)。它允許型別宣告自己支援某些行為或操作。所有 Rust 的運算子,包括加法,都被定義在特徵中。例如,加法運算子 +
被定義在 std::ops::Add
特徵中。
示例
以下是完整的示例程式碼,展示瞭如何使用泛型函式和特徵繫結來實作加法操作:
fn add<T: std::ops::Add<Output = T>>(i: T, j: T) -> T {
i + j
}
fn main() {
let result = add(2.5, 3.7);
println!("Result: {}", result);
}
在這個示例中,add
函式被定義為一個泛型函式,它接受兩個相同型別的變數並傳回加法結果。main
函式展示瞭如何呼叫 add
函式並列印結果。
透過使用特徵繫結,我們可以確保泛型函式 add
只能被呼叫於支援加法操作的型別上,這樣就避免了編譯時期的錯誤。
泛型函式與特徵界限
在 Rust 中,泛型函式可以定義多個型別的引數和傳回值。然而,當涉及運算子時,情況會變得更加複雜。並非所有型別都支援加法運算,例如 Duration
型別就不支援直接加法運算。
特徵界限
為瞭解決這個問題,Rust 引入了特徵界限(trait bounds)的概念。特徵界限允許我們指定泛型型別必須實作的特徵。例如,若要對兩個值進行加法運算,我們需要確保它們的型別實作了 Add
特徵。
示例:加法函式
以下是一個簡單的加法函式,示範瞭如何使用特徵界限:
use std::ops::Add;
fn add<T: Add<Output = T>>(a: T, b: T) -> T {
a + b
}
在這個例子中,add
函式定義了兩個泛型引數 a
和 b
,它們的型別為 T
。T
必須實作 Add
特徵,並且其輸出型別必須也是 T
。這樣,我們就可以確保對 a
和 b
進行加法運算是有效的。
實際應用
現在,讓我們來看看如何使用這個 add
函式:
fn main() {
let floats = add(1.2, 3.4);
let ints = add(10, 20);
// let durations = add(Duration::from_secs(1), Duration::from_secs(2)); // 錯誤:Duration 型別不支援加法運算
}
在這個例子中,我們成功地使用 add
函式對浮點數和整數進行了加法運算。然而,當我們嘗試對 Duration
型別的值進行加法運算時,編譯器會報錯,因為 Duration
型別不支援加法運算。
圖表翻譯
以下是使用 Mermaid 圖表展示 add
函式的流程:
flowchart TD A[開始] --> B[定義泛型函式 add] B --> C[指定特徵界限 Add] C --> D[實作加法運算] D --> E[傳回結果] E --> F[結束]
圖表解釋
這個圖表展示了 add
函式的流程:
- 定義泛型函式
add
。 - 指定特徵界限
Add
,確保泛型型別支援加法運算。 - 實作加法運算。
- 傳回結果。
- 結束函式。
這個圖表幫助我們理解 add
函式的流程和特徵界限的作用。
Rust 基礎知識:泛型函式和文書處理
2.9 建立簡單的 grep 工具
在上一節中,我們討論了 Rust 中的數字。現在,讓我們來建立一個簡單的 grep 工具,學習如何在 Rust 中處理文字。
首先,讓我們看一下清單 2.18 中的程式碼。這個程式碼定義了一個簡單的 grep 工具,稱為 grep-lite
。它的目的是在一段文字中搜尋一個特定的詞彙。
fn main() {
let search_term = "picture";
let text = "dark square is a picture feverishly turned--in search of what?";
println!("{}", text);
}
這個程式碼定義了一個 main
函式,該函式包含兩個變數:search_term
和 text
。search_term
是我們要搜尋的詞彙,而 text
是我們要搜尋的文字。
2.10 使用泛型函式
在 Rust 中,泛型函式可以用於定義可以處理不同型別的資料的函式。讓我們看一下清單 2.17 中的程式碼。
use std::ops::Add;
use std::time::Duration;
fn generic_function<T: Add>(a: T, b: T) -> T {
a + b
}
fn main() {
let floats = generic_function(1.0, 2.0);
let ints = generic_function(1, 2);
let durations = generic_function(Duration::new(5, 0), Duration::new(10, 0));
println!("{}", floats);
println!("{}", ints);
println!("{:?}", durations);
}
這個程式碼定義了一個泛型函式 generic_function
,該函式可以接受兩個相同型別的引數,並傳回一個相同型別的結果。該函式使用了 Add
特徵界限,確保只有實作了 Add
特徵的型別才能被傳遞給該函式。
讀取 Rust 程式碼的原則
在讀取 Rust 程式碼時,以下幾個原則可以幫助您更好地理解程式碼:
- 小寫字母(如
i
、j
)表示變數。 - 單個大寫字母(如
T
)表示泛型型別變數。 - 以大寫字母開頭的詞彙(如
Add
)表示特徵或具體型別,如String
或Duration
。 - 以撇號開頭的標籤(如
'a
)表示壽命引數。
透過這些原則,您可以更好地理解 Rust 程式碼,並學習如何建立自己的 Rust 程式。
使用 Rust 進行字串處理和查詢
在 Rust 中,字串處理和查詢是一個常見的任務。以下是一個簡單的範例,展示如何使用 Rust 進行字串處理和查詢。
範例:查詢特定字串
fn main() {
let quote = "
Every face, every shop, bedroom window, public-house, and
dark square is a picture feverishly turned--in search of what?
It is the same with books.
What do we seek through millions of pages?
";
for line in quote.lines() {
println!("{}", line);
}
}
在這個範例中,我們定義了一個 quote
變數,包含了一段文字。然後,我們使用 lines()
方法將這段文字分割成多行,並使用 for
迴圈印出每一行。
使用 add()
函式進行運算
Rust 的 add()
函式可以用於進行加法運算。以下是使用 add()
函式進行浮點數、整數和時間間隔運算的範例:
use std::ops::Add;
fn main() {
// 浮點數加法
let a = 1.0;
let b = 2.0;
let result = a + b;
println!("{} + {} = {}", a, b, result);
// 整數加法
let c = 1;
let d = 2;
let result = c + d;
println!("{} + {} = {}", c, d, result);
// 時間間隔加法
use std::time::Duration;
let e = Duration::from_secs(1);
let f = Duration::from_secs(2);
let result = e + f;
println!("{} + {} = {:?}", e, f, result);
}
在這個範例中,我們使用 add()
函式進行浮點數、整數和時間間隔的加法運算。注意,時間間隔的加法運算需要使用 Duration
型別。
Rust 的字串型別和功能
Rust 的字串型別可以分為兩大類別:String
和 &str
。String
是一個可增長的字串型別,而 &str
是一個不可變的字串切片型別。
從底層實作到高階應用的全面檢視顯示,Rust 的字串處理能力兼具效能與安全性。分析 Rust 的 String
和 &str
兩種型別,可以發現它們分別適用於不同的場景:String
負責可變字串,&str
則處理不可變的字串切片。這種設計有效避免了常見的字串操作錯誤,例如緩衝區溢位。同時,Rust 的所有權系統和借用機制,確保了記憶體安全,避免了懸空指標等問題。儘管 Rust 的字串處理語法相較於其他語言略顯複雜,但其提供的安全性與效能優勢在系統程式設計中至關重要。對於追求高效能且注重記憶體安全的開發者而言,Rust 的字串處理機制值得深入學習和應用。玄貓認為,Rust 的嚴謹設計將在系統級程式開發領域扮演越來越重要的角色。