上一篇:前言
三維世界的原點,就是在三個數軸上的投影為 0 的點。假設三個數軸為 x,y 和 z,則原點在它們上的投影可表示為 x = 0, y = 0, z = 0,用 Rust 程式碼可表示為
let x: f64 = 0.0;
let y: f64 = 0.0;
let z: f64 = 0.0;
亦即定義了三個變數 x, y, z,它們皆為 64 位的浮點型別,值皆為浮點型別的字面量 0.0。注意,0.0
和 0
不同。
現在可以寫一個能夠問候原點的程式了,
fn main() {
let x: f64 = 0.0;
let y: f64 = 0.0;
let z: f64 = 0.0;
println!("你好啊,({0}, {1}, {2})!", x, y, z);
}
編譯,執行:
$ rustc foo.rs
$ ./foo
你好啊,(0, 0, 0)!
結構體
使用結構體型別可將 x, y, z 繫結到一起,構造抽象意義的三維原點,例如:
struct Point {
x: f64, y: f64, z: f64
}
fn main() {
let origin: Point = Point {x: 0.0, y: 0.0, z: 0.0};
println!("你好啊,({0}, {1}, {2})!", origin.x, origin.y, origin.z);
}
由於 rustc 能夠根據值的語法形式推斷出變數型別,因此
let origin: Point = Point {x: 0.0, y: 0.0, z: 0.0};
可簡化為
let origin = Point {x: 0.0, y: 0.0, z: 0.0};
方法
Rust 語言允許為結構體型別定義方法——有些特殊的函式,例如:
impl Point {
fn origin() -> Point {
Point {x: 0.0, y: 0.0, z: 0.0}
}
}
使用上述定義的方法,可進一步簡化構造原點的語句:
let origin = Point::origin();
origin
方法是通過型別 Point
呼叫,這類方法稱為「靜態方法(Static Method)」。也可以為結構體型別的例項定義方法,這類方法稱為「例項方法(Instance Method)」,例如:
impl Point {
fn hello(&self) {
println!("你好啊,我是 ({0}, {1}, {2}!", self.x, self.y, self.z);
}
}
以下程式碼構造一個點的例項,並呼叫例項方法 hello
:
let x = Point::origin();
x.hello();
n 維點
若點的維度任意,用 C 語言,可將其定義為
typedef struct {
size_t n;
double *body;
} Point;
body
指向堆空間裡大小為 n * sizeof(double)
的一段空間。用 Rust 語言如何定義類似的結構體?可使用 Box<T>
指標,例如
struct Point {
n: isize,
body: Box<[f64]>
}
在 Rust 語言裡,Box<T>
是泛型的智慧指標。在上例中,T
即 [f64]
,即成員型別為 f64
的陣列型別。
為 C 語言版本的 Point
型別構建一個例項:
size_t n = 3;
double *body = malloc(n * sizeof(double));
body[0] = 0.1; body[1] = 0.2; body[2] = 0.3;
Point x = (Point){.n = n, .body = body};
printf("你好啊,%zu 維點 (%f, %f, %f)!\n", x.n, x.body[0], x.body[1], x.body[2]);
free(body);
類似地,上述 Rust 語言版本的 Point
的例項化過程為
let x = Point {n: 3, body: Box::new([0.1, 0.2, 0.3])};
println!("你好啊,{0} 維點 ({1}, {2}, {3})!", x.n, x.body[0], x.body[1], x.body[2]);
在上述示例裡,Rust 語言要比 C 語言簡約得多。另外,無論是 C 語言還是 Rust 語言,示例中的 x.body
所指向的記憶體空間位於程式的堆空間,但是前者需要顯式回收,而後者可自動回收,故 Box<T>
稱為「智慧指標」。
陣列和向量
與早期的 C 語言類似,Rust 語言裡的陣列是固定長度的型別,亦即在定義陣列例項時需要指定陣列的長度,例如
let x: [f64; 3] = [0.1, 0.2, 0.3];
C 語言自 C99 標準開始支援變長陣列。不過,由於陣列空間位於棧上,對於大量的資料而言,陣列是否變長並不重要,因為通常需要在堆空間構造陣列。堆空間的記憶體可以由程式設計者自行分配和維護,因此實現變長陣列僅僅是演算法問題,而不是語法問題。
需要注意的是,在 Rust 語言裡,若在堆空間為陣列分配空間,所用的智慧指標 Box<T>
的泛型引數 T
的值雖然作為陣列型別,但是不需要提供陣列長度,只需提供陣列元素的型別,例如 [f64]
。
Rust 語言提供了堆空間變長陣列的實現,即向量(Vec
)型別,可直接基於該型別定義 n 維點,例如
let mut x: Vec<f64> = Vec::new();
x.push(0.1); x.push(0.2); x.push(0.3);
println!("你好啊,{0} 維點 ({1}, {2}, {3})!", x.len(), x[0], x[1], x[2]);
用 Vec::new
構造的向量例項,若向其中增加元素,則需要將向量例項設定為可變,即 mut
。
使用 vec!
可將上述程式碼簡化為
let x: Vec<f64> = vec![0.1, 0.2, 0.3];
println!("你好啊,{0} 維點 ({1}, {2}, {3})!", x.len(), x[0], x[1], x[2]);
vec!
可在堆空間構造一個向量空間,然後將棧空間裡的陣列的資料複製到向量空間,若後續不需要修改向量的內容或向其中增加新元素,不需要將向量例項設為可變。
上一節基於 Box<[f64]>
定義的 n 維點,實際上相當於低配版本的 Vec<f64>
,如無必要,通常應該使用後者,而且可以使用型別別名的形式,例如
type Point = Vec<f64>;
特性
下面是一個完整的程式,它能夠讓一個 n 維點自報家門:
type Point = Vec<f64>;
fn hello(x: &Point) {
let n = x.len();
print!("你好啊,我是 {} 維點 (", x.len());
for i in 0 .. n {
if i != (n - 1) {
print!("{}, ", x[i]);
} else {
print!("{}", x[i]);
}
}
println!(")!");
}
fn main() {
let x: Point = vec![0.1, 0.2, 0.3];
hello(&x);
}
程式的輸出結果為
你好啊,我是 3 維點 (0.1, 0.2, 0.3)!
能否將 hello
作為 Point
亦即 Vec<f64>
例項的方法呢?例如
impl Point {
fn hello(&self) {
let n = self.len();
print!("你好啊,我是 {} 維點 (", self.len());
for i in 0 .. n {
if i != (n - 1) {
print!("{}, ", self[i]);
} else {
print!("{}", self[i]);
}
}
println!(")!");
}
}
rustc 說不行。錯誤資訊為
error[E0116]: cannot define inherent `impl` for a type outside of the crate
where the type is defined
然後給出修改建議:
define and implement a trait or new type instead
下面定義一個 Hello
特性(Trait)試試,
trait Hello {
fn hello(&self);
}
然後為 Point
亦即 Vec<f64>
型別實現 Hello
特性,
impl Hello for Point {
fn hello(&self) {
let n = self.len();
print!("你好啊,我是 {} 維點 (", self.len());
for i in 0 .. n {
if i != (n - 1) {
print!("{}, ", self[i]);
} else {
print!("{}", self[i]);
}
}
println!(")!");
}
}
然後使用 Hello
特性裡的 hello
函式,在行為上與型別例項的方法完全一致,例如
fn main() {
let x: Point = vec![0.1, 0.2, 0.3];
x.hello();
}
雖然特性與方法有相似之處,但是表達的語義不同。方法面向特定型別,而特性面向不同的型別。不同的型別,可以有相似的行為。人要吃東西,睡覺,生孩子。動物不是人,也要吃東西,睡覺,生孩子。道路上,可以走人,也可以行車。為不同的型別構造相似的行為,這就是特性存在的意義。
小結
幾乎將一年前學過的一點 Rust 語言複習了一遍,只有 Box<T>
指標是初學,最大的感觸是,rustc 對我友好了許多。