与 Rust 勾心斗角 · 解析 OFF 文件

garfileo
English

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 matrix_fmt<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]);
            }
        }
    }
}

之所以为这个函数名取名为 matrix_fmt,是因为元素为向量的向量,不就是矩阵吗?

然后,可将 Meshdisplay 方法定义为

impl Mesh {
    fn display(&self) {
        println!("OFF");
        println!("{0} {1} 0", self.points.len(), self.facets.len());
        matrix_fmt(&self.points);
        matrix_fmt(&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 认为 matrix_fmt 的泛型参数 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 matrix_fmt<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());
        matrix_fmt(&self.points);
        matrix_fmt(&self.facets);
    }
}

感觉代码量比一开始未将点表和面表遍历过程统一的 display 方法所用的代码都要多,所以……我只是为了略加深入地学习一下 Trait 和泛型啊。

Display

既然已对 Trait 有所了解,为 Mesh 实现 display 方法,不如为 Mesh 实现 Display Trait。倘若根据 [4] 所载方法,很快会发现写不下去,因为 Mesh 是复合结构,需要多次 write!,然而能搜到的实现 Display 的示例皆是单次 执行 write!。一个可行的方案是,先用 String 类型的字符串收集所有要显示的信息,然后对该字符串执行 write!

fn matrix_fmt<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 += matrix_fmt(&self.points).as_str();
        s += matrix_fmt(&self.facets).as_str();
        write!(f, "{}", s)
    }
}

此处要画的重点是 &strString 的联系和区别。这些我已在 rhamal.pdf [5] 有所涉及。

闭包

请注意,在企图以泛型函数统一点表和面表的输出过程时,前提是面表和点表的输出过程完全一样,但实际上并不一样。输出面的信息时,不仅要输出面表里向量的值(面片顶点在点表中的下标),还要输出面片顶点个数。这一差异可以通过为 matrix_fmt 增加一个参数的方式消除。例如

fn matrix_fmt<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 matrix_fmt<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 += matrix_fmt(&self.points, false).as_str();
        s += matrix_fmt(&self.facets, true).as_str();
        write!(f, "{}", s)
    }
}

上述实现能够解决问题,但是代码语义不好,并且灵活性太差。试想,如果在输出的面片信息之前要输出的信息不是面片顶点个数,而是由 matrix_fmt 函数的使用者自己决定的其他信息呢?更好的办法是用函数类型作为 prefix 的类型,亦即

fn matrix_fmt<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;
}

然后使用闭包作为 matrix_fmt 的参数 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 += matrix_fmt(&self.points, |_| "".to_string()).as_str();
        s += matrix_fmt(&self.facets, |x| format!("{} ", x.len())).as_str();
        write!(f, "{}", s)
    }
}

上述代码能够工作,但是效率不高。因为输出点表时,matrix_fmt 函数本不需要执行 prefix 函数,而现在不得不执行,而且所执行的 prefix 返回的是空字串。在输出点表时,如何令 matrix_fmt 知道 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()}
    }
}

然后将 matrix_fmt 改写为

fn matrix_fmt<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 += matrix_fmt(&self.points, Prefix::new()).as_str();
        s += matrix_fmt(&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,不妨基于该 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 matrix_fmt<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 += matrix_fmt(&self.points, Prefix::new()).as_str();
        s += matrix_fmt(&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...

阅读 1.1k

5.9k 声望
0 粉丝
0 条评论
5.9k 声望
1.9k 粉丝
宣传栏