前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Rust 关联常量,泛型结构体,内部可变性

Rust 关联常量,泛型结构体,内部可变性

作者头像
草帽lufei
发布2024-05-08 15:27:23
1050
发布2024-05-08 15:27:23
举报
文章被收录于专栏:程序语言交流程序语言交流

9.6 关联常量

Rust 在其类型系统中的另一个特性也采用了类似于 C# 和 Java 的思想,有些值是与类型而不是该类型的特定实例关联起来的。在 Rust 中,这些叫作关联常量

顾名思义,关联常量是常量值。它们通常用于表示指定类型下的常用值。例如,你可以定义一个用于线性代数的二维向量和一个关联的单位向量:

代码语言:javascript
复制
pub struct Vector2 {
    x: f32,
    y: f32,
}

impl Vector2 {
    const ZERO: Vector2 = Vector2 { x: 0.0, y: 0.0 };
    const UNIT: Vector2 = Vector2 { x: 1.0, y: 0.0 };
}

这些值是和类型本身相关联的,你可以在不必引用 Vector2 的任一实例的情况下使用它们。这与关联函数非常相似,使用的名字是与其关联的类型名,后面跟着它们自己的名字:

代码语言:javascript
复制
let scaled = Vector2::UNIT.scaled_by(2.0);

关联常量的类型不必是其所关联的类型,我们可以使用此特性为类型添加 ID 或名称。如果有多种类似于 Vector2 的类型需要写入文件然后加载到内存中,则可以使用关联常量来添加名称或数值 ID,这些名称或数值 ID 可以写在数据旁边以标识其类型。

代码语言:javascript
复制
impl Vector2 {
    const NAME: &'static str = "Vector2";
    const ID: u32 = 18;
}

笔记 在 impl 中定义常量,可以直接使用 :: 的方式使用

9.7 泛型结构体

前面对 Queue 的定义并不令人满意:它是为存储字符而写的,但是它的结构体或方法根本没有任何专门针对字符的内容。如果我们要定义另一个包含 String 值的结构体,那么除了将 char 替换为 String 外,其余代码可以完全相同。这纯属浪费时间。

幸运的是,Rust 结构体可以是泛型的,这意味着它们的定义是一个模板,你可以在其中插入任何自己喜欢的类型。例如,下面是 Queue 的定义,它可以保存任意类型的值:

代码语言:javascript
复制
pub struct Queue<T> {
    older: Vec<T>,
    younger: Vec<T>
}

你可以把 Queue<T> 中的 <T> 读作“对于任意元素类型 T……”。所以上面的定义可以这样解读:“对于任意元素类型 TQueue<T> 有两个 Vec<T> 类型的字段。”例如,在 Queue<String> 中,TString,所以 olderyounger 的类型都是 Vec<String>。而在 Queue<char> 中,Tchar,我们最终得到的结构体与最初那个针对 char 定义的结构体是一样的。事实上,Vec 本身也是一个泛型结构体,它就是这样定义的。

在泛型结构体定义中,尖括号(<>)中的类型名称叫作类型参数。泛型结构体的 impl 块如下所示:

代码语言:javascript
复制
impl<T> Queue<T> {
    pub fn new() -> Queue<T> {
        Queue { older: Vec::new(), younger: Vec::new() }
    }

    pub fn push(&mut self, t: T) {
        self.younger.push(t);
    }

    pub fn is_empty(&self) -> bool {
        self.older.is_empty() && self.younger.is_empty()
    }

    ...
}

你可以将 impl<T> Queue<T> 这一行解读为“对于任意元素类型 T,这里有一些在 Queue<T> 上可用的关联函数。”然后,你可以使用类型参数 T 作为关联函数定义中的类型。

语法可能看起来有点儿累赘,但 impl<T> 可以清楚地表明 impl 块能涵盖任意类型 T,这便能将它与为某种特定类型的 Queue 编写的 impl 块区分开来,如下所示:

代码语言:javascript
复制
impl Queue<f64> {
    fn sum(&self) -> f64 {
        ...
    }
}

这个 impl 块标头表明“这里有一些专门用于 Queue<f64> 的关联函数”。这为 Queue<f64> 提供了一个 sum 方法,不过该方法在其他类型的 Queue 上不可用。

我们在前面的代码中使用了 Rust 的 self 参数简写形式,如果到处都写成 Queue<T>,则让人觉得拗口且容易分心。作为另一种简写形式,每个 impl 块,无论是不是泛型,都会将特殊类型的参数 Self(注意这里是大驼峰 CamelCase)定义为我们要为其添加方法的任意类型。对前面的代码来说,Self 就应该是 Queue<T>,因此我们可以进一步缩写 Queue::new 的定义:

代码语言:javascript
复制
pub fn new() -> Self {
    Queue { older: Vec::new(), younger: Vec::new() }
}

你可能注意到了,在 new 的函数体中,不需要在构造表达式中写入类型参数,简单地写 Queue { ... } 就足够了。这是 Rust 的类型推断在起作用:由于只有一种类型适用于该函数的返回值(Queue<T>),因此 Rust 为我们补齐了该类型参数。但是,你始终都要在函数签名和类型定义中提供类型参数。Rust 不会推断这些,相反,它会以这些显式类型为基础,推断函数体内的类型。

Self 也可以这样使用,我们可以改写成 Self { ... }。你觉得哪种写法最容易理解就写成哪种。

在调用关联函数时,可以使用 ::<>(比目鱼)表示法显式地提供类型参数:

代码语言:javascript
复制
let mut q = Queue::<char>::new();

但实际上,通常可以让 Rust 帮你推断出来:

代码语言:javascript
复制
let mut q = Queue::new();
let mut r = Queue::new();

q.push("CAD");  // 显然是Queue<&'static str>
r.push(0.74);   // 显然是Queue<f64>

q.push("BTC");   // 2019年6月一比特币值多少美元
r.push(13764.0); // Rust可没能力检测出非理性繁荣

事实上,我们在本书中经常这样使用另一种泛型结构体类型 Vec

不仅结构体可以是泛型的,枚举同样可以接受类型参数,而且语法也非常相似。10.1 节会详细介绍“枚举”。

笔记 在实战中似乎会经常使用泛型结构体

9.8 带生命周期参数的泛型结构体

正如我们在 5.3.5 节中讨论的那样,如果结构体类型包含引用,则必须为这些引用的生命周期命名。例如,下面这个结构体可能包含对某个切片的最大元素和最小元素的引用:

代码语言:javascript
复制
struct Extrema<'elt> {
    greatest: &'elt i32,
    least: &'elt i32
}

早些时候,我们建议你把像 struct Queue<T> 这样的声明理解为:给定任意类型 T,都可以创建一个持有该类型的 Queue<T>。同样,可以将 struct Extrema<'elt> 理解为:给定任意生命周期 'elt,都可以创建一个 Extrema<'elt> 来持有对该生命周期的引用。

下面这个函数会扫描切片并返回一个 Extrema 值,这个值的各个字段会引用其中的元素:

代码语言:javascript
复制
fn find_extrema<'s>(slice: &'s [i32]) -> Extrema<'s> {
    let mut greatest = &slice[0];
    let mut least = &slice[0];

    for i in 1..slice.len() {
        if slice[i] < *least    { least    = &slice[i]; }
        if slice[i] > *greatest { greatest = &slice[i]; }
    }
    Extrema { greatest, least }
}

在这里,由于 find_extrema 借用了 slice 的元素,而 slice 有生命周期 's,因此我们返回的 Extrema 结构体也使用了 's 作为其引用的生命周期。Rust 总会为各种调用推断其生命周期参数,所以调用 find_extrema 时不需要提及它们:

代码语言:javascript
复制
let a = [0, -3, 0, 15, 48];
let e = find_extrema(&a);
assert_eq!(*e.least, -3);
assert_eq!(*e.greatest, 48);

因为返回类型的生命周期与参数的生命周期相同是很常见的情况,所以如果有一个显而易见的候选者,那么 Rust 就允许我们省略生命周期。因此也可以把 find_extrema 的签名写成如下形式,意思不变:

代码语言:javascript
复制
fn find_extrema(slice: &[i32]) -> Extrema {
    ...
}

当然,我们的意思也可能Extrema<'static>,但这很不寻常。Rust 只为最常见的情况提供了简写形式。

9.9 带常量参数的泛型结构体

泛型结构体也可以接受常量值作为参数。例如,你可以定义一个表示任意次数多项式的类型,如下所示:

代码语言:javascript
复制
/// N - 1次多项式
struct Polynomial<const N: usize> {
    /// 多项式的系数
    ///
    /// 对于多项式a + bx + cx2 + ... + zxn-1,其第`i`个元素是xi的系数
    coefficients: [f64; N]
}

例如,根据这个定义,Polynomial<3> 是一个二次多项式。这里的 <const N: usize> 子句表示 Polynomial 类型需要一个 usize 值作为它的泛型参数,以此来决定要存储多少个系数。

与通过字段保存长度和容量而将元素存储在堆中的 Vec 不同,Polynomial 会将其系数(coefficients)直接存储在值中,再无其他字段。长度直接由类型给出。(这里不需要容量的概念,因为 Polynomial 不能动态增长。)

也可以在类型的关联函数中使用参数 N

代码语言:javascript
复制
impl<const N: usize> Polynomial<N> {
    fn new(coefficients: [f64; N]) -> Polynomial<N> {
        Polynomial { coefficients }
    }

    /// 计算`x`处的多项式的值
    fn eval(&self, x: f64) -> f64 {
        // 秦九韶算法在数值计算上稳定、高效且简单:
        // c0 + x(c1 + x(c2 + x(c3 + ... x(c[n-1] + x c[n]))))
        let mut sum = 0.0;
        for i in (0..N).rev() {
            sum = self.coefficients[i] + x * sum;
        }

        sum
    }
}

这里,new 函数会接受一个长度为 N 的数组,并将其元素作为新 Polynomial 值的系数。eval 方法将在 0..N 范围内迭代以找到给定点 x 处的多项式值。

与类型参数和生命周期参数一样,Rust 通常也能为常量参数推断出正确的值:

代码语言:javascript
复制
use std::f64::consts::FRAC_PI_2;   // π/2

// 用近似法对`sin`函数求值:sin x ? x - 1/6 x? + 1/120 x5
// 误差几乎为0,相当精确!
let sine_poly = Polynomial::new([0.0, 1.0, 0.0, -1.0/6.0, 0.0,
                                 1.0/120.0]);
assert_eq!(sine_poly.eval(0.0), 0.0);
assert!((sine_poly.eval(FRAC_PI_2) - 1.).abs() < 0.005);

由于我们向 Polynomial::new 传递了一个包含 6 个元素的数组,因此 Rust 知道必须构造出一个 Polynomial<6>eval 方法仅通过查询其 Self 类型就知道 for 循环应该运行多少次迭代。由于长度在编译期是已知的,因此编译器可能会用一些顺序执行的代码完全替换循环。

常量泛型参数可以是任意整数类型、charbool。不允许使用浮点数、枚举和其他类型。

如果结构体还接受其他种类的泛型参数,则生命周期参数必须排在第一位,然后是类型,接下来是任何 const 值。例如,一个包含引用数组的类型可以这样声明:

代码语言:javascript
复制
struct LumpOfReferences<'a, T, const N: usize> {
    the_lump: [&'a T; N]
}

常量泛型参数是 Rust 的一个相对较新的功能,目前它们的使用受到了一定的限制。例如,像下面这样定义 Polynomial 显然更好:

代码语言:javascript
复制
/// 一个N次多项式
struct Polynomial<const N: usize> {
    coefficients: [f64; N + 1]
}

然而,Rust 会拒绝这个定义:

代码语言:javascript
复制
error: generic parameters may not be used in const operations
  |
6 |     coefficients: [f64; N + 1]
  |                         ^ cannot perform const operation using `N`
  |
  = help: const parameters may only be used as standalone arguments, i.e. `N`

虽然 [f64; N] 没问题,但像 [f64; N + 1] 这样的类型显然对 Rust 来说太过激进了。所以 Rust 暂时施加了这个限制,以避免遇到像下面这样的问题:

代码语言:javascript
复制
struct Ketchup<const N: usize> {
    tomayto: [i32; N & !31],
    tomahto: [i32; N - (N % 32)],
}

通过计算可知,不管 N 取何值,N & !31N - (N % 32) 总是相等的,因此 tomaytotomahto 始终具有相同的类型。例如,应该允许将任何一个赋值给另一个。但是,如果想让 Rust 的类型检查器识别这种位运算,就需要把一些令人困惑的极端情况引入这种本已相当复杂的语言中,而这会带来复杂度失控的风险。当然,支持像 N + 1 这样的简单表达式是没问题的,并且也确实已经有人在努力教 Rust 顺利处理这些问题。

由于此处关注的是类型检查器的行为,因此这种限制仅适用于出现在类型中的常量参数,比如数组的长度。在普通表达式中,可以随意使用 N:像 N + 1N & !31 这样的写法是完全可以的。

如果要为 const 泛型参数提供的值不仅仅是字面量或单个标识符,那么就必须将其括在花括号中,就像 Polynomial<{5 + 1}> 这样。此规则能让 Rust 更准确地报告语法错误。

9.10 让结构体类型派生自某些公共特型

结构体很容易编写:

代码语言:javascript
复制
struct Point {
    x: f64,
    y: f64
}

但是,如果你要开始使用这种 Point 类型,很快就会发现它有点儿难用。像这样写的话,Point 不可复制或克隆,不能用 println!("{:?}", point); 打印,而且不支持 == 运算符和 != 运算符。

这些特性中的每一个在 Rust 中都有名称——CopyCloneDebugPartialEq,它们被称为特型。第 11 章会展示如何为自己的结构体手动实现特型。但是对于这些标准特型和其他一些特型,无须手动实现,除非你想要某种自定义行为。Rust 可以自动为你实现它们,而且结果准确无误。只需将 #[derive] 属性添加到结构体上即可:

代码语言:javascript
复制
#[derive(Copy, Clone, Debug, PartialEq)]
struct Point {
    x: f64,
    y: f64
}

这些特型中的每一个都可以为结构体自动实现特型,但前提是结构体的每个字段都实现了该特型。我们可以要求 Rust 为 Point 派生 PartialEq,因为它的两个字段都是 f64 类型,而 f64 类型已经实现了 PartialEq

Rust 还可以派生 PartialOrd,这将增加对比较运算符 <><=>= 的支持。我们在这里并没有这样做,因为比较两个点以了解一个点是否“小于”另一个点是一件很奇怪的事情。毕竟点和点之间并没有任何常规意义上的顺序可言。所以我们选择不让 Point 值支持这些运算符。这种特例就是 Rust 让我们自己编写 #[derive] 属性而不会自动为它派生每一个可能特型的原因之一。而另一个原因是,只要实现某个特型就会自动让它成为公共特性,因此可复制性、可克隆性等都会成为该结构体的公共 API 的一部分,应该慎重选择。

第 13 章会详细描述 Rust 的标准特型并解释哪些可用于 #[derive]

9.11 内部可变性

可变性与其他任何事物一样:过犹不及,而你通常只需要一点点就够了。假设你的蜘蛛机器人控制系统有一个中心结构体 SpiderRobot,其中包含一些设置和 I/O 句柄。该结构体会在机器人启动时设置好,并且值永不改变:

代码语言:javascript
复制
pub struct SpiderRobot {
    species: String,
    web_enabled: bool,
    leg_devices: [fd::FileDesc; 8],
    ...
}

机器人的每个主要系统由不同的结构体处理,它们都有一个指向 SpiderRobot 的指针:

代码语言:javascript
复制
use std::rc::Rc;

pub struct SpiderSenses {
    robot: Rc<SpiderRobot>,  // <--指向设置和I/O的指针
    eyes: [Camera; 32],
    motion: Accelerometer,
    ...
}

织网、捕食、毒液流量控制等结构体也都有一个 Rc<SpiderRobot> 智能指针。回想一下,Rc 代表引用计数(reference counting),并且 Rc 指向的值始终是共享的,因此将始终不可变。

现在假设你要使用标准 File 类型向 SpiderRobot 结构体添加一点儿日志记录。但有一个问题:File 必须是可变的。所有用于写入的方法都需要一个可变引用。

这种情况经常发生。我们需要一个不可变值(SpiderRobot 结构体)中的一丁点儿可变数据(一个 File)。这称为内部可变性。Rust 提供了多种可选方案,本节将讨论两种最直观的类型,即 Cell<T>RefCell<T>,它们都在 std::cell 模块中。1

1cell 意思是“隔离室、单元格”,引申为“细胞”。——译者注

Cell<T> 是一个包含类型 T 的单个私有值的结构体。Cell 唯一的特殊之处在于,即使你对 Cell 本身没有 mut 访问权限,也可以获取和设置这个私有值字段。

Cell::new(value)(新建)

创建一个新的 Cell,将给定的 value 移动进去。

cell.get()(获取)

返回 cell 中值的副本。

cell.set(value)(设置)

将给定的 value 存储在 cell 中,丢弃先前存储的值。

此方法接受一个不可变引用型的 self

代码语言:javascript
复制
fn set(&self, value: T)    // 注意:不是`&mut self`

当然,这对名为 set 的方法来说是相当不寻常的。迄今为止,Rust 一直在告诉我们如果想更改数据,就需要 mut 型访问。但出于同样的原因,这个不寻常的细节正是 Cell 的全部意义所在。Cell 只是改变不变性规则的一种安全方式——一丝不多,一毫不少。

cell 还有其他一些方法,你可以查阅其文档进行了解。

如果你想在 SpiderRobot 中添加一个简单的计数器,那么 Cell 是一个不错的工具。可以写成如下形式:

代码语言:javascript
复制
use std::cell::Cell;

pub struct SpiderRobot {
    ...
    hardware_error_count: Cell<u32>,
    ...
}

然后,即使 SpiderRobot 中的非 mut 方法也可以使用 .get() 方法和 .set() 方法访问 u32

代码语言:javascript
复制
impl SpiderRobot {
    /// 把错误计数递增1
    pub fn add_hardware_error(&self) {
        let n = self.hardware_error_count.get();
        self.hardware_error_count.set(n + 1);
    }

    /// 如果报告过任何硬件错误,则为true
    pub fn has_hardware_errors(&self) -> bool {
        self.hardware_error_count.get() > 0
    }
}

这很容易,但它无法解决我们的日志记录问题。Cell 不允许在共享值上调用 mut 方法。.get() 方法会返回 Cell 中值的副本,因此它仅在 T 实现了 Copy 特型时才有效。对于日志记录,我们需要一个可变的 File,但 File 不是 Copy 类型。

在这种情况下,正确的工具是 RefCell。与 Cell<T> 一样,RefCell<T> 也是一种泛型类型,它包含类型 T 的单个值。但与 Cell 不同,RefCell 支持借用对其 T 值的引用。

RefCell::new(value)(新建)

创建一个新的 RefCell,将 value 移动进去。

ref_cell.borrow()(借用)

返回一个 Ref<T>,它本质上只是对存储在 ref_cell 中值的共享引用。

如果该值已被以可变的方式借出,则此方法会 panic,详细信息稍后会解释。

ref_cell.borrow_mut()(可变借用)

返回一个 RefMut<T>,它本质上是对 ref_cell 中值的可变引用。

如果该值已被借出,则此方法会 panic,详细信息稍后会解释。

ref_cell.try_borrow()(尝试借用)和 ref_cell.try_borrow_mut()(尝试可变借用)

行为与 borrow()borrow_mut() 一样,但会返回一个 Result。如果该值已被以可变的方式借出,那么这两个方法不会 panic,而是返回一个 Err 值。

同样,RefCell 也有一些其他的方法,你可以在其文档中进行查找。

仅当你试图打破“可变引用必须独占”的 Rust 规则时,这两个 borrow 方法才会 panic。例如,以下代码会引起 panic:

代码语言:javascript
复制
use std::cell::RefCell;

let ref_cell: RefCell<String> = RefCell::new("hello".to_string());

let r = ref_cell.borrow();      // 正确,返回Ref<String>
let count = r.len();            // 正确,返回"hello".len()
assert_eq!(count, 5);

let mut w = ref_cell.borrow_mut();  // panic:已被借出
w.push_str(" world");

为避免 panic,可以将这两个借用放入不同的块中。这样,在你尝试借用 w 之前,r 已经被丢弃了。

这很像普通引用的工作方式。唯一的区别是,通常情况下,当你借用一个变量的引用时,Rust 会在编译期进行检查,以确保你在安全地使用该引用。如果检查失败,则会出现编译错误。RefCell 会使用运行期检查强制执行相同的规则。因此,如果你违反了规则,就会收到 panic(对于 try_borrowtry_borrow_mut 则会显示 Err)。

现在我们已经准备好把 RefCell 用在 SpiderRobot 类型中了:

代码语言:javascript
复制
pub struct SpiderRobot {
    ...
    log_file: RefCell<File>,
    ...
}

impl SpiderRobot {
    /// 往日志文件中写一行消息
    pub fn log(&self, message: &str) {
        let mut file = self.log_file.borrow_mut();
        // `writeln!`很像`println!`,但会把输出发送到给定的文件中
        writeln!(file, "{}", message).unwrap();
    }
}

变量 file 的类型为 RefMut<File>,我们可以像使用 File 的可变引用一样使用它。有关写入文件的详细信息,请参阅第 18 章。

Cell 很容易使用。虽然不得不调用 .get().set().borrow().borrow_mut() 略显尴尬,但这就是我们为违反规则而付出的代价。还有一个缺点虽不太明显但更严重:Cell 以及包含它的任意类型都不是线程安全的。因此 Rust 不允许多个线程同时访问它们。第 19 章会讲解内部可变性的线程安全风格,届时我们会讨论“Mutex<T>”(参见 19.3.2 节)、“原子化类型”(参见 19.3.10 节)和“全局变量”(参见 19.3.11 节)这几项技术。

无论一个结构体是具名字段型的还是元组型的,它都是其他值的聚合:如果我有一个 SpiderSenses 结构体,那么就有了指向共享 SpiderRobot 结构体的 Rc 指针、有了眼睛、有了陀螺仪,等等。所以结构体的本质是“和”这个字:我有 X 和 Y。但是如果围绕“或”这个字构建另一种类型呢?也就是说,当你拥有这种类型的值时,你就拥有了 X 或 Y。这种类型也非常有用,在 Rust 中无处不在,它们是第 10 章的主题。

笔记 借用,引用 理解了一点点,但是还没能彻底明白,章节中的泛型结构体相关也看的有点点蒙圈,这部分看来需要在实战中去强化理解

欢迎大家讨论交流,如果喜欢本文章或感觉文章有用,动动你那发财的小手点赞、收藏、关注再走呗 ^_^

本文参与?腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2024-04-23,如有侵权请联系?cloudcommunity@tencent.com 删除

本文分享自 草帽Lufei 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 9.6 关联常量
  • 9.7 泛型结构体
  • 9.8 带生命周期参数的泛型结构体
  • 9.9 带常量参数的泛型结构体
  • 9.10 让结构体类型派生自某些公共特型
  • 9.11 内部可变性
相关产品与服务
对象存储
对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
http://www.vxiaotou.com