Rust 高级语法

2022-09-29 rust language

这里简单介绍基本常用的语法。

泛型

大部分语言都有针对类型的代码复用能力,Rust 同样提供了泛型支持模板功能,包括了函数、结构体等都支持,同时在 Rust 中会通过 trait 限制特定类型。

fn largest<T>(list: &[T]) -> T {
    let mut largest = list[0];
    for &item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}

fn main() {
    let numbers = vec![34, 50, 25, 100, 65];
    println!("The largest number is {}", largest(&numbers));

    let chars = vec!['y', 'm', 'a', 'q'];
    println!("The largest char is {}", largest(&chars));
}

实际上述内容编译时会报错,主要是由于比较符号并非对所有类型都适用,这就需要通过 Trait 对类型进行约束,也就是说,相关的类型需要实现对应的接口才可以。

只需要把上述的第一行修改为 fn largest<T: PartialOrd + Copy>(list: &[T]) -> T { 即可,也就是类型需要支持比较以及复制 trait 才行,这也被称为特征区间 (trait bounds)。

如果约束的 trait 太多,还可以将其放到最后,如下。

fn largest<T>(list: &[T]) -> T
where T: PartialOrd + Copy,
{
    let mut largest = list[0];
    for &item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}

还可以参考如下示例。

struct Rectangle<T> {
    width: T,
    height: T,
}

impl<T> Rectangle<T>
where
    T: std::ops::Mul<Output = T> + Copy,
{
    fn area(&self) -> T {
        self.width * self.height
    }
}

fn main() {
    let rect = Rectangle {
        width: 30,
        height: 50,
    };
    println!("The area is {}", rect.area());
}

大部分情况无需显示指定模板类型,编译器会自动进行推导,不过有些场景需要手动指定,否则有 type annotations needed for xxx ,例如有个队列 Queue::<String>::with_capacity(5)

关联类型

关联类型 (Associated Type) 是泛型的一个子概念,跟 trait 绑定,指定的方法中可以使用,用来指定输出的类型,在实现是具体指定类型。其中最常见的是 Iterator 这个 trait 的实现,其定义方法如下。

pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

当然,也可以直接使用泛型,区别在于,使用关联类型时,只需要在实现时指定后续调用接口时无需每次再指定,而泛型则需要每次实现时都指定类型。

逆变 VS. 协变

一般是编程语言中类型系统 (尤其是泛型) 中的概念,用来描述父子类型使用过程中是否允许替换,例如 RectangleShape 的子类型,那么有如下规则:

  • 协变 Covariance 使用父类 (Shape) 的地方可以用子类 (Rectangle) 替换。
  • 逆变 Contravariance 使用子类 (Rectangle) 的地方可以使用父类 (Shape) 替换。
  • 不变 Invariance 两者不能相互替换。

在 Java 中有继承关系,其中:A) 函数如参是协变,返回值是逆变;B) 泛型支持协变 <? super T> 和逆变 <? extends T> 两种。通常,逆变是希望使用更加具体的子类型。

而 Rust 没有继承的概念,但是生命周期采用的是相同的方式,详细可以查看死灵书的内容。

反射

Rust 中通过 std::any::Any 来实现反射的能力,其定义如下,同时保留了其使用示例。

pub trait Any: 'static {
    /// use std::any::{Any, TypeId};
    ///
    /// fn is_string(s: &dyn Any) -> bool {
    ///     TypeId::of::<String>() == s.type_id()
    /// }
    ///
    /// assert_eq!(is_string(&0), false);
    /// assert_eq!(is_string(&"cookie monster".to_string()), true);
    #[stable(feature = "get_type_id", since = "1.34.0")]
    fn type_id(&self) -> TypeId;
}

pub struct TypeId {
    t: (u64, u64),
}

也就是通过 TypeId 来标识具体的类型,而所有的类型都实现了 Any 接口,其对应的值是编译器在编译阶段确定的。另外,其中的 'static 意味着返回类型是在编译阶段已经确定的,而其数据可以修改。

如下是一个简单示例。

use std::any::Any;

fn dump(val: &dyn Any) {
    if let Some(v) = val.downcast_ref::<u32>() {
        println!("u32 {:x}", v);
    } else if let Some(v) = val.downcast_ref::<&str>() {
        println!("str {}", v);
    }
}

fn main() {
    let val = "Hello World";
    let ptr: &dyn Any = &val;
    println!("{:?}", ptr.type_id());
    assert!(ptr.is::<&str>());

    dump(&val);
    let val: u32 = 42;
    dump(&val);
}

闭包

现在的多数语言都有闭包特性的支持,简单来说就是匿名函数,可以捕获调用者作用域中的值,这样原环境中的变量所有权就没了,完全由闭包控制,即使脱离了上下文仍然可以运行。当然,个别情况并非完全一致,但这是理解闭包的关键。

其语法很简单 |args| -> ret {codes},如果没有返回值或者代码只有一行,那么还可以进一步简化,而且还可以自动推断参数类型。另外,被捕获的原上下文变量被称为自由变量。

fn main() {
	// 简单示例
	let foobar1 = |var: i32| -> i32 {
		println!("variable = {}", var);
		var
	};
	foobar1(10);

	// 没有返回值
	let foobar2 = |var: i32| {
		println!("variable = {}", var);
	};
	foobar2(20);

	// 只有一行
	let foobar3 = |var: i32| println!("variable = {}", var);
	foobar3(30);

	// 自动推导类型
	let foobar4 = |var| println!("variable = {}", var);
	foobar4(40);
}

涉及到闭包变量捕获的会稍微有些麻烦。

变量捕获

与闭包相关的 Trait 有三个,分别为:

  • Fn 不修改,捕获的是 &T 类型,闭包可以重复执行多次。
  • FnMut 会修改,捕获的是 &mut T 类型,可以重复执行多次。
  • FnOnce 变量所有权会转移,也就是 move 到闭包,只能执行一次,而且外部不能使用。

move

关键字 move 的作用是将所引用变量的所有权转移到闭包内,通常用于闭包的生命周期大于所捕获变量的原生命周期。

fn main() {
    // 实现了Copy则闭包使用的是副本
    let num01 = 5;
    let mut num02 = 5;
    let mut func01 = move |x: i32| -> i32 { num02 += x + num01; num02 };
    println!("num is {:?}, mut is {:?}", num01, num02); // 5 5
    let ret01 = func01(3);
    println!("num is {:?}, mut is {:?}, ret is {:?}", num01, num02, ret01); // 5 5 13

    // 因为没有move关键字,虽然实现了Copy所有权仍然被占用,而且在释放所有权之前无法使用
    let num03 = 5;
    let mut num04 = 5;
    let mut func02 = |x: i32| -> i32 { num04 += x + num03; num04 };
    //println!("num is {:?}, mut is {:?}", num03, num04); // no ownership
    let ret02 = func02(3);
    println!("num is {:?}, mut is {:?}, ret is {:?}", num03, num04, ret02); // 5 13 13

    // 未实现Copy则闭包会拥有所有权,无论是否存在move
    let str01 = "Hello";
    let mut str02 = "World".to_string();
    let mut func01 = move |x: &str| -> &str { str02 = str01.to_owned() + x + str02; str02 };
    //println!("str is {:?}, mut is {:?}", num01, num02); // 5 5
    let ret03 = func01(" ");
    println!("str is {:?}, mut is {:?}, ret is {:?}", str01, str02, ret03); // 5 5 13
}
  • move 对实现 copy trait 的变量,闭包使用的是副本,原变量无影响;未实现 copy trait 则原变量所有权无,只是非 mut 变量因为只读,外部扔可以使用。
  • 无 move 所有变量被捕获,同样,只是因为非 mut 变量因为是只读,外部扔可以使用;另外,如果包的声明周期较短,外部仍然可以使用。

简言之,闭包会捕获变量,有两个特殊地方:A) move + copy trait 捕获时会使用复制,这样原作用域仍然可以使用;B) mut 变量被捕获后原作用域不能使用,非 mut 变量因为只读两侧都可以使用。

函数编程

最常用的是迭代器,提供了零抽象能力,不会引入运行时开销,如下是常用的示例。

fn main() {
    let v0 = vec![1, 2, 3];
    for i in v0.iter() {
        println!("{}", i);
    }

    let v1: Vec<i32> = v0.iter().map(|x| x * x).collect();
    println!("{:?}", v1); // [1, 4, 9]

    let v1: Vec<&i32> = v0.iter().filter(|x| *x % 2 == 0).collect();
    println!("{:?}", v1); // [2]

    let v1: i32 = v0.iter().fold(0, |a, x| a + x);
    println!("{:?}", v1); // 6

    let v2 = vec![4, 5, 6];
    let v1: Vec<i32> = v0.iter().chain(v2.iter()).copied().collect();
    println!("{:?}", v1); // [1, 2, 3, 4, 5, 6]

    let v2 = vec!["Andy", "Tom", "Bob"];
    for (key, val) in v2.iter().zip(v0.iter()) {
        println!("{}={}", key, val); // Andy=1 Tome=2 Bob=3
    }

    let mut v3 = vec![1, 2, 3];
    for i in v3.iter_mut() {
        *i *= 2;
    }
    println!("{:?}", v3); // [2, 4, 6]
}

常用方法:

  • map 转换数据,通过闭包处理其中每个元素,然后返回新的含结果迭代器,原迭代器不变。
  • filter 过滤数据,同样通过闭包处理每个元素,当闭包返回 true 时对应元素包含在新的迭代器中。
  • fold 聚合数据,通过初始值和闭包循环调用每个元素,最终生成单一最终值。
  • chain 用于将两个迭代器链接在一起,会先便利第一然后第二个。
  • zip 将两个迭代器同时进行遍历处理,每次同时返回对应的元素。
  • copied() 复制所有的元素,可以将 &T 转换为 T 类型,其作用与 map(|&x| x) 相同。

注意,闭包调用时,某些函数使用的是引用。如果要同时修改变量,那么就需要使用 iter_mut() 迭代。