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

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

反射

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 变量因为只读两侧都可以使用。