與 Rust 勾心鬥角 · 點

garfileo發表於2022-04-05
上一篇:前言

三維世界的原點,就是在三個數軸上的投影為 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.00 不同。

現在可以寫一個能夠問候原點的程式了,

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 對我友好了許多。

相關文章