Rust中slice和reference有什么区别?

16

我不明白切片和引用之间的区别。 &String&str 之间有什么区别?我在网上看到一些东西说,引用是一个细指针,而切片是一个粗指针,但我不知道它们两个的含义。 我知道一个切片可以强制转换成引用,但它是如何做到的? Deref 特性是什么?


我在网上读了一些关于Rust中“fat pointer”概念的内容。 - KamilCuk
我读了它,但我不明白为什么在编译时一个切片(例如&str)的大小是未知的。 - oberblastmeister
给定一个类型为 &str 的变量 x,它有多大?与类型为 i8(1个字节)或 i128(16个字节)或 struct Foo {x:i8,y:i128}(1个字节+ 16个字节=17个字节,忽略对齐)的变量进行比较。类型为 &Foo 的变量将使用指针的大小,解引用将给出一个大小为 17 字节的对象。由于 str[i8] 的大小是可变的,因此必须将其存储为引用的 fat 指针形式。 - CoronA
只是为了链接到什么是切片和数组的区别?而进行评论。 - trent
2个回答

33
在Rust中,slice是一个同类型数据的连续块,长度可以不同。
这意味着什么?
[u8]是一个slice。在内存中,它是一块u8的块。slice本身就是数据。但很多时候,人们将&[u8]称为slice。&[u8]是对该数据块的引用。该引用包含两个内容:指向数据本身的指针和数据的长度。由于它包含两个内容,因此被称为fat pointer。&u8也是一个引用(在这种情况下也可以认为是指针*),但我们已经知道它所指向的将是单个u8。因此,它是一个thin pointer,因为它只有一个元素。
可以保证[u8]中的所有数据都是u8类型。
由于你的[u8]只是定义为一块类型为u8的内存连续块,因此编译时无法定义其大小。因此,我们需要在指向它的指针中存储其长度。我们也不能将其放在堆栈上(这意味着我们不能有一个本地变量,它只是一个[u8]**)。
扩展:
[T]是T类型的slice。对于任何给定的T,只要T本身是一个大小已知的类型***,我们就可以想象出一个类型[T]。
str是一个字符串的slice。它保证是有效的UTF-8文本,这是它与[u8]之间的区别。Rust可以放弃有效的UTF-8保证,并将str中的所有其他内容定义为[u8]的一部分。
好了,既然你不能在本地拥有一个slice****,那么你可能会想知道如何创建slice。
答案是将数据放入已知大小的容器中,然后从其中借用slice。
例如:
let my_array: [u32; 3] = [1, 2, 3];

我们可以像这样将my_array切片成一个[u32]:

let my_slice: [u32] = my_array[..];

但是通过切片,我们会失去静态的大小信息。由于局部变量最终会被放在需要预定大小的堆栈上,因此我们必须将其放在一个引用下:

let my_slice: &[u32] = &my_array[..];

一个切片的优点在于,它是一种非常灵活(除了生命周期)的处理连续数据块的方法,无论数据来自哪里。我也可以把“my_array”变成一个堆分配的“Vec”,它仍然可以工作。
“&String”是对整个字符串对象的引用。在Rust中,字符串对象本质上是一个“Vec”。一个“Vec”包含指向其所“包含”的数据的指针,因此你的“&String”可以被认为是一个“&&str”。这就是为什么我们可以执行以下任一操作的原因:
let my_string: String = "Abc".to_string();

let my_str: &str = &my_string[..]; // As explained previously
// OR
let my_str: &str = &*my_string;

这让我想到了你的最后一个问题:
“什么是deref trait?” Deref trait是描述解引用 (*) 运算符的特征。正如你在上面看到的,我可以执行 *my_string。这是因为 String 实现了 Deref,它允许你对 String 进行解引用操作。同样地,我也可以将 Vec<T> 解引用为 [T]
需要注意的是,Deref trait 在除了 * 的其他地方也会被使用:
let my_string: String = "Abc".to_string();

let my_str: &str = &my_string;

如果我试图将类型为 &T 的值分配到类型为 &U 的位置,则 Rust 将尝试解引用我的 T,直到得到一个 U,同时仍保留至少一个引用。同样,如果我有一个 &&&&....&&&&T,并试图将其赋值给 &&&&....&&&&U,它仍然可以工作。 这个 被称为解引用强制转换:自动将 &T 转换为 &U,其中一定数量的 *T 会产生一个 U
  • *: 原始指针*const T*mut T与引用的大小相同,但编译器将它们视为不透明。编译器对原始指针背后的内容没有任何保证,甚至不能保证它们正确对齐。因此,对其进行解引用是不安全的。但由于Deref特性定义了一个安全的deref方法,因此解引用原始指针是特殊的,也不会自动完成。
  • **: 这还包括其他动态大小的类型,例如特质对象和extern types。这还包括作为其最后成员包含动态大小类型的structs,尽管这些非常难以正确构造,但在未来使用CoerceUnsized特性将变得更加容易。可以使用unsized_locals夜间功能使所有这些(除了extern types)无效,该功能允许一些使用动态大小局部变量。
  • ***: 大小已知的类型是所有在编译时已知大小的类型。您可以通用地标识它们;给定类型T,如果T:Sized,则T的大小在编译时已知。如果T:?Sized,则其大小可能在编译时未知(T:?Sized是调用者最灵活的要求,因为它接受任何)。由于片段需要内部数据连续,并且大小和类型同质,因此不可能在片段、数组或Vec<T>中包含动态大小的类型(或!Sized),并保持O(1)索引。虽然Rust可能可以编写特殊代码以对一组动态大小的类型进行索引,但目前还没有。
  • ****: 实际上您可以拥有一个切片,只需将其置于拥有它的指针下即可。例如,这可以是Box<[T]>Rc<[T]>。这些将自行处理切片的释放(当删除Box时,当所有强引用和弱引用都被删除时(当所有强引用被删除时,值的析构函数被调用,但是直到所有弱引用也消失才释放内存。)Rc)。

1
引用书籍中的话:另一种没有所有权的数据类型是切片。切片允许您引用集合中连续的元素序列,而不是整个集合。 因此,[u8]不是切片,它是一个无大小数组; &[u8]才是切片。 - Aloso
4
该书称&[T]为切片(slice),因为对于语言的初学者来说更容易理解。关于切片的参考页面指出,_切片类型的写法为 [T]_,而切片的文档则将其描述为 _一个动态大小的视图,查看连续的序列,即 [T]_。 - Optimistic Peach
这是一个不错的回答,但是这部分还不够清晰:“_但是由于我们无法拥有一个大小未知的局部变量,因此我们必须将其放在引用下面_”。当您执行let my_slice: [u32] = my_array[..];时,my_array具有已知的大小。我也不确定动词“own”在这种情况下是否合适,因为它在Rust中有特殊的含义。我认为您在这里没有使用它的Rust意义。 - nbro
@nbro 好的,我已经编辑过了,更准确地表达了我想说的意思。 - Optimistic Peach

8

什么是引用

引用类似于C语言的指针(代表内存位置),但引用永远不会无效*(即null),并且您不能在引用上进行指针算术运算。Rust中的引用与C ++中的引用非常相似。使用引用的一个重要动机是避免moveclone变量。假设您有一个计算向量总和的函数(注意:这是一个玩具示例,获取向量总和的正确方法是nums.iter().sum())。

fn sum(nums: Vec<u32>) -> Option<u32> {
    if nums.len() == 0 {
        return None;
    }
    let mut sum = 0;
    for num in nums {
        sum += num;
    }
    Some(sum);
}

这个函数移动了向量,因此之后无法使用。
let nums = vec!(1,2,3,4,5);
assert_eq!(sum(nums), 15);
assert_eq!(nums[0], 1); //<-- error, nums was moved when we calculated sum

解决方案是传递一个指向向量的引用。
fn sum(nums: &Vec<u32>) -> Option<u32> {
...
}

let nums = vec!(1,2,3,4,5);
assert_eq!(sum(&nums), 15);
assert_eq!(nums[0], 1); // <-- it works!

什么是slice

Slice指的是“作为指针和长度表示的连续内存块的视图”。它可以被视为对数组(或类似数组的东西)的引用。Rust安全性保证的一部分是确保您不会访问超出数组末尾的元素。为了实现这一点,slice在内部表示为指针和长度。与只包含没有长度信息的指针相比,这是较为臃肿的。就像上面的求和示例一样,如果nums是一个数组,而不是一个Vec,你应该传递一个slice给sum(),而不是数组本身。

String vs str

String和str之间的区别很容易混淆。简单来说,String是可变的,而str是不可变的。String是由标准库提供的类型,它是基于堆分配的并且拥有所有权。str是Rust中的原始类型之一,表示字符串切片。它通常出现在函数签名中,以表示将要处理的字符串的范围,而不需要分配新内存。

str 是一组 utf-8 编码的字符数组,而 &str 则是一组 utf-8 编码的字符切片。 String 是一组 utf-8 编码的字符向量,String 实现了 Deref<Target=str>,这意味着 &String 行为类似于(强制转换为)&str。这与 &Vec<u32> 类似,它的行为类似于 &[u32](Vec 实现了 Deref<Target=[T]>)。


* 除非使用不安全的 Rust 使其无效


2
请注意,切片也是引用。当您对类型 T 进行泛型处理时,&T 明确是一个引用,并且它也可能是一个切片。 - Aloso

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接