C++ 玩家的 Rust 学习笔记(下)

泛型编程

开始进入有趣的地方了。 Rust 中的泛型和 C++ 的泛型实现类似,都是 reified generic ,也就是在运行时会保留类型信息的泛型。与之相对的, Java 的泛型会在编译检查完成后进行类型擦除,两者各有利弊,这里不作讨论。

Rust 的泛型、 trait 就是简化后的 C++ templateconcept ,没有什么图灵完备的一大堆折磨人的模板元编程,更不会有现在又在折腾的什么 Stateful Meta-programming 。Rust 的泛型,就是简单纯粹的泛型。需要编译期运算,还是安安心心等 nightly 的 const 函数吧。

从泛型函数开始。如果我们只需要一个类型,比如说我们叫它 T ,我们要找两个 T 类型值的最大和最小值,如果像 C++ 一样只需要 LessThanComparable 也就是能用小于号进行比较的条件,在 Rust 中这是一个 trait ,叫 std::cmp::PartialOrd ,我们就需要在声明处显式指定 T: std::cmp::PartialOrd ,限制传入的 T 需要满足这个 trait,或者用类似于 C++2a concept 中 requires 关键字的 where 在函数声明尾部指定依赖的 traits。

pub fn min<'a, T: std::cmp::PartialOrd>(lhs: &'a T, rhs: &'a T) -> &'a T {
    if rhs < lhs { rhs } else { lhs }
}

pub fn max<'a, T: std::cmp::PartialOrd>(lhs: &'a T, rhs: &'a T) -> &'a T {
    if rhs < lhs { lhs } else { rhs }
}

于是这样,我们的 minmax 就可以接受任何实现了 std::cmp::PartialOrd 的类型(的引用)了。

对于结构体而言,我们也可以定义自己的泛型数据结构,这个和 C/C++ 类似,实现半个复数举例子。

pub struct Cmplx<T> {
    real: T,
    imag: T,
}

impl<T> Cmplx<T> {
    pub fn new(real: T, imag: T) -> Cmplx<T> {
        Cmplx { real, imag, }
    }
}

impl<T: Copy> Clone for Cmplx<T> {
    fn clone(&self) -> Cmplx<T> {
        *self
    }
}

impl<T: Copy> Copy for Cmplx<T> {
}

可见,实现的时候也需要指定一个 T ,这里 impl 后的 T 和 C++ 中 templatetypename T 类似,都是声明泛型类型, for 后面接着的是为哪个类实现 trait 。接下来试着为这个复数类实现一个加法:

impl<T> std::ops::Add for Cmplx<T>
        where T: Copy + std::ops::Add<Output=T> {
    type Output = Cmplx<T>;
    fn add(self, other: Cmplx<T>) -> Cmplx<T> {
        return Cmplx {
            real: self.real + other.real,
            imag: self.imag + other.imag,
        };
    }
}

impl<T> std::ops::Add<T> for Cmplx<T>
        where T: Copy + std::ops::Add<Output=T> {
    type Output = Cmplx<T>;
    fn add(self, other: T) -> Cmplx<T> {
        return Cmplx {
            real: self.real + other,
            imag: self.imag,
        };
    }
}

稍微变得复杂了。首先指定 TCopyAdd 的被移动到了一个 where 中,以降低 impl<T> 块的复杂度。我们指定了 TCopystd::ops::Add<Output=T> ,也就是可以 Copy 而且可以相加,且返回类型是 T 本身的类才能作为我们复数类的一部分。另外 impl 块也可以特化,但是对于其它普通函数和结构体的特化似乎只在 nightly 里有。

很蛋疼的是,以上实现的加法都是 Cmplx<T> 在左侧的 (LHS) ,如果需要实现类在右手侧 (RHS) 的加法,不能直接 impl<T> std::ops::Add<Cmplx<T>> for T ,必须要给每个你想实现的类型一个一个实现过去。虽然可以用宏来降低一点难度( Rust 的宏真是玄学,简直就是另一个小语言)。

当然,还可以给这个复数类实现地更完美,我就不在这上面继续花时间了,需要用复数类可以用 num::complex::Complex

运行时多态

Rust 中不允许类的直接继承,但是类可以继承 trait ,而 trait 可以用 trait object 的形式保存被实现的类的引用或指针,所以 Rust 还是有办法实现动态多态的。

用 trait object 和引用或指针实现。

trait Animal {
    fn talk(&self);
}

struct Dog {
}

impl Animal for Dog {
    fn talk(&self) {
        println!("Woof");
    }
}

struct Cat {
}

impl Animal for Cat {
    fn talk(&self) {
        println!("Meow");
    }
}

fn main() {
    let animals: [&Animal; 2] = [
        &Dog { },
        &Cat { },
    ];
    for ani in animals.iter() {
        ani.talk();
    }
}

优点自然是扩展性强,只要实现 trait 就可以作为一个实例被使用即,但是缺点是 trait object 会维护一张虚表,运行时代价略大了一点。

另一种是静态的,利用枚举配合 match 实现多态。

enum Animal {
    Dog { },
    Cat { },
}

impl Animal {
    fn talk(&self) {
        match self {
            &Animal::Dog {} => println!("Woof"),
            &Animal::Cat {} => println!("Meow"),
        };
    }
}

fn main() {
    let animals: [Animal; 2] = [
        Animal::Dog { },
        Animal::Cat { },
    ];
    for ani in animals.iter() {
        ani.talk();
    }
}

这种相对不容易扩展,每次扩展都需要在 enum 中声明,在 impl 中 match 对应的分支,但是运行时效率高一点,它不需要虚函数也不需要反射。

卫生

在之前的学习中我主要用的宏函数是 println! vec! panic!,可见 Rust 的宏都是标识符后带一个感叹号的形式。 Rust 的宏是卫生宏, 相对于 C/C++ 的宏, Rust 的宏利用编译器为宏内的变量与宏外的变量打上不同的标记避免了一系列的直接替换带来的变量名重复、运算符优先级混乱的问题。但是模式匹配的语法另一方面又引入了新的复杂性。

举几个简单的 C/C++ 宏的例子,

#define ADD_3(x) x + 3

#define SWAP(x, y) do {	\
    __typeof(x) t = x;  \
    x = y;              \
    y = t;              \
} while (true);

int main(void) {
    int y = 3 * ADD_3(5);
//  int y = 3 * 5 + 3;
//  int y = (3 * 5) + 3;
}

从简单的宏说起, Rust 使用 macro_rules! 定义宏,如果我们定义一个空的宏,就像:

macro_rules! identity {
    ($e:expr) => ($e);
}

左边匹配项的括号里没有东西就表示接受空参数,右边扩展后一个括号里面空的表示什么都不干。而如果我们想写一个接受一个表达式但是什么都不干的宏。

macro_rules! identity {
 ($e:expr) => ($e);
}

这个表示接受一个 expr 也就是表达式参数绑定给 $x ,如果匹配到了则扩展成 $x 。可以使用 identity!(expr) 或者 identity![expr] 的模式调用。当然这个和 vec! 还是差很多的, vec! 支持用 [init; len] 形式初始化,我这里没有匹配。

Rust 的宏还可以循环匹配,也可以理解成多参数的宏,用 * 匹配零个或以上,用 + 匹配一个或以上。拿 vec! 举例而言

macro_rules! myvec {
    ( $( $e:expr ), * ) => {
        {
            let mut temp = Vec::new();
            $(
                temp.push($e);
            )*
            temp
        }
    }
}

左边括号里的含义是接受任意数量的参数,调用的时候每个逗号分隔的区间都是一个表达式。右侧的扩展中,外侧大括号是语法规则,表示扩展成这堆括号内的内容,里面的大括号就是扩展内容。比如 myvec!(0, 1, 2, 3) 扩展成

{
    let temp = Vec::new();
        temp.push(0);
        temp.push(1);
        temp.push(2);
        temp.push(3);
    temp
}

于是最后返回的是一个 temp

除此之外,宏还可以递归调用自身,瞬间强大很多有木有……

不过宏的限制还是相对比较高的,因为只能捕获有限的 expr ident 之类的预先定义的东西,所以捕获一个自定义的匹配进行递归其实也很难做到。不过总之还是比 C/C++ 的宏要强大。

后记 & 吐槽

这系列文章偏向于语法还有一些编程范式上和 C/C++ 对比,很少有库的解释,所以没有什么测试用库、并发库之类的介绍。嘛,毕竟我还是个初学者。我在写这些东西的同时还在继续查资料, 还在不停地被一些神奇的(没胆写进来)的语法吓到,比如说那个 syntax::ast 我现在还是懵逼的。

如何评价 Rust ?这门语言从出生就明显地想与 C++ 一较高下(与 C 一较高下我说不出口,你都链接到 libc 了还高下个头),试图做一个更安全、更简单的 C++ 。可以说 Rust 是做到了安全,但是为了安全又引入了非常复杂的概念和记号,对于 C++ 程序员可能一看就会心一笑,知道新的语法是为了解决什么样的问题,但是大部分并不使用 C++ 的程序员可能对一堆新的概念不明所以,为了打败 C++ 平添了许多自带 GC 的语言中不存在的坑,对这类程序员来说很不友好。讲道理,这都 21 世纪了,开发效率才是王道,这一堆编译错误是想让人调到什么时候,更何况编译错误还没法调试,我甚至都不知道到底是哪个地方的逻辑问题导致的编译出错。

不过从 Rust 的角度回去看 C++ ,近年来 C++ 的新东西—— C++11 的匿名函数, Rust 有了; C++11 的智能指针, Rust 默认指针就是智能的; C++11 的类型推导, Rust 有更强大的推导; C++11 的移动语义, Rust 有了; C++11 的多线程支持, Rust 标准库自带了; C++17 的 std::variant , Rust 有枚举类型了; C++17 有结构体化绑定声明了, Rust 有更强大的模式匹配; C++2a 要引入 concept 了, Rust 有 trait 了; C++?? 要引入 ranges 了, Rust 有迭代器了; C++?? 终于要有模块 Module 了, Rust 早就有了。可见, Rust 确实在试图做一个更好的 C++ ,而且也的确做到了,并且做的更好。

但是一开始就说过了,一门语言能不能成为潮流和这门语言的设计好不好无关,和生态、环境的关系很大。Rust 现在的提到的应用场景都有其它语言独占鳌头,它又不像 Go 有它的 goroutine 一样,没有什么夺人眼球的特性特别有利于它挤进排行榜前列,就落着个不上不下的尴尬位置。

不管 Rust 最终会不会像 Dart 、 D 一样被人遗忘,它的一些概念也的确被一些更新的语言所采用,比如 C# 和 Swift ,连 C++ 也从 Rust 中学到了不少新的语法。 Rust 的存在,至少不是无意义的。

阿 Rua 是真的严格。

发表评论

电子邮件地址不会被公开。 必填项已用*标注