Rust学习笔记2:for循环与迭代器、类型等

Rust学习笔记2:for循环与迭代器、类型等

for循环与迭代器特征⭐

在Rust中,for循环适用于数组、切片、向量、范围、字符串、字典:

use std::collections::HashMap;
fn main() {
    // 数组
    let arr = [1, 2, 3];
    for v in arr {
        println!("{}", v); // 输出 1, 2, 3
    }
    // 切片
    let slice1 = &arr[0..2];
    for v in slice1 {
        println!("{}", v); // 输出 1, 2
    }
    // 向量
    let vec = vec![1, 2, 3];
    for v in vec {
        println!("{}", v); // 输出 1, 2, 3
    }
    // 范围(Range)
    for i in 0..=5 {
        println!("{}", i); // 输出 0, 1, 2, 3, 4, 5
    }
    // 字符串
    let s = String::from("hi");
    for c in s.chars() {
        println!("{}", c); // 输出 h, i
    }
    // 字典
    let mut map = HashMap::new();
    map.insert("a", 1);
    map.insert("b", 2);
    for (k, v) in map {
        println!("{}={}", k, v); // 输出 a=1, b=2
    }
}

这些集合类型之所以可以进行for循环,因为它们实现了IntoIterator trait。for循环其实是迭代器的语法糖。

Iterator自定义实现示例:

struct Counter {
  chars: String,
}
impl Counter {
  fn from(chars: String) -> Counter {
      Counter { chars }
  }
}
impl Iterator for Counter {
  type Item = char;
  fn next(&mut self) -> Option<Self::Item> {
      self.chars.pop()
  }
}
fn main() {
  let counter = Counter::from("123".to_string());
  for i in counter {
      println!("{}", i); // 输出 3 2 1
  }
}

结构体Counter实现了迭代器(Iterator)特征,在第17行便可以可以for关键字进行循环了。第12行,pop方法会从字符串顶部抛出字符,直到最后一个字符抛完,返回None。迭代器的结束是返回None。

上面说的是IntoIterator,这里为什么又是Iterator?前者实现的是into_iter方法,返回Iterator,然后给for进行迭代。for在循环时,真正调用的是Iterator的next方法。

三种迭代器特征

这三种特征是:

  • into_iter:拿走所有权,for循环中的默认操作
  • iter_mut:可变借用
  • iter:不可变借用

下面的示例可以呈现出它们的不同:

fn main() {
    let values = vec![1, 2, 3];

    for v in values {
        println!("{}", v) // 输出 1 2 3
    }

    // 下面的代码将报错,因为 values 的所有权已经被转移走
    // println!("{:?}",values);

    let mut values = vec![1, 2, 3];
    // 对 values 中的元素进行可变借用
    let mut values_iter_mut = values.iter_mut();

    // 取出第一个元素,并修改为0
    if let Some(v) = values_iter_mut.next() {
        *v = 0;
    }
    for v in values.iter_mut() {
        *v += 1; // 每个元素都可以修改
    }
    println!("{:?}", values); // 输出 [1, 3, 4]

    let values = vec![1, 2, 3];
    for v in values.iter() {
        println!("{}", v) // 输出 1 2 3
    }
}

第4行,默认调用的是into_iter方法,不写也是它。第13行,调用的是iter_mut方法,所以元素可以修改。第25行,调用的是iter方法,迭代取出的元素是不可变引用。

消费者适配器与迭代器适配器⭐

适配器是一种经典的设计模式,在这里是两类不同的方法。对于Rust中的适配器(Iterator),返回迭代器但不执行的方法是迭代器适配器,它们的作用是链式封装;消耗迭代器并返回值的方法是消费者迭代器。

常用的消费者适配器有collect(将迭代器元素收集到Vec、HashMap等集合中)、fold(累积)、sum、count、min、max、any、all等。常用的迭代器适配器有map、filter、take、skip、zip(合二为一)、rev(反转)等。

一般情况下,在拿到迭代器之后,先调用迭代器适配器方法一个或几个,最后调用消费者适配器给操作收尾。示例:

fn main() {
    let v = vec![1, 2, 3, 4, 5];
    // 使用 map 对每个元素加 1,再使用 filter 过滤偶数
    let filtered: Vec<_> = v.iter().map(|x| x + 1).filter(|x| x % 2 == 0).collect();
    println!("Filtered: {:?}", filtered); // 输出 Filtered: [2, 4, 6]
}

map与filter是最经常使用的迭代器适配器方法,这类方法是惰性方法,直到最后一步collect方法,迭代器代码才开始真正执行。

如何拿到集合的索引?

拿到索引,在其他语言中稀松平常,但在Rust中却要费一番“周折”。示例:

fn main() {
    let v = vec![1u64, 2, 3, 4, 5, 6];
    let val = v
        .iter()
        .enumerate()
        // 每两个元素剔除一个,剩余 [1, 3, 5]
        .filter(|(index, _)| index % 2 == 0)
        .map(|(_, val)| val)
        // 累加 1+1+3+5 = 10
        .fold(1, |sum, acm| sum + acm);

    println!("{}", val);
}

第5行,enumerate是一个迭代器适配器方法,它会在元素前面放一个索引,并将两者包装成一个元组,重新组成新的元素。这是Rust拿到索引的方式。第10行,fold是累积方法,在这个示例中它会将各元素累积加起来,因为起始数字是1,总结果是10。

使用as进行类型转换

小类型向大类型转移用as:

let c = 'a' as u32; // 将字符'a'转换为整数

反过来不一定成功,用try_into:

fn main() {
  let a: u16 = 15;
  let b: u8 = a.try_into().unwrap(); 
  println!("{},{}",a,b); // 15,15
}

第3行使用了try_info,这是从哪里到哪里try呢?是从a的类型(u16)到b的类型(u8)尝试进行的类型转换。

点操作符的自动转换规则⭐

在其他语言中,值调用与指针调用是分得很开的,例如在C++中,通过点操作符调用值对象的方法,通过箭头符号(->)调用指针对象的方法。但在Rust中不是这样,我们可以严格按照类型分明的风格进行调用,也可以采用“偷懒”的方式进行调用,因为Rust在使用点操作符时有一些自动的转换规则:

1,尝试通过值方法调用

2,尝试通过引用方法或可变引用方法调用

3,尝试通过解引用方法调用

4,尝试将定长类型降为非定长类型进行调用

Rust编译器会从上向下依次尝试,直到有一种方式可以行通。在Rust中,方法的定义是很严格的,只有确切定义了某种方法也可以调用,没有被定义的是调用不了的。

下面是一个综合示例:

use std::ops::{Deref, DerefMut};

// 定义一个结构体 MyStruct
struct MyStruct;

impl MyStruct {
    // 通过值调用
    fn foo_once(self, n: i32) {
        println!("{n} MyStruct::foo called by value");
    }

    // 通过引用调用
    fn foo(&self, n: i32) {
        println!("{n} MyStruct::foo called by reference");
    }

    // 通过引用调用
    fn bar(&mut self, n: i32) {
        println!("{n} MyStruct::bar called by mut reference");
    }
}

// 定义一个元组结构体 Wrapper,包裹 MyStruct
struct Wrapper(MyStruct);

// 实现 Deref 特征,使得 Wrapper 可以被解引用为 MyStruct
impl Deref for Wrapper {
    type Target = MyStruct;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

// 实现 DerefMut 特征,使得 Wrapper 可以被解引用为可变的 MyStruct
impl DerefMut for Wrapper {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

fn main() {
    // 1. 值方法调用
    let value = MyStruct;
    value.foo_once(1); // 输出 1 MyStruct::foo called by value
    // println!("{}", value);  // 打印失败,因为已经被消耗

    // 2. 引用方法调用
    let value = MyStruct;
    (&value).foo(2); // 输出 2 MyStruct::foo called by reference
    value.foo(2); // 输出 2 MyStruct::foo called by reference
    //  可变引用方法调用
    let mut value = MyStruct;
    (&mut value).bar(2); // 输出 2 MyStruct::bar called by mut reference
    value.bar(2); // 输出 2 MyStruct::bar called by mut reference

    // 3. 解引用方法调用
    let mut wrapper = Wrapper(MyStruct);
    (&mut (*wrapper)).bar(3); // 输出 3 MyStruct::foo called by reference
    wrapper.bar(3); // 输出 3 MyStruct::bar called by mut reference

    // 4. 定长类型转为不定长类型
    let array = [Wrapper(MyStruct), Wrapper(MyStruct)]; // 数组元素类型为 Wrapper
    array[0].foo(4); // 输出 4 MyStruct::foo called by reference
    for v in array {
        v.foo(4); // 输出 4 MyStruct::foo called by reference
    }
    let mut array = [Wrapper(MyStruct), Wrapper(MyStruct)]; 
    for v in &mut array {
        v.bar(4); // 输出 4 MyStruct::bar called by mut reference
    }
}

第45行,这是一个老老实实的值方法调用。第46行调用不了,那是因为在第45行value的所有权被转移,被消耗了。

第50行和第54行的调用是老老实实的调用,代码中的对象写法,与方法中的参数类型是相同的。然而第51行和第55行的写法,就应用了点操作符的自动转换规则,这种写法很简洁,但不了解的程序员看了会感到迷惑。有时候初学者反而不容易感到迷惑,反而是有经验的程序员更容易感到迷惑。

第59行,这是一个正常的调用,先是解引用,然后再转换到可变引用,最后调用foo方法。如果Rust代码都需要这样写,9成的程序员都要疯掉。于是Rust增加了隐藏转换规则,第60行,直接在wrapper对象上使用点操作符调用bar方法就可以了。

第66行,演示了引用方法调用。第70行,演示了可变引用方法调用。这两处的for循环,在循环开始之前就已经把定长的数组,变成了不定长的切片,只不过这一点很难测试出来。

据上面这个综合示例,及它要演示的转换规则,就可以大声埋怨:Rust是所有高级语言中语法最复杂的语言,没有之一。

新类型与类型别名

新类型是为既有类型添加新方法的途径。在Rust中,如果我们想为A类型添加T特征,必须保证A与T至少有一个在当前的作用域内,如果两者都是官方的标准库中或在自己无法控制的第三方类库中怎么办呢?这时候可以使用新类型(newtype)。

示例:

use std::fmt;

struct StringVec(Vec<String>);

impl fmt::Display for StringVec {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}]", self.0.join(", "))
    }
}

fn main() {
    let w = StringVec(vec![String::from("hello"), String::from("world")]);
    println!("{}", w); // 输出 [hello, world]
}

第3行,这是使用无组结构体定义的一个新类型。第5行为它实现了Display特征。第13行,使用println!输出,结果是一个中括号包裹的以逗号分隔的元素列表,这个打印样式是在第7行定义的。

如果不是新类型,我们无法为Vec<String>类型实现Display特征。在Rust中,实现某个特征,就是实现该特征定义的方法。

下面看类型别名。先看一个糟糕的示例:

let f: Box<dyn Fn() + Send + 'static> = Box::new(|| println!("hi"));

fn takes_long_type(f: Box<dyn Fn() + Send + 'static>) { }

fn returns_long_type() -> Box<dyn Fn() + Send + 'static> { }

在这个示例中,f的类型拥有一个很长的描述。描述略有不同,甚至元素长度不同就是一个新的类型,这个传统貌似是Go语言开创的,现在Rust将它充分发扬光大了,在Rust中,可以说有着数不尽的类型。在以往的语言中,例如C、JS等,类型是固定的,但是在Rust中不再是这样,像这里的Box<dyn Fn() + Send + 'static>就是一个独立的类型。这个类型很长,使用很不方便,这时候可以用类型别名简化它。

type Thunk = Box<dyn Fn() + Send + 'static>;

let f: Thunk = Box::new(|| println!("hi"));

fn takes_long_type(f: Thunk) { }

fn returns_long_type() -> Thunk { }

使用类型别名后,代码简单了许多。类型别名仅是一个别名,与传统开发中在bashrc文件中写的alias类似,原类型有什么特征,别名依然具有,类型的本质并没有发生变化。

2025年03月10日

该文由 rustpress 编译。

版权所有

本文链接:

许可证:署名-非商业性 4.0 国际 (CC-BY-NC-4.0) 查看许可说明

分享这篇文章

评论

微信小游戏开发

微信小游戏开发

学习微信小游戏开发技术,掌握游戏开发全流程

查看详情