改进rust代码的35种具体方法-类型(十)-使用回数变换而不是显式循环

上一篇文章


卑微的循环经历了一段越来越便利和越来越抽象的漫长旅程。B语言(C的前身)只有while (condition) { ... },但随着C的到来,通过数组索引进行遍默的常见场景随着for循环的添加而变得更加方便:

  // C code
  int i;
  for (i = 0; i < len; i++) {
    Item item = collection[i];
    // body
  }

C++的早期版本允许将循环变量声明嵌入for语句中,从而进一步提高了便利性和范围界定(C在C99中也采用了这一点):

  // C++98 code
  for (int i = 0; i < len; i++) {
    Item item = collection[i];
    // ...
  }

大多数现代语言进一步抽象了循环的概念:循环的核心功能通常是移动到某个容器的下一个项目,跟踪到达该项目所需的物流(index++++it)大多是一个无关紧要的细节。这一实现产生了两个核心概念:

  • iterators:一种类型,其目的是重复发射容器1的下一个项目,直到用尽。
  • For-Each Loops:一个紧凑的循环表达式,用于遍及容器中的所有项目,将循环变量绑定到该项目,而不是到达该项目的详细信息。

这些概念允许更短的循环代码,并且(更重要的是)更清楚地说明了什么意图:

  // C++11 code
  for (Item& item : collection) {
    // ...
  }

一旦这些概念可用,它们显然非常强大,以至于它们被迅速改装成那些还没有它们的语言(例如,每个循环都被添加到Java1.5和C++11中)。

Rust包括整数和for-a每个样式循环,但它也包括抽象的下一步:允许整个循环表示为整数变换。与改进rust代码的35种具体方法-类型(三)OptionResult的讨论一样,该项目将尝试展示如何使用这些回数变换而不是显式循环,并指导何时是一个好主意。

在本项结束时,一个类似C的显式循环将向量的前五个偶数项的平方相加:

    let mut even_sum_squares = 0;
    let mut even_count = 0;
    for i in 0..values.len() {
        if values[i] % 2 != 0 {
            continue;
        }
        even_sum_squares += values[i] * values[i];
        even_count += 1;
        if even_count == 5 {
            break;
        }
    }

应该开始感觉更自然地表达为功能性风格表达:

    let even_sum_squares: u64 = values
        .iter()
        .filter(|x| *x % 2 == 0)
        .take(5)
        .map(|x| x * x)
        .sum();

像这样的惯量转换表达式大致可以分为三部分:

  • 初始源更像器,来自Rust的其它者特征之一。
  • 一系列的iterator变换。
  • 将迭代结果组合成最终值的最终消费者方法。

前两个有效地将功能从循环主体移动到for表达式中;最后一个完全消除了对for语句的需求。

遍历特质(迭代特质)

核心Iterator特征有一个非常简单的界面:next方法,产生Some项目,直到它没有(None)。

允许对其内容进行迭代的集合(称为迭代对象)实现了IntoIterator特征;该特征的into_iter方法消耗了Self,并代替其发出一个Iterator。编译器将自动使用此特征来表达表单

    for item in collection {
        // body
    }

有效地将它们转换为代码,大致如下:

    let mut iter = collection.into_iter();
    loop {
        let item: Thing = match iter.next() {
            Some(item) => item,
            None => break,
        };
        // body
    }

或更简洁、更惯用:

    let mut iter = collection.into_iter();
    while let Some(item) = iter.next() {
        // body
    }

(为了保持事情顺利进行,还有适用于任何IteratorIntoIterator的实现,它只是返回self;毕竟,将Iterator转换为Iterator很容易!)

这个初始形式是一个消耗的iterator,在创建时用尽了集合:

    let collection = vec![Thing(0), Thing(1), Thing(2), Thing(3)];
    for item in collection {
        println!("Consumed item {:?}", item);
    }

任何在被遍复使用集合的尝试都失败了:

    println!("Collection = {:?}", collection);
error[E0382]: borrow of moved value: `collection`
   --> iterators/src/main.rs:156:35
    |
148 |     let collection = vec![Thing(0), Thing(1), Thing(2), Thing(3)];
    |         ---------- move occurs because `collection` has type `Vec<Thing>`, which does not implement the `Copy` trait
149 |     for item in collection {
    |                 ----------
    |                 |
    |                 `collection` moved due to this implicit call to `.into_iter()`
    |                 help: consider borrowing to avoid moving into the for loop: `&collection`
...
156 |     println!("Collection = {:?}", collection);
    |                                   ^^^^^^^^^^ value borrowed here after move
    |
note: this function takes ownership of the receiver `self`, which moves `collection`

虽然简单易懂,但这种全消耗的行为往往是不可取的;需要借用某种形式的重新项目。

为了确保行为清晰,这里的示例使用不是CopyItem类型(改进rust代码的35种具体方法-类型(五)-熟悉标准特征),因为这将隐藏所有权问题-编译器会无声地在任何地方进行复制。

    // Deliberately not `Copy`
    #[derive(Clone, Debug, Eq, PartialEq)]
    struct Thing(u64);

    let collection = vec![Thing(0), Thing(1), Thing(2), Thing(3)];

如果正在遍默的集合以&为前缀:

    for item in &collection {
        println!("{}", item.0);
    }
    println!("collection still around {:?}", collection);

然后,Rust编译器将为类型&Collection寻找IntoIterator的实现。正确设计的集合类型将提供这样的实现;该实现仍将消耗Self,但现在Self&Collection而不是Collection,并且相关的Item类型将是参考&Thing

这使得迭代后的集合完好无损,等效的扩展代码是:

    let mut iter = (&collection).into_iter();
    while let Some(item) = iter.next() {
        println!("{}", item.0);
    }

如果在可变引用2上提供迭代是有意义的,那么类似的模式适用于for item in &mut collection:编译器为&mut Collection查找并使用IntoIterator的实现,每个Item都是&mut Thing类型。

按照惯例,标准容器还提供了一个iter()方法,该方法通过对基础项的引用返回一个遍历表,并在适当的情况下等同于iter_mut()方法,其行为与刚才描述的行为相同。这些方法可用于循环,但当用作回数转换的开始时,具有更明显的好处:

    let result: u64 = (&collection).into_iter().map(|thing| thing.0).sum();

修改为:

    let result: u64 = collection.iter().map(|thing| thing.0).sum();

iterator变换

Iterator特征具有单个必需方法(next),但也提供了大量其他方法的默认实现(项目13),这些方法在遍历器上执行转换。

其中一些转换会影响整个迭代过程:

  • take(n)限制一个更多的一个它来发射n个项目。
  • skip(n)跳过遍遍位数的前n元素。
  • step_by(n)转换一个iterator,因此它只发出第n个项目。
  • chain(other)将两个遍构件粘在一起,以构建一个组合的遍构,然后穿过一个。
  • cycle()将终止的回复器转换为永远重复的回复器,每当它到达终点时,从头开始。(它必须支持Clone才能允许这样做。)
  • rev()颠倒了一个反转器的方向。(回历器必须实现DoubleEndedIterator特征,该特征具有额外的next_back必需方法。)

其他转换会影响作为Iterator主题Item的性质:

  • map(|item| {...})是最通用的版本,反复应用闭包来依次转换每个项目。此列表中的以下几个条目是方便变体,可以等效地作为map实现。
  • cloned()生成原始重复器中所有项目的克隆;这对于&Item引用上的重复器特别有用。(这显然需要底层Item类型来实现Clone)。
  • copied()生成原始遍位器中所有项的副本;这对&Item引用的位参与位器特别有用。(这显然需要底层Item类型来实现Copy)。
  • enumerate()将项目上的遍合器转换为(usize, Item)对的遍/位器,为遍/或者中的项目提供索引。
  • zip(it)将一个更像器与第二个更像器连接起来,产生一个组合的更像器,从每个原始的更像器中发出一对项目,直到两个较短的一个完成。

然而,其他转换对Iterator发出的项目进行过滤:

  • filter(|item| {...})是最通用的版本,对每个项目引用应用bool返回闭包,以确定是否应该通过。
  • take_while()skip_while()是彼此的镜像,基于谓词发出初始子范围或其初始子范围或最终子范围。

flatten()方法处理一个iterator,其项目本身就是iterators,使结果变平。就其本身而言,这似乎没有那么有帮助,但当与OptionResult都充当iterators的观察相结合时,它变得更加有用:它们产生零(forNone,Err(e))或一个(for Some(v)Ok(v))项目。这意味着flattenOption/Result值流是仅提取有效值而忽略其余值的简单方法。

总的来说,这些方法允许对一体进行转换,以便它们准确地生成大多数情况下所需的元素序列。

Iterator消费者

前两节描述了如何获得迭代器,以及如何将其转换为精确迭代的正确形式。这种精确的目标迭代可以作为显式的每个循环发生:

    let mut even_sum_squares = 0;
    for value in values.iter().filter(|x| *x % 2 == 0).take(5) {
        even_sum_squares += value * value;
    }

然而,Iterator方法的大量集合包括许多允许在单个方法调用中使用迭代的方法,消除了对显式循环的需求。

这些方法中最通用的是for_each(|item| {...})它为Iterator生成的每个项目运行闭包。这可以做显式for循环可以做的大多数事情(例外情况如下所述),但其通用性也使其使用起来有点尴尬——闭包需要使用对外部状态的可变引用才能发出任何内容:

    let mut even_sum_squares = 0;
    values
        .iter()
        .filter(|x| *x % 2 == 0)
        .take(5)
        .for_each(|value| {
            // closure needs a mutable reference to state elsewhere
            even_sum_squares += value * value;
        });

然而,如果for循环的主体与一些常见模式之一匹配,那么有更具体的更清晰、更短、更惯用的惯用方法。

这些模式包括从集合中构建单个值的快捷方式:

  • sum(),用于求和数值(整数或浮点数)的集合。
  • product(),用于将数值集合相乘。
  • min()max()用于查找相对于ItemOrd实现的集合的极端值(见项目5)。
  • min_by(f)max_by(f)用于查找集合的极端值,相对于用户指定的比较函数f
  • reduce(f)是一个更通用的操作,包括以前的方法,通过在每个步骤运行一个闭包来构建Item类型的累积值,该闭包将迄今为止的累积值和当前项目。
  • fold(f)reduce的推广,允许“累积值”为任意类型(不仅仅是Iterator::Item类型)。
  • scan(f)以略微不同的方式推广,使闭包在每一步都对一些内部状态进行可变引用。

还有从集合中选择单个值的方法:

  • find(p)找到满足谓词的第一个项目。
  • position(p)也找到满足谓词的第一个项目,但这次它返回该项目的索引。
  • nth(n)返回回复器的第n个元素(如果可用)。

有一些方法可以对集合中的每个项目进行测试:

  • any(p)指示谓词对集合中的任何项目是否为true
  • all(p)指示谓词对集合中的所有项目是否为true

(无论哪种情况,如果发现相关的反例,迭代都将提前终止。)

有一些方法允许在每个项目使用的闭包中出现故障的可能性;在每种情况下,如果闭包返回一个项目的故障,则迭代被终止,整个操作返回第一个故障。

最后,有一些方法将所有经过的累进项目累积到一个新的集合中。其中最重要的是collect()它可用于构建实现FromIterator特征的任何集合类型的新实例。

FromIterator特征是针对所有标准库集合类型(VecHashMapBTreeSet等)实现的,但这种普遍性也意味着您经常必须使用显式类型,否则编译器无法确定您是试图组装(例如)Vec<i32>还是HashSet<i32>

    use std::collections::HashSet;

    // Build collections of even numbers.  Type must be specified, because
    // the expression is the same for either type.
    let myvec: Vec<i32> = (0..10).into_iter().filter(|x| x % 2 == 0).collect();
    let h: HashSet<i32> = (0..10).into_iter().filter(|x| x % 2 == 0).collect();

(顺便说一句,这个例子还说明了使用范围表达式来生成要遍及的初始数据。)

其他(更晦涩的)收藏生产方法包括:

  • unzip(),它将成对的iterator分为两个集合。
  • partition(p),它根据应用于每个项目的谓词将一个反演符拆分为两个集合。

本项目涉及了广泛的Iterator方法选择,但这只是可用方法的一个子集;有关更多信息,请参阅Iterator文档或阅读Programming Rust(第二版)第15章,该章涵盖了广泛的可能性。

这个丰富的惯用器转换集合旨在被使用——以产生更惯用、更紧凑、意图更清晰的代码。

从Result值构建集合

上一节描述了使用collect()从重述器构建集合,但collect()在处理Result值时也有特别有用的功能。

考虑尝试将i64值的向量转换为字节(u8),乐观地期望它们都适合:

    use std::convert::TryFrom;
    let inputs: Vec<i64> = vec![0, 1, 2, 3, 4];
    let result: Vec<u8> = inputs
        .into_iter()
        .map(|v| <u8>::try_from(v).unwrap())
        .collect();

这有效,直到出现一些意想不到的输入:

    let inputs: Vec<i64> = vec![0, 1, 2, 3, 4, 512];

并导致运行时故障:

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: TryFromIntError(())', iterators/src/main.rs:249:36
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

根据改进rust代码的35种具体方法-类型(三)的建议,我们希望保持Result类型,并使用?操作员使任何故障成为调用代码的问题。发出Result的明显修改并没有真正帮助:

    let result: Vec<Result<u8, _>> =
        inputs.into_iter().map(|v| <u8>::try_from(v)).collect();
    // Now what?  Still need to iterate to extract results and detect errors.

然而,有一个替代版本的collect()它可以组装一个持有VecResult,而不是一个持有结果的Vec

强制使用此版本需要涡轮鱼(::<Result<Vec<_>, _>>):

    let result: Vec<u8> = inputs
        .into_iter()
        .map(|v| <u8>::try_from(v))
        .collect::<Result<Vec<_>, _>>()?;

将此与问号运算符相结合会产生有用的行为:

  • 如果迭代遇到错误值,该错误值将发送到调用者,迭代停止。
  • 如果没有遇到错误,代码的其余部分可以处理正确类型的合理值集合。

循环转换

此项目的目的是说服您,许多显式循环可以被视为转换为重子变换的东西。对于不习惯的程序员来说,这可能会感觉有些不自然,所以让我们一步一步地进行转变。

从一个非常类似C的显式循环开始,将向量前五个偶数项的平方相加:

// C code

    let mut even_sum_squares = 0;
    let mut even_count = 0;
    for i in 0..values.len() {
        if values[i] % 2 != 0 {
            continue;
        }
        even_sum_squares += values[i] * values[i];
        even_count += 1;
        if even_count == 5 {
            break;
        }
    }

第一步是用在for-al循环中直接使用一个回率来替换向量索引:

    let mut even_sum_squares = 0;
    let mut even_count = 0;
    for value in values.iter() {
        if value % 2 != 0 {
            continue;
        }
        even_sum_squares += value * value;
        even_count += 1;
        if even_count == 5 {
            break;
        }
    }

使用continue跳过某些项目的循环初始臂自然表示为afilterfilter()

    let mut even_sum_squares = 0;
    let mut even_count = 0;
    for value in values.iter().filter(|x| *x % 2 == 0) {
        even_sum_squares += value * value;
        even_count += 1;
        if even_count == 5 {
            break;
        }
    }

接下来,一旦发现5个偶数项目,就提前退出循环,映射到take(5)

    let mut even_sum_squares = 0;
    for value in values.iter().filter(|x| *x % 2 == 0).take(5) {
        even_sum_squares += value * value;
    }

项目的值从不直接使用,仅用于value * value组合,这使其成为map()的理想目标:

    let mut even_sum_squares = 0;
    for val_sqr in values.iter().filter(|x| *x % 2 == 0).take(5).map(|x| x * x)
    {
        even_sum_squares += val_sqr;
    }

原始循环的这些重构产生了一个循环体,这是适合在sum()方法:

    let even_sum_squares: u64 = values
        .iter()
        .filter(|x| *x % 2 == 0)
        .take(5)
        .map(|x| x * x)
        .sum();

更好解决方式

本项目强调了重转换的优点,特别是在简洁和清晰方面。那么,什么时候惯用器变换合适或惯用?

  • 如果循环体是大和/或多功能的,那么将其保留为显式体而不是将其挤压到闭包是有意义的。
  • 如果循环主体涉及导致周围函数提前终止的错误条件,这些通常最好保持明确性——try_..()方法只会有一点帮助。然而,collect()Result值的集合转换为持有值集合Result的能力通常允许仍然用?处理错误条件。操作员。
  • 如果性能至关重要,则涉及闭包的重现器转换应该得到优化,使其与等效的显式代码一样快。但是,如果核心循环的性能如此重要,请测量不同的变体并进行适当调整。
    • 小心确保您的测量反映现实世界的性能——编译器的优化器可以对测试数据给出过于乐观的结果(如第30项所述)。
    • Godbolt编译器浏览器是探索编译器吐出内容的惊人工具。

最重要的是,如果转换是被迫的或尴尬的,不要将循环转换为迭代转换。这是一个品味问题,可以肯定的是——但请注意,随着您越来越熟悉功能风格,您的品味可能会发生变化。


1:事实上,它可以更通用——在完成之前发出下一个项目的想法不需要与容器相关联。

2:如果项目突变可能会使容器的内部保证无效,则无法提供此方法。例如,以改变其Hash值的方式更改项目的内容将使HashMap的内部数据结构无效。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要配置rust-analyzer的代码提示,你可以按照以下步骤进行操作: 1. 确保已经在你的项目中安装了rust-analyzer插件。你可以在VSCode的插件市场中搜索并安装"rust-analyzer"插件。 2. 打开VSCode的设置(可以通过菜单栏的"文件" -> "首选项" -> "设置"或者使用快捷键Ctrl + ,打开设置)。 3. 在设置页面的搜索框中输入"rust-analyzer",找到相关的设置选项。 4. 根据你的需求,配置下列常用的代码提示相关的设置: - "rust-analyzer.enable": 设置为true以启用rust-analyzer插件。 - "rust-analyzer.completion.enable": 设置为true以启用代码补全功能。 - "rust-analyzer.completion.addCallArgumentSnippets": 设置为true以自动添加函数调用时的参数提示。 - "rust-analyzer.completion.addCallParenthesis": 设置为true以自动添加函数调用时的括号。 - "rust-analyzer.completion.postfix.enable": 设置为true以启用后缀代码补全功能,例如`.if`、`.let`等。 - "rust-analyzer.hover.enable": 设置为true以启用悬停提示功能。 - "rust-analyzer.inlayHints.enable": 设置为true以启用内联提示功能。 5. 根据你的需求,可以进一步自定义配置rust-analyzer的代码提示行为。你可以在设置中找到更多相关的选项,并根据注释进行配置。 6. 保存设置,并重启VSCode使更改生效。 通过以上步骤,你可以根据自己的喜好和需求来配置rust-analyzer的代码提示功能。请注意,具体的配置选项可能会因rust-analyzer插件版本的不同而有所差异,请参考插件的官方文档或参考其它资源获取更多定制化的配置信息。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值