初学rust——Iterators、Closures、Crates.io and Pointers

这篇博客详细介绍了Rust编程语言中的关键概念,包括迭代器、闭包的使用,Cargo配置和Crates.io发布流程,以及智能指针的类型和作用。学习了闭包的捕获环境特性,迭代器的懒评估和性能优势,以及如何通过Cargo定制构建、发布crate。此外,还探讨了Box、Rc和Arc等智能指针在堆内存管理和内存安全方面的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

学习rust的第五、六天,学习资料是官网的《The Rust Programming Language》,本笔记的内容包括第13-15章的内容

Chapter 13 Functional Language Features: Iterators and Closure

本章主要内容:

  • Closure,function-like的结构,可以存储在变量中
  • Iterators,处理一系列元素的方法
  • 如何利用以上两种特性来增强上一章的代码
  • 以上两种特性的性能分析

13.1 Closures: Anonymous Functions that Can Capture Their Environment

目标:代码在程序中仅定义一次,同时仅在需要时调用一次
closure的声明使用let语句,声明表示这个closure包含一个匿名函数的定义,不代表其包含一个匿名函数的运行结果。
closure不要求声明参数的类型,或返回值的类型,这一点与函数是不同的。由于函数作为显式的用户接口,必须声明其参数与返回值的类型,作为所有使用这个函数的成员的“共识”。但是闭包不同,闭包不作为暴露的接口,仅为一个匿名函数,存储在变量中,因而不需要做类型声明。

closure的类型是编译器推测的,但是仅能用作一种类型。

所有的闭包至少实现以下之一的trait:
Fn, FnMut, FnOnce

闭包具有的函数所没有的性质:capture their environment and access variables from the scope in which they’re defined.

fn main() {
    let x = 4;

    let equal_to_x = |z| z == x;  //注意此处,我们并没有把x作为参数传递给闭包,但是它依然可以访问x的值,因为他们定一下相同的scope下

    let y = 4;

    assert!(equal_to_x(y));
}

closure从环境中抓取值的方式有三种,这三种方式分别对应着函数获得变量的三种方法:taking ownership、borrowing mutably和borrowing immutably。他们被编码进了三种Fn trait:

  1. FnOnce 从其scope中获取变量,并得到变量的ownership,将其move到closure中。Once表示闭包无法超过一次获得同个变量的ownership
  2. FnMutmutably borrow,从而可以改变环境
  3. Fnimmutably borrow

13.2 Processing a Series of Items with Iterators

所有的Iterator都具有trait:Iterator,在标准库中定义:

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
// methods with default implementations elided
}

定义一个iterator的关键在于实现next函数
注意rust中的iterator是“lazy”的,什么也不做,除非被调用。

13.3 Improving Our I/O Project

本节内容见代码Chapter12

13.4 Comparing Performance: Loops vs. Iterators

本节内容仅给出结论:Iterator的速度很快,比loop的效率高,可以放心大胆地使用~
zero-cost abstraction

Chapter 14, More About Cargo and Crates.io

本章的内容可以帮助程序媛做到:

  • Customize your build through released profile
  • 在crates.io发布libraries
  • 使用workshop组织大规模的项目
  • 从crates.io安装binaries
  • 使用custom commands扩展Cargo

14.1 Customizing Builds

Cargo具有两个主要文件:dev:在运行cargo run时使用;release:在运行cargo build --release。

当Cargo.toml文件中没有指定[profile.*]时,cargo对每个文件都有默认的设置。通过加入[profile.*]可以重写默认值:

[profile.dev]  
opt-level = 0 //规定了rust对代码进行优化的次数

[profile.release]
opt-level = 3

14.2 Publishing a Crate to Crates.io

(1)使用有效的文档注释,方便其他人快速了解代码

Documentation comment: ///该文档注释功能能够进行HTML build,命令是cargo doc,使用这个功能还可以在运行cargo test的时候自动运行注释文档中的代码,作为test~

//! 注释的内容一般作为crates和modules的解释

(2) 利用pub use导出public API

当代码具有很复杂的结构时,其他人难以使用其中的某个模块,导入也相对复杂。
可以重新导出某个模块,使得其他人可以直接使用,而不必搞清楚整个代码的逻辑架构,一层一层找到这个模块

(3) 设置一个Crates.io Account

在crates.io的主页通过github账号登录获取账户和API token

(4) 向新的Crate加入metadata

在发布crate之前,需要向Cargo.toml文件中加入[packages] section
Crates.io上的crates的命名具有唯一性,不可以与已存在的重复。
description和license也是必须要求的内容,同样写在Cargo.toml中

(5) 发布

注意发布是永久的,已经发布不能删除,可以更新版本。
可以拒绝其他项目将发布的crates作为依赖。cargo yank命令

14.3 Cargo Workspaces

Workspaces可以帮助我们管理多个相关的packages

Workspace 是一组共用同一个Cargo.lock的packages,因而其中所有的crates会共用所有的依赖,当发生更新时也是同步更新。

14.4 Installing Binaries from Crates.io with cargo install

注意只能安装具有binary targets的packages

14.5 Extending Cargo with Custom Commands

Chapter 15, Smart Pointers

学到这我才发现之前学的reference不完全等价于指针…真是一个尴尬的事情…

pointers,指针,包含内存中的一个地址。这个地址指向另一个数据。Rust中最常见的pointer之一就是之前提到的reference,由符号&来表示。
smart pointers,是一种数据结构,不仅具有指针的能力,而且还有metadata和其他的能力。smart pointers与reference的主要区别之一就是reference只borrow数据,而smart pointers拥有数据。前文中提到过的String、Vec都是smart pointers的例子。

Smart pointers一般使用结构体来实现,一个区分smart pointers和普通结构体的主要性质就是smart pointers实现DerefDroptraits。其中Deref允许一个sp的实例拥有reference的功能,Drop能够帮助程序媛在sp超出scope后去customize代码。

本章会涉及rust中最常见的标准库中的sp:

  • Box<T> 将数据分配到堆上
  • Rc<T> reference counting type,enable多个ownership
  • Ref<T>RefMut<T>accessed through RefCall<T>,将borrowing规则施加在运行时而不是编译时。

除此之外还会涉及interior mutabilityreference cycles

15.1 Using Box<T>to Point to Data on the Heap

Box是最为直接的sp,写作Box。Boxes允许程序媛在堆上存储数据而非栈,栈中唯一存放的是指向堆的指针。在以下情况中经常用到Boxes:

  • 当有一个类型在编译时不能确定大小,但是想要使用这个类型时需要一个明确的大小。
  • 当有很多数据想要转换ownership,但是想要保证在这个过程中不会发生copy。
  • 当想要own一个值,只关心它实现了特定的trait,而不太关心这个值的具体类型时
fn main() { 
    let b = Box::new(5);   //定义
    println!("b = {}", b);
}

注意在Box out of scope时,他会同时释放栈中存放的指向数据堆的指针,和数据堆。

Enabling Recursive Types with Boxes

在编译时不知道具体大小的类型称为recursive type(递归类型),其中一个值可以作为其自身的一部分拥有同一类型的另一个值,由于在理论上这个递归可以无穷进行下去,所以rust无法确定这个类型的大小。
举一个栗子:cons list。cons list是一个来自Lisp编程语言和衍生的数据结构,在Lisp中,cons function是construct function的简称,从它的两个arguments中构建一个新的pair。两个arguments通常为一个单值和另一个pair(因此构成了递归),pairs构成了一个list。
在cons list中,每一个item包含两个元素:current item和next item。表中的最后一个元素只包含一个值Nil,没有next item。

15.2 Treating Smarts Pointers Like Regular Referencens with the Deref Trait

符号*:dereference operator,实现了Deref trait,sp在这种运算符下,与reference等价。

一般理解,*v 操作,是 &v 的反向操作,即试图由资源的引用获取到资源的拷贝(如果资源类型实现了 Copy),或所有权(资源类型没有实现 Copy)。

Rust中该操作符可以重载

let x = 5;
let y = &x;

assert_eq!(5, x);
assert_eq!(5, *y);

deref最为神奇的设计应属他的“deref coercion”,强制隐式转换,其规则为:
一个类型为T 的对象foo,如果 T: Deref<Target=U>,那么,相关 foo 的某个sp或引用(比如 &foo)在应用的时候会自动转换成 &U。

考虑以下代码:

fn hello(name: &str) {
    println!("Hello, {}!", name);
}
fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&m);  
}

这里hello函数的参数是一个&str,主函数调用hello,传参&m,而m是MyBox<String>的引用。如果没有deref coercoin的实现,代码应写为:

fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&(*m)[..]);
}

*m将MyBox<String>转换为String,然后&和[…]从String中取全切片

可以使用DerefMut对可变引用重写操作符*
在以下三种情况下,rust会进行强制隐式转换:

  • &T&UT: Deref<Target = U>
  • &mut T&mut UT: DerefMut<Target = U>
  • &mut T&UT: Deref<Target = U>

15.3 Running Code on Cleanup with the Drop trait

在rust中,程序媛可以指定一段特定的代码用于变量out of scope后进行内存释放,rust会自动在需要的时候插入这段代码。而指定这段代码其实就是实现了Drop trait。drop trait要求实现一个名为drop的方法,并动态指向self。drop释放变量的顺序与创建的顺序相反。

如果想要提前释放某一个变量,可以使用std::mem::drop

fn main() {
    let c = CustomSmartPointer { data: String::from("some data") };
    println!("CustomSmartPointer created.");
    drop(c);  //调用std::men::drop,提前释放
    println!("CustomSmartPointer dropped before the end of main.");
}

15.4 Rc<T>, the Reference Counted Smart Pointers

有些情况下,有的变量会有多个不同的所有者,比如图论中,多分边指针会指向同一个节点。Rc<T>就是用来处理这种情况的。RC为reference counting的简写,rc记录着某个值的reference的个数,来决定这个值是否依然被使用着。

enum List {
    Cons(i32, Rc<List>),  //使用rc list
    Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;  //RC需要显式声明use

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    let b = Cons(3, Rc::clone(&a));  //clone一个a的list
    let c = Cons(4, Rc::clone(&a));
}

15.5 RefCell<T> and the Interior Mutability Pattern

Interior Mutablitily允许程序媛修改数据,即便有不可变的指针指向该数据。一般来讲,这个操作是违反borrowing rules的,采用关键字unsafe可以打破这个规定。

在使用RefCell时,rust的borrowing rules会在运行时进行检查,而不再是默认的编译时,运行时一些特定的memory-safe scenarios会被允许,也就是说在编译阶段,一些代码上的问题是不能被发现并解决的。

  • Rc<T> 使得变量得以有多个owner;RefCell<T>Box<T>有单个owner
  • Box允许在编译时的immutable 或 mutable borrow检查,Rc只允许编译时的immutable检查,RecCell允许运行时的immutable或mutable检查。
  • RefCell可以允许程序媛在其内部改动变量,即便RefCell本身是immutable的

Rc<T>RefCell<T>经常组合使用(二者都是作用于单线程的)rc可以允许多owner,但必须都为immutable,refcell允许mutable ref,所以可以用rc<refcell>来实现多owner,同时对变量进行修改

15.6 Reference Cycles can leak Memory

leak memory:有些内存没有被清理释放。Rc和refcell的使用(指针相互指向对方)会导致memory leak。

use std::rc::Rc;
use std::cell::RefCell;
use crate::List::{Cons, Nil};

#[derive(Debug)]
enum List {
    Cons(i32, RefCell<Rc<List>>),  //与之前不同,将list的第二项变为可变、多owner
    Nil,
}

impl List {
    fn tail(&self) -> Option<&RefCell<Rc<List>>> {
        match self {
            Cons(_, item) => Some(item), //返回下一个
            Nil => None,
        }
    }
}

当我们使用Refcell包裹Rc时,就会容易出现循环,需要程序媛自己检查代码,rust无法帮我们做到。
另外一种防止出现reference cycle的方法:将Rc<T>转换为Weak<T>

调用Rc::downgrade,得到一个sp,类型为Weak<T>,会将weak_count加一(类似于之前的strong_count)weak_count记录有多少个Weak 指针指向这个变量,区别在于释放这个变量时weak_count不需要变为0。

weak不会带来死循环,代价是rust不能确保weak指向的值依然存在,因此需要程序媛来自行判断。使用upgrade方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值