與 Rust 勾心鬥角 · 解析 OFF 檔案

garfileo發表於2022-04-15

OFF 檔案是純文字檔案,可用於記錄三維(或更高維)多面體資訊,其基本格式為

OFF
點數 面數 邊數
點表
面表

例如

OFF
4 4 6
0 0 0
1 0 0
0 1 0
0 0 1
3 0 1 2
3 0 1 3
3 1 2 3
3 0 2 3

記錄的是四個頂點定義的四面體,其邊數為 6。點表記錄了四個頂點的座標資訊

0 0 0
1 0 0
0 1 0
0 0 1

面表記錄了四個面片的資訊

3 0 1 2
3 0 1 3
3 1 2 3
3 0 2 3

面表的第一列數字表示每個面片由多少個頂點構成,後繼各列為點表索引號,例如

3 0 1 3

表示面片由 3 個頂點構成,這些頂點在點表中的索引號分別是 0, 1 和 3,分別對應點表中第一個點,第 2 個點和第 4 個點。

注意,OFF 檔案頭部分的邊數可以設為 0,並不影響多面體結構的正確性。

Mesh 結構體

在考慮如何解析 OFF 檔案之前,需要先確定記憶體中能夠用於表達多面體結構的資料型別,不然解析工作難以落到實處,該資料型別可定義為結構體型別:

struct Mesh {
    points: Vec<Vec<f64>>,  // 點表
    facets: Vec<Vec<usize>> // 面表
}

其中用於表示點和麵片的向量(Vec)例項位於堆上,因此通常不必擔心 Mesh 例項造成棧溢位的情況。原本我想將 Mesh 定義為

struct Point {
    n: usize, // 維度
    body: Box<[f64]> // 座標
}

struct Facet {
    n: usize, // 頂點個數
    vertices: Box<[usize]> // 頂點索引
}

struct Mesh {
    points: Vec<Point>, // 點表
    facets: Vec<Facet>  // 面表
}

上述定義邏輯上更符合多面體模型,但是使用 Box 指標無法為 PointFacet 動態分配堆空間。實際上該問題已在「點」的那一節的「n 維點」部分出現過了。

以下程式碼為 Mesh 定義了 newdisplay 方法:

impl Mesh {
    fn new() -> Mesh {
        return Mesh {points: Vec::new(), facets: Vec::new()};
    }
    
    fn display(&self) {
        println!("OFF");
        println!("{0} {1} 0", self.points.len(), self.facets.len());
        for x in &(self.points) {
            let n = x.len();
            for i in 0 .. n {
                if i == n - 1 {
                    println!("{}", x[i]);
                } else {
                    print!("{} ", x[i]);
                }
            }
        }
        for f in &(self.facets) {
            let n = f.len();
            print!("{} ", n);
            for i in 0 .. n {
                if i == n - 1 {
                    println!("{}", f[i]);
                } else {
                    print!("{} ", f[i]);
                }
            }
        }
    }
}

其中,&(self.points) 表示對 Mesh 結構體的 points 成員的引用,也可寫為 &self.points,因為 . 運算子的優先順序比 & 高。不過,此處為何需要引用呢?

集合型別的引用

對於向量,在使用 for ... in 語法對其進行遍歷時,幕後需將向量例項作為引數傳給迭代器。有三種傳參形式:

  • 傳向量例項;
  • 傳向量例項的引用;
  • 傳向量例項的可變引用。

由於上述程式碼中的 pointsMesh 結構體的成員,它的所有權倘若發生轉移會導致 Mesh 失去這個成員,rustc 不允許結構體的定義被如此隨意地破壞,故而報錯,而使用傳引用的方式則是允許的。

還有一個問題,在遍歷向量的過程中,有訪問向量元素的程式碼:

for x in &(self.points) {
    let n = x.len();
    for i in 0 .. n {
        if i == n - 1 {
            println!("{}", x[i]);
        } else {
            print!("{} ", x[i]);
        }
    }
}

其中 xVec<f64> 例項還是它的引用?答案是後者。事實上,遍歷向量的過程中,獲取向量元素的方式也相應分為三種:

  • 傳向量例項時,獲取的是向量元素的所有權;
  • 傳向量例項的引用,獲取的是向量元素的引用;
  • 傳向量例項的可變引用,獲取的是向量元素的可變引用。

泛型

迭代點集和麵片集的過程幾乎一樣,亦即

for x in &(self.points) {
    let n = x.len();
    for i in 0 .. n {
        if i == n - 1 {
            println!("{}", x[i]);
        } else {
            print!("{} ", x[i]);
        }
    }
}
for f in &(self.facets) {
    let n = f.len();
    print!("{} ", n);
    for i in 0 .. n {
        if i == n - 1 {
            println!("{}", f[i]);
        } else {
            print!("{} ", f[i]);
        }
    }
}

倘若假設面表的輸出過程裡沒有 `

print!("{} ", n);

便可將點表和的輸出過程統一為一個泛型函式:

fn display_matrix<T>(v: &Vec<T>) {
    for x in v {
        let n = x.len();
        for i in 0 .. n {
            if i == n - 1 {
                println!("{}", x[i]);
            } else {
                print!("{} ", x[i]);
            }
        }
    }
}

之所以為這個函式名取名為 display_matrix,是因為元素為向量的向量,不就是矩陣嗎?

然後,可將 Meshdisplay 方法定義為

impl Mesh {
    fn display(&self) {
        println!("OFF");
        println!("{0} {1} 0", self.points.len(), self.facets.len());
        display_matrix(&self.points);
        display_matrix(&self.facets);
    }
}

結果會遭到 rustc 的毒打:

error[E0599]: no method named `len` found for reference `&T` in the current scope
error[E0608]: cannot index into a value of type `&T`
error[E0608]: cannot index into a value of type `&T`

第一個錯誤是,rustc 認為 display_matrix 的泛型引數 T 沒有 len 方法。後兩個錯誤是 rustc 認為 T 不能用下標獲得值。換言之,rustc 希望我能夠「證明」T 既有 len 方法,也能用通過下標獲得值。rustc 是蠢的,它不知道在我的例子裡,TVec 型別既有 len 方法也能通過下標獲得值。

證明各種向量都有長度

證明的途徑是基於特性。例如,下面是一份完整的程式程式碼,它能夠為一個泛型函式證明 Tlen 方法:

trait HasLength {
    fn len(&self) -> usize;
}

impl HasLength for Vec<usize> {
    fn len(&self) -> usize {
        return self.len();
    }
}

fn display_vec_len<T: HasLength>(v: &T) {
    println!("{}", v.len());
}

fn main() {
    let mut a: Vec<usize> = Vec::new();
    a.push(1);
    a.push(2);
    display_vec_len(&a);
}

上述程式碼,首先定義了一個名為 HasLength 的 Trait,然後為 Vec<usize> 型別實現了該 Trait。在泛型函式 display_vec_len 中將泛型引數 T 限定為實現了 HasLength Trait 的型別。

如果將 display_vec_len 函式應用於 Vec<f64> 型別呢?需要為 Vec<f64> 也實現 HasLength Trait。於是一個新的問題出現了,Trait 能不能也泛型化呢?這樣便可無需為每種具體的 Vec<T> 型別定義 HasLength Trait 了。試試看:

trait HasLength {
    fn len(&self) -> usize;
}

impl<T> HasLength for Vec<T> {
    fn len(&self) -> usize {
        return self.len();
    }
}

fn display_vec_len<T: HasLength>(v: &T) {
    println!("{}", v.len());
}

fn main() {
    let mut a: Vec<usize> = Vec::new();
    a.push(1);
    a.push(2);
    display_vec_len(&a);
    
    let mut b: Vec<f64> = Vec::new();
    b.push(0.1);
    b.push(0.2);
    b.push(0.3);
    display_vec_len(&b);
}

結果符合預期。

下標運算

通過下標訪問陣列和向量的元素是 Rust 的語法糖,而該語法糖是基於標準庫定義的 std::ops::Index Trait 實現的。例如

fn main() {
    let a = vec![1, 2, 3, 4];
    println!("{}", a[2]);
}

等同於

use std::ops::Index;
fn main() {
    let a = vec![1,2,3,4];
    println!("{}", a.index(2));
}

對於以下泛型函式

fn matrix_index<T>(v: &Vec<T>, i: usize, j: usize) -> 不知該返回什麼型別 {
    return &v[i][j];
}

若其呼叫程式碼為

let a = vec![vec![1, 2], vec![3, 4]];
println!("{}", matrix_index(&a, 0, 1));

這裡有兩個問題。首先如何向 rustc 證明 matrix_index 的泛型引數 T 支援下標運算呢?其次 matrix_index 的返回型別是什麼?答案是

fn matrix_index<T: Index<usize>>(v: &Vec<T>, i: usize, j: usize) -> &impl Display
    where <T as Index<usize>>::Output: Sized,
          <T as Index<usize>>::Output: Display {
    return &v[i][j];
}

若想知道這個答案從何而來,需要在錯誤中逐步嘗試。

首先,嘗試證明 T 實現了 std::ops::Index Trait:

use std::ops::Index;

fn matrix_index<T: Index>(v: &Vec<T>, i: usize, j: usize) -> 不知該返回什麼型別 {
    return &v[i][j];
}

但是 rust 會報錯,並給出整改建議:

rror[E0107]: missing generics for trait `Index`
add missing generic argument
  |
  | fn matrix_index<T: Index<Idx>>(v: &Vec<T>, ... ... ...

看起來 Index Trait 用起來並不容易。去 Rust 標準庫文件搜尋一番,發現 Index Trait 的定義 [1] 為

pub trait Index<Idx> where
    Idx: ?Sized, {
    type Output: ?Sized;
    fn index(&self, index: Idx) -> &Self::Output;
}

那麼 Vec<T> 有沒有實現這個 Trait 呢?實現了,原始碼為

impl<T, I: SliceIndex<[T]>, A: Allocator> Index<I> for Vec<T, A> {
    type Output = I::Output;

    #[inline]
    fn index(&self, index: I) -> &Self::Output {
        Index::index(&**self, index)
    }
}

上述程式碼裡有許多語法是我從未見過的,結合 rustc 給出的建議,只能看出 Index Trait 用於證明泛型引數支援下標運算的方式是需要為該 Trait 提供一個型別引數,該引數對於 Vec<T> 型別而言,應該是 SliceIndex<[T]>,後者又是一個 Trait。

再試試看:

use std::ops::Index;
use std::slice::SliceIndex;

fn matrix_index<T: Index<SliceIndex<[T]>>>(v: &Vec<T>, i: usize, j: usize) -> 不知該返回什麼型別 {
    return &v[i][j];
}

對於上述程式碼,rustc 給出警告:

warning: trait objects without an explicit `dyn` are deprecated
  |
  | fn matrix_index<T: Index<SliceIndex<[T]>>>(v: &Vec<T>, ... ... ...
  |                          ^^^^^^^^^^^^^^^

按照 rustc 的建議,將 matrix_index 修改為

fn matrix_index<T: Index<dyn SliceIndex<[T]>>>(v: &Vec<T>, i: usize, j: usize) -> 不知該返回什麼型別 {
    return &v[i][j];
}

實事求是,此時我並不知道 dyn 的用途是什麼,姑且先聽命於 rustc。如此這般之後,阿彌陀佛,我們的 rustc 終於能少說兩句了,最後它只是冷冰冰地說:

error[E0191]: the value of the associated type `Output` (from trait `SliceIndex`) must be specified
  |
  | ...x_index<T: Index<dyn SliceIndex<[T]>>>(v: &Vec<T>, ... ... ...
  |                         ^^^^^^^^^^^^^^^ help: specify the associated type: `SliceIndex<[T], Output = Type>`

好吧,再試試:

fn matrix_index<T: Index<dyn SliceIndex<[T], Output = usize>>>(v: &Vec<T>, i: usize, j: usize) -> 不知該返回什麼型別 {
    return v[i][j];
}

現在給 matrix_index 新增的這些零碎已經差不多像天書了,即便如此, rustc 不僅沒有消停,反而瘋掉了,報出的錯誤資訊多得讓正經人看不下去。於是我只好靈光一閃,大概猜到了 Index<...> 這裡的空應該填什麼了。要填的應該是實現了 SliceIndex<[T]> Trait 的型別。對於我的需求,下標的型別應該是 usize,那麼 usize 是否實現了 SliceIndex<[T]> Trait 呢?答案是實現了,有標準庫原始碼為證 [2]。據此,應將 matrix_index 定義為

fn matrix_index<T: Index<usize>>(v: &Vec<T>, i: usize, j: usize) -> 不知該返回什麼型別 {
    return &v[i][j];
}

現在,rustc 終於不再批評泛型引數 T 有什麼問題了,它的注意力轉移到 matrix_index 的返回值型別上了,報錯資訊為

error[E0412]: cannot find type `不知該返回什麼型別` in this scope
    |
    |   ...size, j: usize) -> 不知該返回什麼型別 {
    |                         ^^^^^^^^^^^^^^^^^^ help: a trait with a similar name exists: `AsMut

我決定無視 rustc 給出的 help,原因是,我不懂它在說什麼。matrix_index 是泛型函式,它接受的引數是個矩陣——元素為向量的向量,矩陣的元素並不是特定的某種型別。我都不知道的事,rustc 憑什麼知道呢?此外,我也實在看不出 不知道該返回什麼型別AsMut 這兩個名字有啥相似的地方。

不過,rustc 的上述建議還是給了我一些啟發,函式的返回型別可以是 Trait [3]!matrix_index 返回值的型別應該是什麼 Trait 呢?由於 matrix_index 的返回結果是傳遞給 println! 的。基於我對 Rust 的淺薄認識,println! 所接受的引數必須提供 Display Trait 的實現。因此,不妨試試將 matrix_index 的返回值型別設定成 &impl Display——實現了 Display Trait 型別的引用——,亦即

use std::ops::Index;
use std::fmt::Display;

fn matrix_index<T: Index<usize>>(v: &Vec<T>, i: usize, j: usize) -> &impl Display {
    return &v[i][j];
}

rustc 看了我的靈光一閃的做法後,又發表了一通批判和建議:

error[E0277]: `<T as Index<usize>>::Output` doesn't implement `std::fmt::Display`
  |
  | ...i: usize, j: usize) -> impl Display {
  |                           ^^^^^^^^^^^^ `<T as Index<usize>>::Output` cannot be formatted with the default formatter
  ... ... ...
  | fn matrix_index<T: Index<usize>>(v: &Vec<T>, i: usize, j: usize) -> impl Display 
  | where <T as Index<usize>>::Output: std::fmt::Display {
  | ++++++++++++++++++++++++++++++++++++++++++++++++++++

對其建議,姑且一試:

fn matrix_index<T: Index<usize>>(v: &Vec<T>, i: usize, j: usize) -> &impl Display
where <T as Index<usize>>::Output: Display {
    return &v[i][j];
}

rustc 又給出了新的錯誤報告以及建議:

error[E0277]: the size for values of type `<T as Index<usize>>::Output` 
cannot be known at compilation time
  |
  | fn matrix_index<T: Index<usize>>(v: &Vec<T>, i: usize, j: usize) 
  | -> &impl Display
  |    ^^^^^^^^^^^^^ doesn't hav  a size known at compile-time
  = help: the trait `Sized` is not implemented for `<T as Index<usize>>::Output`
  ... ... ...
help: consider further restricting the associated type
  |
  | where <T as Index<usize>>::Output: Display, <T as Index<usize>>::Output: Sized {
  |                                           ++++++++++++++++++++++++++++++++++++

於是,就有了本節一開始的那個答案。至此,我實在不知該如何讚美 rustc 了。也許要證明一個什麼東西是什麼東西,一直都是世界級難題吧。在人生中,請務必記住,不要去證明自己,否則自己的人生將像 Rust 程式碼一樣笨重。不過,還有一種可能,Rust 還有什麼更好的辦法能將本節一開始的程式碼寫得更優雅,只是我不知道,現在只能希望如此。

繁文縟節

現在可以寫出幾乎能夠統一各種元素為向量的向量(矩陣)的顯示函式了,

use std::ops::Index;
use std::fmt::Display;

trait HasLength {
    fn len(&self) -> usize;
}

impl<T> HasLength for Vec<T> {
    fn len(&self) -> usize {
        return self.len();
    }
}

fn display_matrix<T: HasLength + Index<usize>>(v: &Vec<T>)
where <T as Index<usize>>::Output: Display,
      <T as Index<usize>>::Output: Sized {
    for x in v {
        let n = x.len();
        for i in 0 .. n {
            if i == n - 1 {
                println!("{}", x[i]);
            } else {
                print!("{} ", x[i]);
            }
        }
    }
}

impl Mesh {
    fn display(&self) {
        println!("OFF");
        println!("{0} {1} 0", self.points.len(), self.facets.len());
        display_matrix(&self.points);
        display_matrix(&self.facets);
    }
}

感覺程式碼量比一開始未將點表和麵表遍歷過程統一的 display 方法所用的程式碼都要多,所以……我只是為了略加深入地學習一下 Trait 和泛型啊。

Display

既然已對 Trait 有所瞭解,為 Mesh 實現 display 方法,不如為 Mesh 實現 Display Trait。倘若根據 [4] 所載方法,很快會發現寫不下去,因為 Mesh 是複合結構,需要多次 write!,然而能搜到的實現 Display 的示例皆是單次 執行 write!。一個可行的方案是,先用 String 型別的字串收集所有要顯示的資訊,然後對該字串執行 write!

fn display_matrix<T: HasLength + Index<usize>>(v: &Vec<T>) -> String
where <T as Index<usize>>::Output: Display,
      <T as Index<usize>>::Output: Sized {
    let mut s = String::new();
    for x in v {
        let n = x.len();
        for i in 0 .. n {
            if i == n - 1 {
                s += format!("{}\n", x[i]).as_str();
            } else {
                s += format!("{} ", x[i]).as_str();
            }
        }
    }
    return s;
}

impl Display for Mesh {
    fn fmt(&self, f: &mut Formatter) -> Result {
        let mut s = String::new();
        s += format!("OFF\n").as_str();
        s += format!("{0} {1} 0\n", self.points.len(), self.facets.len()).as_str();
        s += display_matrix(&self.points).as_str();
        s += display_matrix(&self.facets).as_str();
        write!(f, "{}", s)
    }
}

此處要畫的重點是 &strString 的聯絡和區別。這些我已在 rhamal.pdf [5] 有所涉及。

閉包

請注意,在企圖以泛型函式統一點表和麵表的輸出過程時,前提是面表和點表的輸出過程完全一樣,但實際上並不一樣。輸出面的資訊時,不僅要輸出面表裡向量的值(面片頂點在點表中的下標),還要輸出面片頂點個數。這一差異可以通過為 display_matrix 增加一個引數的方式消除。例如

fn display_matrix<T: HasLength + Index<usize>>(v: &Vec<T>,
                                               prefix: 型別待定) -> String
where <T as Index<usize>>::Output: Display,
      <T as Index<usize>>::Output: Sized {
    let mut s = String::new();
    for x in v {
        let n = x.len();
        s += prefix(x).as_str();
        for i in 0 .. n {
            if i == n - 1 {
                s += format!("{}\n", x[i]).as_str();
            } else {
                s += format!("{} ", x[i]).as_str();
            }
        }
    }
    return s;
}

prefix 的意思是在輸出的每個點或面片資訊之前增加一些資訊,但是它應該是何種型別,值得探討,而且這可能是學習更多的 Rust 的好機會。

首先,如果將 prefix 設定為 bool 型別會怎樣?試試看

fn display_matrix<T: HasLength + Index<usize>>(v: &Vec<T>, prefix: bool) -> String
where <T as Index<usize>>::Output: Display,
      <T as Index<usize>>::Output: Sized {
    let mut s = String::new();
    for x in v {
        let n = x.len();
        if prefix {
            s += format!("{} ", n).as_str();
        }
        for i in 0 .. n {
            if i == n - 1 {
                s += format!("{}\n", x[i]).as_str();
            } else {
                s += format!("{} ", x[i]).as_str();
            }
        }
    }
    return s;
}

相應地,需將 MeshDisplay Trait 的實現修改為

impl Display for Mesh {
    fn fmt(&self, f: &mut Formatter) -> Result {
        let mut s = String::new();
        s += format!("OFF\n").as_str();
        s += format!("{0} {1} 0\n", self.points.len(), self.facets.len()).as_str();
        s += display_matrix(&self.points, false).as_str();
        s += display_matrix(&self.facets, true).as_str();
        write!(f, "{}", s)
    }
}

上述實現能夠解決問題,但是程式碼語義不好,並且靈活性太差。試想,如果在輸出的面片資訊之前要輸出的資訊不是面片頂點個數,而是由 display_matrix 函式的使用者自己決定的其他資訊呢?更好的辦法是用函式型別作為 prefix 的型別,亦即

fn display_matrix<T: HasLength + Index<usize>>(v: &Vec<T>,
                                               prefix: impl Fn(&T) -> String) -> String
where <T as Index<usize>>::Output: Display,
      <T as Index<usize>>::Output: Sized {
    let mut s = String::new();
    for x in v {
        let n = x.len();
        s += prefix(x).as_str();
        for i in 0 .. n {
            if i == n - 1 {
                s += format!("{}\n", x[i]).as_str();
            } else {
                s += format!("{} ", x[i]).as_str();
            }
        }
    }
    return s;
}

然後使用閉包作為 display_matrix 的引數 prefix 的值:

impl Display for Mesh {
    fn fmt(&self, f: &mut Formatter) -> Result {
        let mut s = String::new();
        s += format!("OFF\n").as_str();
        s += format!("{0} {1} 0\n", self.points.len(), self.facets.len()).as_str();
        s += display_matrix(&self.points, |_| "".to_string()).as_str();
        s += display_matrix(&self.facets, |x| format!("{} ", x.len())).as_str();
        write!(f, "{}", s)
    }
}

上述程式碼能夠工作,但是效率不高。因為輸出點表時,display_matrix 函式本不需要執行 prefix 函式,而現在不得不執行,而且所執行的 prefix 返回的是空字串。在輸出點表時,如何令 display_matrix 知道 prefix 是空值,不需要理睬呢?只需令 prefix 函式帶有狀態即可,下面給出一個可行方案。

首先定義一個泛型的結構體:

struct Prefix<T> {
    status: bool,
    body: fn(&T) -> String
}

impl<T> Prefix<T> {
    fn new() -> Prefix<T> {
        Prefix{status: false, body: |_| "".to_string()}
    }
}

然後將 display_matrix 改寫為

fn display_matrix<T: HasLength + Index<usize>>(v: &Vec<T>,
                                               prefix: Prefix<T>) -> String
where <T as Index<usize>>::Output: Display,
      <T as Index<usize>>::Output: Sized {
    let mut s = String::new();
    for x in v {
        let n = x.len();
        if prefix.status {
            s += (prefix.body)(x).as_str();
        }
        for i in 0 .. n {
            if i == n - 1 {
                s += format!("{}\n", x[i]).as_str();
            } else {
                s += format!("{} ", x[i]).as_str();
            }
        }
    }
    return s;
}

最後將 MeshDisplay 實現改寫為

impl Display for Mesh {
    fn fmt(&self, f: &mut Formatter) -> Result {
        let mut s = String::new();
        s += format!("OFF\n").as_str();
        s += format!("{0} {1} 0\n", self.points.len(), self.facets.len()).as_str();
        s += display_matrix(&self.points, Prefix::new()).as_str();
        s += display_matrix(&self.facets, Prefix{status: true,
                                                 body: |x| format!("{} ", x.len())}).as_str();
        write!(f, "{}", s)
    }
}

OFF 檔案 -> Mesh 結構體

經歷了幾個大插曲,現在可以迴歸正題了。以下程式碼能夠開啟 OFF 檔案,

use std::path::Path;
use std::fs::File;

let path = Path::new("foo.off");
let file = File::open(path).unwrap();

看到 unwrap 就該想到與之相關的型別可能是 ResultOption。Rust 標準庫總是擔心某個函式的返回值太過於直接而導致世界毀滅,因而非常熱衷於將函式的返回值先圈禁,再酌情釋放。

以下程式碼能夠讀取 OFF 檔案的第一行並對其核驗:

use std::io::{BufRead, BufReader};

let buf = BufReader::new(file);
let mut lines_iter = buf.lines().map(|l| l.unwrap());

// 核驗 OFF 檔案第一行內容是否為 "OFF"
// 凡是實現了 Eq Trait 的型別,皆能用 assert_eq! 比較是否相等
assert_eq!(lines_iter.next(), Some(String::from("OFF")));

BufReaderlines 方法返回的是迭代器,其型別為 Lines 結構體。Lines 結構體提供了 Iterator Trait (迭代器)的實現。迭代器可用於遍歷資料序列。Iteratormap 方法能夠接受函式作為引數並將其應用於迭代器所訪問的資料。上述程式碼中,map 方法接受的是閉包 |l| l.unwrap()Iteratornext 方法將迭代器向後移動,令其指向下一個元素並將其作為返回值。對迭代器執行第一次 next 時,得到的值便是迭代器所訪問的資料序列的第一個值,倘若繼續執行 next 方法,便可依次訪問資料序列後續的值,該過程通常隱匿於 for ... in 語法背後。倘若使用 for ... in 語法,遍歷檔案內容的每一行,只需

for line in buf.lines() {
    println!("{}", line.unwrap());
}

由於 OFF 檔案的第二行定義了多面體結構的點表和麵表的長度,決定了遍歷 OFF 檔案內容的過程需要由計數器控制,因此為方便獲得點表和麵表,顯式呼叫迭代器的的 next 方法較 for ... in 語法更為合適。

接下來,解析 OFF 檔案的第二行,獲得點表和麵表的長度資訊:

let second_line = lines_iter.next().unwrap();
let mut split = second_line.split_whitespace();
let n_of_points: usize = split.next().unwrap().parse().unwrap();
let n_of_facets: usize = split.next().unwrap().parse().unwrap();

由於 line_iter 迭代器返回值是包含著 String 型別例項的引用,對其 unwrap 後,便得到該引用。Stringsplit_whitespace 方法可基於空白字元對 String 型別例項進行分割。上一章我用了一個簡單的狀態機實現的 split_str 是針對 &str 型別的,分割結果儲存於 Vec<(usize, usize)>Stringsplit_whitespace 方法返回的並非字串的分割結果,而是迭代器。倘若使用我實現的 split_str 函式實現上述程式碼等同功能,只需

let second_line = lines_iter.next().unwrap();
let second_line_as_str = second_line.as_str();
let split = split_str(second_line_as_str);
let n_of_points: usize = second_line_as_str[(split[0].0 .. split[0].1)].parse().unwrap();
let n_of_facets: usize = second_line_as_str[(split[1].0 .. split[1].1)].parse().unwrap();

相形之下,還是用 Stringsplit_whitespace 方法吧。

有了點表和麵表的長度,便可用迭代器繼續讀取 OFF 檔案的剩下內容,解析點集資訊和麵集資訊:

let mut mesh = Mesh::new();
for _i in 0 .. n_of_points {
    let line = lines_iter.next().unwrap();
    let mut p: Vec<f64> = Vec::new();
    for x in line.split_whitespace() {
        p.push(x.parse().unwrap());
    }
    mesh.points.push(p);
}
for _i in 0 .. n_of_facets {
    let line = lines_iter.next().unwrap();
    let mut f: Vec<usize> = Vec::new();
    let mut split = line.split_whitespace();
    let n:usize = split.next().unwrap().parse().unwrap(); // 忽略第一個元素
    for x in split {
        f.push(x.parse().unwrap());
    }
    assert_eq!(n, f.len());
    mesh.facets.push(f);        
}

注意,在上述 for ... in 語句中,變數 i 在迭代過程中並未被使用,rustc 建議用 _i 代替。此外,在構造面表的過程中,由於檔案內容的第一個數字是面片的頂點數,該資訊我僅用於檢測面片解析結果的頂點數量是否正確。

基於 OFF 檔案構造的 Mesh 例項,其正確性如何驗證呢?由於 Mesh 型別已經實現了 Display Trait,不妨將其內容輸出,

print!("{}", mesh);

然後將輸出內容與 OFF 檔案內容予以比對。

小結

use std::ops::Index;
use std::fmt::{Result, Formatter, Display};
use std::path::Path;
use std::fs::File;
use std::io::{BufRead, BufReader};

struct Mesh {
    points: Vec<Vec<f64>>,  // 點表
    facets: Vec<Vec<usize>> // 面表
}

trait HasLength {
    fn len(&self) -> usize;
}

impl<T> HasLength for Vec<T> {
    fn len(&self) -> usize {
        return self.len();
    }
}

struct Prefix<T> {
    status: bool,
    body: fn(&T) -> String
}

impl<T> Prefix<T> {
    fn new() -> Prefix<T> {
        Prefix{status: false, body: |_| "".to_string()}
    }
}

fn display_matrix<T: HasLength + Index<usize>>(v: &Vec<T>,
                                               prefix: Prefix<T>) -> String
where <T as Index<usize>>::Output: Display,
      <T as Index<usize>>::Output: Sized {
    let mut s = String::new();
    for x in v {
        let n = x.len();
        if prefix.status {
            s += (prefix.body)(x).as_str();
        }
        for i in 0 .. n {
            if i == n - 1 {
                s += format!("{}\n", x[i]).as_str();
            } else {
                s += format!("{} ", x[i]).as_str();
            }
        }
    }
    return s;
}

impl Display for Mesh {
    fn fmt(&self, f: &mut Formatter) -> Result {
        let mut s = String::new();
        s += format!("OFF\n").as_str();
        s += format!("{0} {1} 0\n", self.points.len(), self.facets.len()).as_str();
        s += display_matrix(&self.points, Prefix::new()).as_str();
        s += display_matrix(&self.facets, Prefix{status: true,
                                                 body: |x| format!("{} ", x.len())}).as_str();
        write!(f, "{}", s)
    }
}

impl Mesh {
    fn new() -> Mesh {
        return Mesh {points: Vec::new(), facets: Vec::new()};
    }
    fn load(&mut self, path: &str) {
        let path = Path::new(path);
        let file = File::open(path).unwrap();
        let buf = BufReader::new(file);
        
        let mut lines_iter = buf.lines().map(|l| l.unwrap());
        assert_eq!(lines_iter.next(), Some(String::from("OFF")));
        let second_line = lines_iter.next().unwrap();
        let mut split = second_line.split_whitespace();
        let n_of_points: usize = split.next().unwrap().parse().unwrap();
        let n_of_facets: usize = split.next().unwrap().parse().unwrap();

        for _i in 0 .. n_of_points {
            let line = lines_iter.next().unwrap();
            let mut p: Vec<f64> = Vec::new();
            for x in line.split_whitespace() {
                p.push(x.parse().unwrap());
            }
            self.points.push(p);
        }
        for _i in 0 .. n_of_facets {
            let line = lines_iter.next().unwrap();
            let mut f: Vec<usize> = Vec::new();
            let mut split = line.split_whitespace();
            let n:usize = split.next().unwrap().parse().unwrap(); // 忽略第一個元素
            for x in split {
                f.push(x.parse().unwrap());
            }
            assert_eq!(n, f.len());
            self.facets.push(f);        
        }
    }
}

fn main() {
    let mut mesh = Mesh::new();
    mesh.load("foo.off");
    print!("{}", mesh);
}

參考

[1] https://doc.rust-lang.org/std...
[2] https://doc.rust-lang.org/src...
[3] https://doc.rust-lang.org/sta...
[4] https://rustwiki.org/zh-CN/ru...
[5] https://gitee.com/garfileo/rh...

相關文章