原文链接:https://hashrust.com/blog/arrays-vectors-and-slices-in-rust/

原文标题:Arrays, vectors and slices in Rust


公众号:Rust 碎碎念


翻译: Praying

引言(Introduction)

在本文中,我将会介绍 Rust 中的 array、vector 和 slice。有 C 和 C++编程经验的程序员应该已经熟悉 array 和 vector,但因 Rust 致力于安全性(safety),所以与不安全的同类语言相比仍有一些区别。另外,slice 是一个全新且非常有用的概念。

Array

Array 是初学者最先接触的数据类型之一。一个 array 是一组相同类型的数据集合,这些数据位于连续的内存块中。例如,如果你像下面这样分配一个数组:

let array: [i32; 4] = [42, 10, 5, 2];

接着,所有的i32整数在栈上紧挨着彼此被分配:

在 Rust 中,array 的大小(size)是类型的一部分。例如,下面的代码将无法编译:

//error:expected an array with a fixed size of 4 elements,
//found one with 3 elements
let array: [i32; 4] = [0, 1, 2];

Rust 的严谨性避免了像 C/C++中的数组到指针的衰变问题:

//C++ code
#include <iostream>

using namespace std;

//Looks can be deceiving: arr is not a pointer
//to an array of 5 integers. It has decayed to
//a pointer to an integer.
void print_array_size(int (*arr)[5]) {
    //prints 8 (the size of a pointer)
    cout << "Array size in print_array_size function: " << sizeof(arr) << endl;
}

int main()
{
    int arr[5] = {1, 2, 3, 4, 5};
    //prints 20 (size of 5 4-byte integers)
    cout << "Array size in main function: " << sizeof(arr) << endl;
    print_array_size(&arr);
    return 0;
}

print_array_size函数打印出了 8 而不是期望的 20(5 个整数,每个整数 4 字节),因为arr已经从一个指向包含 5 个整数的数组(array)的指针衰退为指向一个整数的指针。相似的代码在 Rust 中能够正确运行:

use std::mem::size_of_val;

fn print_array_size(arr: [i32; 5]) {
    //prints 20
    println!("Array size in print_array_size function: {}", size_of_val(&arr));
}

fn main() {
    let arr: [i32; 5] = [1, 2, 3, 4, 5];
    //print 20
    println!("Array size in main function: {}", size_of_val(&arr));
    print_array_size(arr);
}

C/C++和 Rust 在 array 上的另一个区别是,在 Rust 中访问元素会进行边界检查。例如,在下面的 C++代码中,我们试图访问一个大小为 3 的 array 中的第 5 个元素,这导致了未定义行为[1]

#include <iostream>

using namespace std;

int main()
{
    int arr[3] = {1, 2, 3};
    const auto index = 5;
    //arr[index] is undefined behaviour
    cout << "Integer at index " << index << ": " << arr[index] << endl;
    return 0;
}

而类似的代码在 Rust 中则会 panic:

fn main() {
    let arr: [i32; 3] = [1, 2, 3];
    let index = 5;
    //arr[index] panics with the following message:
    //index out of bounds: the len is 3 but the index is 5
    println!("Integer at index {}: {}", index, arr[index]);
}

你可能想知道,Rust 版本的代码怎么就比 C++版本的代码好了?因为 C++版本的代码表现出未定义行为,它给了编译器一个不受限制的许可,允许编译器以优化的名义来做任何事。在最糟糕的情况下,这可能会把信息泄露给攻击者。

与之相对,Rust 版本的代码总是会 panic。此外,因为进程由于 panic 而终止,程序员更有可能注意并修复这个 bug。相反,C++把问题掩盖起来并且进程可以像什么事都没发生一样继续运行。比起 C/C++的未定义行为,我宁愿使用 Rust 的 panic。

Vector

Array 最大的一个限制是它的固定大小。与之相对,vector 可以在运行时扩容:

fn main() {
    //There are three elements in the vector initially
    let mut v: Vec<i32> = vec![1, 2, 3];
    //prints 3
    println!("v has {} elements", v.len());
    //but you can add more at runtime
    v.push(4);
    v.push(5);
    //prints 5
    println!("v has {} elements", v.len());
}

Vector 是如何做到在运行时扩容的呢?在其内部,vector 把所有的元素放在一个分配在堆(heap)上的 array 上。当一个新元素被 push 进来时,vector 检查 array 是否有足够的剩余空间。如果空间不足,vector 就分配一个更大的 array,将所有的元素都拷贝到这个新的 array 中,然后释放旧的 array。这可以在下面的代码中验证:

fn main() {
    let mut v: Vec<i32> = vec![1, 2, 3, 4];
    //prints 4
    println!("v's capacity is {}", v.capacity());
    println!("Address of v's first element: {:p}", &v[0]);//{:p} prints the address
    v.push(5);
    //prints 8
    println!("v's capacity is {}", v.capacity());
    println!("Address of v's first element: {:p}", &v[0]);
}

最开始,v内部的 array 容量(capacity)为 4:

接着,一个新元素被 push 到 vector 中,这使得 vector 把所有元素拷贝到一个新的容量为 8 的内部 array 中:

上面这段代码还会打印出,在放入一个元素之前和放入之后,vector 里的 array 中的第一个元素的地址。这两个地址会互不相同。地址的变化清楚地证明了其幕后分配了一个容量为 8 的新 array。

如果你在 vector 中 push 进了一个元素但是却没有看到不同的地址,这可能是因为原始的 buffer 尾部还有足够的空间,因此新旧 buffer 拥有相同的起始地址。尝试 push 更多的元素,你就会看到不同的地址。阅读 C 的库函数realloc来理解这是如何运作的。

Slice

Slice 就像一个 array 或 vector 的临时视图(temporary views)。例如,如果你有一个 array 如下:

let arr: [i32; 4] = [10, 20, 30, 40];

你可以像下面这样,创建一个包含第二个和第三个元素的 slice:

let s = &arr[1..3];

[1..3]语法创建一个区间,从索引 1(包含)到 3(不包含)(译注:即左闭右开)。如果你省略区间的第一个数([..3]),它会默认从 0 开始,如果你省略最后一个数([1..]),它会默认为到数组的长度。如果你打印 slice [1..3]中的元素,你将会得到 20 和 30:

//prints 20
println!("First element in slice: {:}", s[0]);
//prints 30
println!("Second element in slice: {:}", s[1]);

但是如果你尝试访问 slice 范围之外的元素,它会 panic:

//panics: index out of bounds
println!("Third element in slice: {:}", s[2]);

但, slice 是如何知道它只有两个元素呢?这是因为 slice 不是一个简单的指向 array 的指针,它还在一个额外的长度字段中标记了 slice 中的元素数量。

除了指向对象的地址外,还带有某些额外数据的指针称为胖指针(fat pointer)。Slice 不是 Rust 中唯一的胖指针类型。还有例如,trait 对象,除了指向对象的指针外,还有一个虚表指针。

例如,你可以创建一个 vector 的 slice:

let v: Vec<i32> = vec![1, 2, 3, 4];
let s = &v[1..3];

除了有一个指针指向v的 buffer 中的第二个元素之外,s还有一个长度为 8 字节的字段(length),其值为 2:

长度字段(length)的存在可以通过下面的代码来看到,在这段代码中,一个 slice(&[i32])的大小为 16 字节(8 字节为 buffer pointer,8 字节为长度字段):

use std::mem::size_of;

fn main() {
    //prints 8
    println!("Size of a reference to an i32: {:}", size_of::<&i32>());
    //print 16
    println!("Size of a slice: {:}", size_of::<&[i32]>());
}

Array 的 slice 也是类似,但是 buffer pointer 不是指向堆(heap)上的 buffer,而是指向栈(stack)上的 array。

因为 slice 借用自底层的数据结构,所有的常见借用规则都在此适用。例如,下面的代码会被编译器拒绝:

fn main() {
    let mut v: Vec<i32> = vec![1, 2, 3, 4];
    let s = &v[..];
    v.push(5);
    println!("First element in slice: {:}", s[0]);
}

为什么呢?因为当 slice 被创建时,它指向 vector 内部 buffer 的第一个元素并且当一个新元素被 push 进 vector 时,它(指 vector)会分配一个新的 buffer 并且旧的 buffer 会被释放。这就导致 slice 指向了一个无效的内存地址,如果访问这个无效地址则会导致未定义行为。Rust 再一次从灾难中拯救了你。

因为 array 和 vector 都可以创建 slice,它们(指 slice)是非常强大的抽象。因此,对于函数中的参数,默认的选择应该是接收一个 slice 而不是一个 array 或 vector。事实上,很多函数,像lenis_empty等,都是作用于 slice 而非 vector 或 array。

总结(Conclusion)

Array 和 vector 作为新手程序员学习过程中最先接触的数据类型之一,Rust 支持它们也不足为奇。但是,正如我们所见,Rust 的安全性保证不允许程序员对这些基础数据类型进行滥用。Slice 是 Rust 中的一个新概念,但是因为它们(指 slice)是这样一个给力的抽象,你会发现它们在任意的 Rust 代码库里都被普遍使用。

参考资料

[1]

未定义行为: https://blog.regehr.org/archives/213

【译】Rust中的array、vector和slice的更多相关文章

  1. 观V8源码中的array.js,解析 Array.prototype.slice为什么能将类数组对象转为真正的数组?

    在官方的解释中,如[mdn] The slice() method returns a shallow copy of a portion of an array into a new array o ...

  2. JavaScript中的Array.prototype.slice.call()方法学习

    JavaScript中的Array.prototype.slice.call(arguments)能将有length属性的对象转换为数组(特别注意: 这个对象一定要有length属性). 但有一个例外 ...

  3. 【译】深入理解Rust中的生命周期

    原文标题:Understanding Rust Lifetimes 原文链接:https://medium.com/nearprotocol/understanding-rust-lifetimes- ...

  4. 【译】理解Rust中的闭包

    原文标题:Understanding Closures in Rust 原文链接:https://medium.com/swlh/understanding-closures-in-rust-21f2 ...

  5. 【译】理解Rust中的局部移动

    原文标题:Understanding Partial Moves in Rust 原文链接:https://whileydave.com/2020/11/30/understanding-partia ...

  6. 【译】理解Rust中的Futures (一)

    原文标题:Understanding Futures In Rust -- Part 1 原文链接:https://www.viget.com/articles/understanding-futur ...

  7. 【译】理解Rust中的Futures(二)

    原文标题:Understanding Futures in Rust -- Part 2 原文链接:https://www.viget.com/articles/understanding-futur ...

  8. go 数组(array)、切片(slice)、map、结构体(struct)

    一 数组(array) go语言中的数组是固定长度的.使用前必须指定数组长度. go语言中数组是值类型.如果将数组赋值给另一个数组或者方法中参数使用都是复制一份,方法中使用可以使用指针传递地址. 声明 ...

  9. 详解Javascript中的Array对象

    基础介绍 创建数组 和Object对象一样,创建Array也有2种方式:构造函数.字面量法. 构造函数创建 使用构造函数的方式可以通过new关键字来声明,如下所示: 12 var arr = new ...

随机推荐

  1. 不知如何创建UML电路图?看看本文

    Visual Paradigm是包含设计共享.线框图和数据库设计新特性的企业项目设计工具.现在你只需要这样单独的一款模型软件 Visual Paradigm就可以完成用UML设计软件,用BPMN去执行 ...

  2. Linux 串口工具 lsz lrz 移植

    //之前写的,刚才不小心误删了,所以重新再发出来. 1 下载源码包 首先下载最新版的lrzsz,地址:https://ohse.de/uwe/software/lrzsz.html.下面以 0.12. ...

  3. Matlab中界面和注释---中英文切换问题

    有参考网页后实践的心得: Matlab中界面和注释---中英文切换问题 网上有大把的方法,并不是一一有效,这里介绍一种比较简单的方法我自己的电脑挺好用的,大家的电脑matlab需要你们自己实验了. 1 ...

  4. .NET Standard 版本支持

    系列目录     [已更新最新开发文章,点击查看详细] .NET标准已版本化.每个新版本都添加了更多的api.当库是针对某个.NET标准版本构建的时,它可以在实现该版本的.NET标准(或更高版本)的任 ...

  5. snappy压缩/解压库

    snappy snappy是由google开发的压缩/解压C++库,注重压缩速度,压缩后文件大小比其它算法大一些 snappy在64位x86并且是小端的cpu上性能最佳 在Intel(R) Core( ...

  6. pytest文档43-元数据使用(pytest-metadata)

    前言 什么是元数据?元数据是关于数据的描述,存储着关于数据的信息,为人们更方便地检索信息提供了帮助. pytest 框架里面的元数据可以使用 pytest-metadata 插件实现.文档地址http ...

  7. HCIA——应用层常用协议

    DNS协议 1.什么是DNS协议呢? DNS协议简单来说就是为IP取一个别名的系统(叫域名如www.baidu.com),最终目的是便于我们记忆. 一个域名可能有多个IP,同样一个IP可能也会有多个域 ...

  8. XUEXI0.4

    1.堆是一种内存管理方式,堆和栈是没有关联的.由于内存的容量很大,并且内存需求在时间和空间上没有规律,所以对操作系统来说,管理内存是非常复杂的. 2.堆这种内存管理方式特点是自由.堆内存是由操作系统划 ...

  9. spring boot:用redis+lua实现基于ip地址的分布式流量限制(限流/简单计数器算法)(spring boot 2.2.0)

    一,限流有哪些环节? 1,为什么要限流? 目的:通过对并发请求进行限速或者一个时间单位内的的请求进行限速,目的是保护系统可正常提供服务,避免被压力太大无法响应服务. 如果达到限制速率则可以采取预定的处 ...

  10. go创建http服务

    Go语言这种从零开始使用到解决问题的速度,在其他语言中是完全不可想象的.学过 C++ 的朋友都知道,一到两年大强度的理论学习和实战操练也只能学到这门语言的皮毛,以及知道一些基本的避免错误的方法. 那么 ...