cratosw

LINQ

LINQ

本节在“序列查询与转换”的语境下讨论 LINQ,主要关注 IEnumerable / IEnumerable<T> 及常见集合(列表、集合、字典等)。

IEnumerable<T>

Rust 中最接近 IEnumerable<T> 的抽象是 IntoIterator
就像 .NET 中 IEnumerable<T>.GetEnumerator() 会返回 IEnumerator<T>, Rust 中 IntoIterator::into_iter 会返回 Iterator

两门语言也都提供了遍历语法糖:

  • C# 使用 foreach
  • Rust 使用 for

C# 示例:

using System;
using System.Text;

var values = new[] { 1, 2, 3, 4, 5 };
var output = new StringBuilder();

foreach (var value in values)
{
    if (output.Length > 0)
        output.Append(", ");
    output.Append(value);
}

Console.Write(output); // Prints: 1, 2, 3, 4, 5

Rust 示例:

use std::fmt::Write;

fn main() {
    let values = [1, 2, 3, 4, 5];
    let mut output = String::new();

    for value in values {
        if output.len() > 0 {
            output.push_str(", ");
        }
        _ = write!(output, "{value}");
    }

    println!("{output}");  // Prints: 1, 2, 3, 4, 5
}

for 在 Rust 中大致可展开为:

use std::fmt::Write;

fn main() {
    let values = [1, 2, 3, 4, 5];
    let mut output = String::new();

    let mut iter = values.into_iter();
    while let Some(value) = iter.next() {
        if output.len() > 0 {
            output.push_str(", ");
        }
        _ = write!(output, "{value}");
    }

    println!("{output}");
}

需要特别注意:Rust 的所有权规则会影响迭代行为。
当对 Vec<T>for value in values 时,默认会移动每个元素所有权;若后续还要继续使用 原集合,应写成 for value in &values(按引用迭代)。

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

    let mut sum = 0;
    for value in &values {
        sum += value;
    }
    println!("sum = {sum}");

    let mut max = None;
    for value in &values {
        if let Some(some_max) = max {
            if value > some_max {
                max = Some(value)
            }
        } else {
            max = Some(value)
        }
    }
    println!("max = {max:?}");
}

可通过 Drop 观察“按值迭代会逐项被销毁”的行为:

struct Int(i32);

impl Drop for Int {
    fn drop(&mut self) {
        println!("{} dropped", self.0)
    }
}

fn main() {
    let values = [Int(1), Int(2), Int(3), Int(4), Int(5)];
    let mut sum = 0;

    for value in values {
        sum += value.0;
    }

    println!("sum = {sum}");
}

另见:

操作符

LINQ 操作符本质是可链式调用的扩展方法。Rust 没有 C# 的查询表达式语法(from/where/select), 但迭代器适配器(adapters)提供了非常接近的方法链能力。

在 C# 中,把命令式循环改写为 LINQ 往往能提升可读性与组合性,但有时会有性能权衡;
在 Rust 中,迭代器链通常可被编译器优化到接近手写循环的性能,因此在实际代码中非常常见。

下表给出常见 LINQ 方法与 Rust 近似映射:

.NETRustNote
Aggregatereduce见注释 1
Aggregatefold见注释 1
Allall
Anyany
Concatchain
Countcount
ElementAtnth
GroupBy-
Lastlast
Maxmax
Maxmax_by
MaxBymax_by_key
Minmin
Minmin_by
MinBymin_by_key
Reverserev
Selectmap
Selectenumerate
SelectManyflat_map
SelectManyflatten
SequenceEqualeq
Singlefind
SingleOrDefaulttry_find
Skipskip
SkipWhileskip_while
Sumsum
Taketake
TakeWhiletake_while
ToArraycollect见注释 2
ToDictionarycollect见注释 2
ToListcollect见注释 2
Wherefilter
Zipzip
  1. 不带 seed 的 Aggregate 接近 reduce;带 seed 的 Aggregate 接近 fold
  2. collect 可把迭代器收集为任意实现 FromIterator 的类型; 有时需显式标注目标类型(如 collect::<Vec<_>>(),即 turbofish)。

示例对比:

var result =
    Enumerable.Range(0, 10)
              .Where(x => x % 2 == 0)
              .SelectMany(x => Enumerable.Range(0, x))
              .Aggregate(0, (acc, x) => acc + x);

Console.WriteLine(result); // 50
let result = (0..10)
    .filter(|x| x % 2 == 0)
    .flat_map(|x| (0..x))
    .fold(0, |acc, x| acc + x);

println!("{result}"); // 50

延迟执行(惰性)

LINQ 中大量操作符都是延迟执行:只有在真正枚举结果时才发生计算。
Rust 迭代器同样遵循这种 惰性 语义,也天然支持流式处理。

这使得两者都能表达“无限序列”,再通过 Take/take 限制输出。

C#:

foreach (var x in InfiniteRange().Take(5))
    Console.Write($"{x} "); // Prints "0 1 2 3 4"

IEnumerable<int> InfiniteRange()
{
    for (var i = 0; ; ++i)
        yield return i;
}

Rust:

for value in (0..).take(5) {
    print!("{value} "); // Prints "0 1 2 3 4"
}

迭代器方法(yield

C# 的 yield 可快速实现迭代器方法(返回 IEnumerable<T>IEnumerator<T>), 由编译器生成状态机实现。

Rust 对应能力通常通过迭代器适配器与组合来实现;语言级 coroutines 在撰写本文时仍属不稳定特性。

On this page