first commit
This commit is contained in:
@ -0,0 +1,368 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
16 数据结构:Vec_T_、&[T]、Box_[T]_ ,你真的了解集合容器么?
|
||||
16|数据结构:Vec、&[T]、Box<[T]> ,你真的了解集合容器么?
|
||||
|
||||
你好,我是陈天。今天来学集合容器。
|
||||
|
||||
现在我们接触到了越来越多的数据结构,我把 Rust 中主要的数据结构从原生类型、容器类型和系统相关类型几个维度整理一下,你可以数数自己掌握了哪些。-
|
||||
-
|
||||
可以看到,容器占据了数据结构的半壁江山。
|
||||
|
||||
提到容器,很可能你首先会想到的就是数组、列表这些可以遍历的容器,但其实只要把某种特定的数据封装在某个数据结构中,这个数据结构就是一个容器。比如 Option,它是一个包裹了 T 存在或不存在的容器,而Cow 是一个封装了内部数据 B 或被借用或拥有所有权的容器。
|
||||
|
||||
对于容器的两小类,到目前为止,像 Cow 这样,为特定目的而产生的容器我们已经介绍了不少,包括 Box、Rc、Arc、RefCell、还没讲到的 Option 和 Result 等。
|
||||
|
||||
今天我们来详细讲讲另一类,集合容器。
|
||||
|
||||
集合容器
|
||||
|
||||
集合容器,顾名思义,就是把一系列拥有相同类型的数据放在一起,统一处理,比如:
|
||||
|
||||
|
||||
我们熟悉的字符串 String、数组 [T; n]、列表 Vec和哈希表 HashMap等;
|
||||
虽然到处在使用,但还并不熟悉的切片 slice;
|
||||
在其他语言中使用过,但在 Rust 中还没有用过的循环缓冲区 VecDeque、双向列表 LinkedList 等。
|
||||
|
||||
|
||||
这些集合容器有很多共性,比如可以被遍历、可以进行 map-reduce 操作、可以从一种类型转换成另一种类型等等。
|
||||
|
||||
我们会选取两类典型的集合容器:切片和哈希表,深入解读,理解了这两类容器,其它的集合容器设计思路都差不多,并不难学习。今天先介绍切片以及和切片相关的容器,下一讲我们学习哈希表。
|
||||
|
||||
切片究竟是什么?
|
||||
|
||||
在 Rust 里,切片是描述一组属于同一类型、长度不确定的、在内存中连续存放的数据结构,用 [T] 来表述。因为长度不确定,所以切片是个 DST(Dynamically Sized Type)。
|
||||
|
||||
切片一般只出现在数据结构的定义中,不能直接访问,在使用中主要用以下形式:
|
||||
|
||||
|
||||
&[T]:表示一个只读的切片引用。
|
||||
&mut [T]:表示一个可写的切片引用。
|
||||
Box<[T]>:一个在堆上分配的切片。
|
||||
|
||||
|
||||
怎么理解切片呢?我打个比方,切片之于具体的数据结构,就像数据库中的视图之于表。你可以把它看成一种工具,让我们可以统一访问行为相同、结构类似但有些许差异的类型。
|
||||
|
||||
来看下面的代码,辅助理解:
|
||||
|
||||
fn main() {
|
||||
let arr = [1, 2, 3, 4, 5];
|
||||
let vec = vec![1, 2, 3, 4, 5];
|
||||
let s1 = &arr[..2];
|
||||
let s2 = &vec[..2];
|
||||
println!("s1: {:?}, s2: {:?}", s1, s2);
|
||||
|
||||
// &[T] 和 &[T] 是否相等取决于长度和内容是否相等
|
||||
assert_eq!(s1, s2);
|
||||
// &[T] 可以和 Vec<T>/[T;n] 比较,也会看长度和内容
|
||||
assert_eq!(&arr[..], vec);
|
||||
assert_eq!(&vec[..], arr);
|
||||
}
|
||||
|
||||
|
||||
对于 array 和 vector,虽然是不同的数据结构,一个放在栈上,一个放在堆上,但它们的切片是类似的;而且对于相同内容数据的相同切片,比如 &arr[1…3] 和 &vec[1…3],这两者是等价的。除此之外,切片和对应的数据结构也可以直接比较,这是因为它们之间实现了 PartialEq trait(源码参考资料)。
|
||||
|
||||
下图比较清晰地呈现了切片和数据之间的关系:
|
||||
|
||||
另外在 Rust 下,切片日常中都是使用引用 &[T],所以很多同学容易搞不清楚 &[T] 和 &Vec 的区别。我画了张图,帮助你更好地理解它们的关系:
|
||||
|
||||
在使用的时候,支持切片的具体数据类型,你可以根据需要,解引用转换成切片类型。比如 Vec 和 [T; n] 会转化成为 &[T],这是因为 Vec 实现了 Deref trait,而 array 内建了到 &[T] 的解引用。我们可以写一段代码验证这一行为(代码):
|
||||
|
||||
use std::fmt;
|
||||
fn main() {
|
||||
let v = vec![1, 2, 3, 4];
|
||||
|
||||
// Vec 实现了 Deref,&Vec<T> 会被自动解引用为 &[T],符合接口定义
|
||||
print_slice(&v);
|
||||
// 直接是 &[T],符合接口定义
|
||||
print_slice(&v[..]);
|
||||
|
||||
// &Vec<T> 支持 AsRef<[T]>
|
||||
print_slice1(&v);
|
||||
// &[T] 支持 AsRef<[T]>
|
||||
print_slice1(&v[..]);
|
||||
// Vec<T> 也支持 AsRef<[T]>
|
||||
print_slice1(v);
|
||||
|
||||
let arr = [1, 2, 3, 4];
|
||||
// 数组虽没有实现 Deref,但它的解引用就是 &[T]
|
||||
print_slice(&arr);
|
||||
print_slice(&arr[..]);
|
||||
print_slice1(&arr);
|
||||
print_slice1(&arr[..]);
|
||||
print_slice1(arr);
|
||||
}
|
||||
|
||||
// 注意下面的泛型函数的使用
|
||||
fn print_slice<T: fmt::Debug>(s: &[T]) {
|
||||
println!("{:?}", s);
|
||||
}
|
||||
|
||||
fn print_slice1<T, U>(s: T)
|
||||
where
|
||||
T: AsRef<[U]>,
|
||||
U: fmt::Debug,
|
||||
{
|
||||
println!("{:?}", s.as_ref());
|
||||
}
|
||||
|
||||
|
||||
这也就意味着,通过解引用,这几个和切片有关的数据结构都会获得切片的所有能力,包括:binary_search、chunks、concat、contains、start_with、end_with、group_by、iter、join、sort、split、swap 等一系列丰富的功能,感兴趣的同学可以看切片的文档。
|
||||
|
||||
切片和迭代器 Iterator
|
||||
|
||||
迭代器可以说是切片的孪生兄弟。切片是集合数据的视图,而迭代器定义了对集合数据的各种各样的访问操作。
|
||||
|
||||
通过切片的 iter() 方法,我们可以生成一个迭代器,对切片进行迭代。
|
||||
|
||||
在[第12讲]Rust类型推导已经见过了 iterator trait(用 collect 方法把过滤出来的数据形成新列表)。iterator trait 有大量的方法,但绝大多数情况下,我们只需要定义它的关联类型 Item 和 next() 方法。
|
||||
|
||||
|
||||
Item 定义了每次我们从迭代器中取出的数据类型;
|
||||
|
||||
next() 是从迭代器里取下一个值的方法。当一个迭代器的 next() 方法返回 None 时,表明迭代器中没有数据了。
|
||||
|
||||
#[must_use = “iterators are lazy and do nothing unless consumed”]
|
||||
pub trait Iterator {
|
||||
|
||||
type Item;
|
||||
fn next(&mut self) -> Option<Self::Item>;
|
||||
// 大量缺省的方法,包括 size_hint, count, chain, zip, map,
|
||||
// filter, for_each, skip, take_while, flat_map, flatten
|
||||
// collect, partition 等
|
||||
...
|
||||
|
||||
}
|
||||
|
||||
|
||||
看一个例子,对 Vec 使用 iter() 方法,并进行各种 map/filter/take 操作。在函数式编程语言中,这样的写法很常见,代码的可读性很强。Rust 也支持这种写法(代码):
|
||||
|
||||
fn main() {
|
||||
// 这里 Vec<T> 在调用 iter() 时被解引用成 &[T],所以可以访问 iter()
|
||||
let result = vec![1, 2, 3, 4]
|
||||
.iter()
|
||||
.map(|v| v * v)
|
||||
.filter(|v| *v < 16)
|
||||
.take(1)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
println!("{:?}", result);
|
||||
}
|
||||
|
||||
|
||||
需要注意的是 Rust 下的迭代器是个懒接口(lazy interface),也就是说这段代码直到运行到 collect 时才真正开始执行,之前的部分不过是在不断地生成新的结构,来累积处理逻辑而已。你可能好奇,这是怎么做到的呢?
|
||||
|
||||
在 VS Code 里,如果你使用了 rust-analyzer 插件,就可以发现这一奥秘:-
|
||||
|
||||
|
||||
原来,Iterator 大部分方法都返回一个实现了 Iterator 的数据结构,所以可以这样一路链式下去,在 Rust 标准库中,这些数据结构被称为 Iterator Adapter。比如上面的 map 方法,它返回 Map 结构,而 Map 结构实现了 Iterator(源码)。
|
||||
|
||||
整个过程是这样的(链接均为源码资料):
|
||||
|
||||
|
||||
在 collect() 执行的时候,它实际试图使用 FromIterator 从迭代器中构建一个集合类型,这会不断调用 next() 获取下一个数据;
|
||||
此时的 Iterator 是 Take,Take 调自己的 next(),也就是它会调用 Filter 的 next();
|
||||
Filter 的 next() 实际上调用自己内部的 iter 的 find(),此时内部的 iter 是 Map,find() 会使用 try_fold(),它会继续调用 next(),也就是 Map 的 next();
|
||||
Map 的 next() 会调用其内部的 iter 取 next() 然后执行 map 函数。而此时内部的 iter 来自 Vec。
|
||||
|
||||
|
||||
所以,只有在 collect() 时,才触发代码一层层调用下去,并且调用会根据需要随时结束。这段代码中我们使用了 take(1),整个调用链循环一次,就能满足 take(1) 以及所有中间过程的要求,所以它只会循环一次。
|
||||
|
||||
你可能会有疑惑:这种函数式编程的写法,代码是漂亮了,然而这么多无谓的函数调用,性能肯定很差吧?毕竟,函数式编程语言的一大恶名就是性能差。
|
||||
|
||||
这个你完全不用担心, Rust 大量使用了 inline 等优化技巧,这样非常清晰友好的表达方式,性能和 C 语言的 for 循环差别不大。如果你对性能对比感兴趣,可以去最后的参考资料区看看。
|
||||
|
||||
介绍完是什么,按惯例我们就要上代码实际使用一下了。不过迭代器是非常重要的一个功能,基本上每种语言都有对迭代器的完整支持,所以只要你之前用过,对此应该并不陌生,大部分的方法,你一看就能明白是在做什么。所以这里就不再额外展示,等你遇到具体需求时,可以翻 Iterator 的文档查阅。
|
||||
|
||||
如果标准库中的功能还不能满足你的需求,你可以看看 itertools,它是和 Python 下 itertools 同名且功能类似的工具,提供了大量额外的 adapter。可以看一个简单的例子(代码):
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
fn main() {
|
||||
let err_str = "bad happened";
|
||||
let input = vec![Ok(21), Err(err_str), Ok(7)];
|
||||
let it = input
|
||||
.into_iter()
|
||||
.filter_map_ok(|i| if i > 10 { Some(i * 2) } else { None });
|
||||
// 结果应该是:vec![Ok(42), Err(err_str)]
|
||||
println!("{:?}", it.collect::<Vec<_>>());
|
||||
}
|
||||
|
||||
|
||||
在实际开发中,我们可能从一组 Future 中汇聚出一组结果,里面有成功执行的结果,也有失败的错误信息。如果想对成功的结果进一步做 filter/map,那么标准库就无法帮忙了,就需要用 itertools 里的 filter_map_ok()。
|
||||
|
||||
特殊的切片:&str
|
||||
|
||||
好,学完了普通的切片 &[T],我们来看一种特殊的切片:&str。之前讲过,String 是一个特殊的 Vec,所以在 String 上做切片,也是一个特殊的结构 &str。
|
||||
|
||||
对于 String、&String、&str,很多人也经常分不清它们的区别,我们在之前的一篇加餐中简单聊了这个问题,在上一讲智能指针中,也对比过String和&str。对于&String 和 &str,如果你理解了上文中 &Vec 和 &[T] 的区别,那么它们也是一样的:
|
||||
|
||||
String 在解引用时,会转换成 &str。可以用下面的代码验证(代码):
|
||||
|
||||
use std::fmt;
|
||||
fn main() {
|
||||
let s = String::from("hello");
|
||||
// &String 会被解引用成 &str
|
||||
print_slice(&s);
|
||||
// &s[..] 和 s.as_str() 一样,都会得到 &str
|
||||
print_slice(&s[..]);
|
||||
|
||||
// String 支持 AsRef<str>
|
||||
print_slice1(&s);
|
||||
print_slice1(&s[..]);
|
||||
print_slice1(s.clone());
|
||||
|
||||
// String 也实现了 AsRef<[u8]>,所以下面的代码成立
|
||||
// 打印出来是 [104, 101, 108, 108, 111]
|
||||
print_slice2(&s);
|
||||
print_slice2(&s[..]);
|
||||
print_slice2(s);
|
||||
}
|
||||
|
||||
fn print_slice(s: &str) {
|
||||
println!("{:?}", s);
|
||||
}
|
||||
|
||||
fn print_slice1<T: AsRef<str>>(s: T) {
|
||||
println!("{:?}", s.as_ref());
|
||||
}
|
||||
|
||||
fn print_slice2<T, U>(s: T)
|
||||
where
|
||||
T: AsRef<[U]>,
|
||||
U: fmt::Debug,
|
||||
{
|
||||
println!("{:?}", s.as_ref());
|
||||
}
|
||||
|
||||
|
||||
有同学会有疑问:那么字符的列表和字符串有什么关系和区别?我们直接写一段代码来看看:
|
||||
|
||||
use std::iter::FromIterator;
|
||||
|
||||
fn main() {
|
||||
let arr = ['h', 'e', 'l', 'l', 'o'];
|
||||
let vec = vec!['h', 'e', 'l', 'l', 'o'];
|
||||
let s = String::from("hello");
|
||||
let s1 = &arr[1..3];
|
||||
let s2 = &vec[1..3];
|
||||
// &str 本身就是一个特殊的 slice
|
||||
let s3 = &s[1..3];
|
||||
println!("s1: {:?}, s2: {:?}, s3: {:?}", s1, s2, s3);
|
||||
|
||||
// &[char] 和 &[char] 是否相等取决于长度和内容是否相等
|
||||
assert_eq!(s1, s2);
|
||||
// &[char] 和 &str 不能直接对比,我们把 s3 变成 Vec<char>
|
||||
assert_eq!(s2, s3.chars().collect::<Vec<_>>());
|
||||
// &[char] 可以通过迭代器转换成 String,String 和 &str 可以直接对比
|
||||
assert_eq!(String::from_iter(s2), s3);
|
||||
}
|
||||
|
||||
|
||||
可以看到,字符列表可以通过迭代器转换成 String,String 也可以通过 chars() 函数转换成字符列表,如果不转换,二者不能比较。
|
||||
|
||||
下图我把数组、列表、字符串以及它们的切片放在一起比较,可以帮你更好地理解它们的区别:
|
||||
|
||||
切片的引用和堆上的切片,它们是一回事么?
|
||||
|
||||
开头我们讲过,切片主要有三种使用方式:切片的只读引用 &[T]、切片的可变引用 &mut [T] 以及 Box<[T]>。刚才已经详细学习了只读切片 &[T],也和其他各种数据结构进行了对比帮助理解,可变切片 &mut [T] 和它类似,不必介绍。
|
||||
|
||||
现在我们来看看 Box<[T]>。
|
||||
|
||||
Box<[T]> 是一个比较有意思的存在,它和 Vec 有一点点差别:Vec 有额外的 capacity,可以增长;而 Box<[T]> 一旦生成就固定下来,没有 capacity,也无法增长。
|
||||
|
||||
Box<[T]>和切片的引用&[T] 也很类似:它们都是在栈上有一个包含长度的胖指针,指向存储数据的内存位置。区别是:Box<[T]> 只会指向堆,&[T] 指向的位置可以是栈也可以是堆;此外,Box<[T]> 对数据具有所有权,而 &[T] 只是一个借用。
|
||||
|
||||
那么如何产生 Box<[T]> 呢?目前可用的接口就只有一个:从已有的 Vec 中转换。我们看代码:
|
||||
|
||||
use std::ops::Deref;
|
||||
|
||||
fn main() {
|
||||
let mut v1 = vec![1, 2, 3, 4];
|
||||
v1.push(5);
|
||||
println!("cap should be 8: {}", v1.capacity());
|
||||
|
||||
// 从 Vec<T> 转换成 Box<[T]>,此时会丢弃多余的 capacity
|
||||
let b1 = v1.into_boxed_slice();
|
||||
let mut b2 = b1.clone();
|
||||
|
||||
let v2 = b1.into_vec();
|
||||
println!("cap should be exactly 5: {}", v2.capacity());
|
||||
|
||||
assert!(b2.deref() == v2);
|
||||
|
||||
// Box<[T]> 可以更改其内部数据,但无法 push
|
||||
b2[0] = 2;
|
||||
// b2.push(6);
|
||||
println!("b2: {:?}", b2);
|
||||
|
||||
// 注意 Box<[T]> 和 Box<[T; n]> 并不相同
|
||||
let b3 = Box::new([2, 2, 3, 4, 5]);
|
||||
println!("b3: {:?}", b3);
|
||||
|
||||
// b2 和 b3 相等,但 b3.deref() 和 v2 无法比较
|
||||
assert!(b2 == b3);
|
||||
// assert!(b3.deref() == v2);
|
||||
}
|
||||
|
||||
|
||||
运行代码可以看到,Vec 可以通过 into_boxed_slice() 转换成 Box<[T]>,Box<[T]> 也可以通过 into_vec() 转换回 Vec。
|
||||
|
||||
这两个转换都是很轻量的转换,只是变换一下结构,不涉及数据的拷贝。区别是,当 Vec 转换成 Box<[T]> 时,没有使用到的容量就会被丢弃,所以整体占用的内存可能会降低。而且Box<[T]> 有一个很好的特性是,不像 Box<[T;n]> 那样在编译时就要确定大小,它可以在运行期生成,以后大小不会再改变。
|
||||
|
||||
所以,当我们需要在堆上创建固定大小的集合数据,且不希望自动增长,那么,可以先创建 Vec,再转换成 Box<[T]>。tokio 在提供 broadcast channel 时,就使用了 Box<[T]> 这个特性,你感兴趣的话,可以自己看看源码。
|
||||
|
||||
小结
|
||||
|
||||
我们讨论了切片以及和切片相关的主要数据类型。切片是一个很重要的数据类型,你可以着重理解它存在的意义,以及使用方式。
|
||||
|
||||
今天学完相信你也看到了,围绕着切片有很多数据结构,而切片将它们抽象成相同的访问方式,实现了在不同数据结构之上的同一抽象,这种方法很值得我们学习。此外,当我们构建自己的数据结构时,如果它内部也有连续排列的等长的数据结构,可以考虑 AsRef 或者 Deref 到切片。
|
||||
|
||||
下图描述了切片和数组 [T;n]、列表 Vec、切片引用 &[T] /&mut [T],以及在堆上分配的切片 Box<[T]> 之间的关系。建议你花些时间理解这张图,也可以用相同的方式去总结学到的其他有关联的数据结构。-
|
||||
|
||||
|
||||
下一讲我们继续学习哈希表……
|
||||
|
||||
思考题
|
||||
|
||||
1.在讲 &str 时,里面的 print_slice1 函数,如果写成这样可不可以?你可以尝试一下,然后说明理由。
|
||||
|
||||
// fn print_slice1<T: AsRef<str>>(s: T) {
|
||||
// println!("{:?}", s.as_ref());
|
||||
// }
|
||||
|
||||
fn print_slice1<T, U>(s: T)
|
||||
where
|
||||
T: AsRef<U>,
|
||||
U: fmt::Debug,
|
||||
{
|
||||
println!("{:?}", s.as_ref());
|
||||
}
|
||||
|
||||
|
||||
2.类似 itertools,你可以试着开发一个新的 Iterator trait IteratorExt,为其提供 window_count 函数,使其可以做下图中的动作(来源):-
|
||||
|
||||
|
||||
感谢你的阅读,如果你觉得有收获,也欢迎你分享给你身边的朋友,邀他一起讨论。你已经完成了Rust学习的第16次打卡啦,我们下节课见。
|
||||
|
||||
参考资料:Rust 的 Iterator 究竟有多快?
|
||||
|
||||
当使用 Iterator 提供的这种函数式编程风格的时候,我们往往会担心性能。虽然我告诉你 Rust 大量使用 inline 来优化,但你可能还心存疑惑。
|
||||
|
||||
下面的代码和截图来自一个 Youtube 视频:Sharing code between iOS & Android with Rust,演讲者通过在使用 Iterator 处理一个很大的图片,比较 Rust/Swift/Kotlin native/C 这几种语言的性能。你也可以看到在处理迭代器时, Rust 代码和 Kotlin 或者 Swift 代码非常类似。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
运行结果,在函数式编程方式下(C 没有函数式编程支持,所以直接使用了 for 循环),Rust 和 C 几乎相当在1s 左右,C 比 Rust 快 20%,Swift 花了 11.8s,而 Kotlin native 直接超时:-
|
||||
|
||||
|
||||
所以 Rust 在对函数式编程,尤其是 Iterator 上的优化,还是非常不错的。这里面除了 inline 外,Rust 闭包的优异性能也提供了很多支持(未来我们会讲为什么)。在使用时,你完全不用担心性能。
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,578 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
30 Unsafe Rust:如何用C++的方式打开Rust?
|
||||
你好,我是陈天。
|
||||
|
||||
到目前为止,我们撰写的代码都在 Rust 精心构造的内存安全的国度里做一个守法好公民。通过遵循所有权、借用检查、生命周期等规则,我们自己的代码一旦编译通过,就相当于信心满满地向全世界宣布:这个代码是安全的!
|
||||
|
||||
然而,安全的 Rust 并不能适应所有的使用场景。
|
||||
|
||||
首先,为了内存安全,Rust 所做的这些规则往往是普适性的,编译器会把一切可疑的行为都严格地制止掉。可是,这种一丝不苟的铁面无情往往会过于严苛,导致错杀。
|
||||
|
||||
就好比“屋子的主人只会使用钥匙开门,如果一个人尝试着撬门,那一定是坏人”,正常情况下,这个逻辑是成立的,所有尝试撬门的小偷,都会被抓获(编译错误);然而,有时候主人丢了钥匙,不得不请开锁匠开门(unsafe code),此时,是正常的诉求,是可以网开一面的。
|
||||
|
||||
其次,无论 Rust 将其内部的世界构建得多么纯粹和完美,它总归是要跟不纯粹也不完美的外界打交道,无论是硬件还是软件。
|
||||
|
||||
计算机硬件本身是 unsafe 的,比如操作 IO 访问外设,或者使用汇编指令进行特殊操作(操作 GPU或者使用 SSE 指令集)。这样的操作,编译器是无法保证内存安全的,所以我们需要 unsafe 来告诉编译器要法外开恩。
|
||||
|
||||
同样的,当 Rust 要访问其它语言比如 C/C++ 的库,因为它们并不满足 Rust 的安全性要求,这种跨语言的 FFI(Foreign Function Interface),也是 unsafe 的。
|
||||
|
||||
这两种使用 unsafe Rust 的方式是不得而为之,所以情有可原,是我们需要使用 unsafe Rust 的主要原因。
|
||||
|
||||
还有一大类使用 unsafe Rust 纯粹是为了性能。比如略过边界检查、使用未初始化内存等。这样的 unsafe 我们要尽量不用,除非通过 benchmark 发现用 unsafe 可以解决某些性能瓶颈,否则使用起来得不偿失。因为,在使用 unsafe 代码的时候,我们已经把 Rust 的内存安全性,降低到了和 C++ 同等的水平。
|
||||
|
||||
可以使用 unsafe 的场景
|
||||
|
||||
好,在了解了为什么需要 unsafe Rust 之后,我们再来看看在日常工作中,都具体有哪些地方会用到 unsafe Rust。
|
||||
|
||||
我们先看可以使用、也推荐使用 unsafe 的场景,根据重要/常用程度,会依次介绍:实现 unsafe trait,主要是 Send/Sync 这两个 trait、调用已有的 unsafe 接口、对裸指针做解引用,以及使用 FFI。
|
||||
|
||||
实现 unsafe trait
|
||||
|
||||
Rust 里,名气最大的 unsafe 代码应该就是 Send/Sync 这两个 trait 了:
|
||||
|
||||
pub unsafe auto trait Send {}
|
||||
pub unsafe auto trait Sync {}
|
||||
|
||||
|
||||
相信你应该对这两个 trait 非常了解了,但凡遇到和并发相关的代码,尤其是接口的类型声明时,少不了要使用 Send/Sync 来约束。我们也知道,绝大多数数据结构都实现了 Send/Sync,但有一些例外,比如 Rc/RefCell /裸指针等。
|
||||
|
||||
因为 Send/Sync 是 auto trait,所以大部分情况下,你自己的数据结构不需要实现 Send/Sync,然而,当你在数据结构里使用裸指针时,因为裸指针是没有实现 Send/Sync 的,连带着你的数据结构也就没有实现 Send/Sync。但很可能你的结构是线程安全的,你也需要它线程安全。
|
||||
|
||||
此时,如果你可以保证它能在线程中安全地移动,那可以实现 Send;如果可以保证它能在线程中安全地共享,也可以去实现 Sync。之前我们讨论过的 Bytes 就在使用裸指针的情况下实现了 Send/Sync:
|
||||
|
||||
pub struct Bytes {
|
||||
ptr: *const u8,
|
||||
len: usize,
|
||||
// inlined "trait object"
|
||||
data: AtomicPtr<()>,
|
||||
vtable: &'static Vtable,
|
||||
}
|
||||
|
||||
// Vtable must enforce this behavior
|
||||
unsafe impl Send for Bytes {}
|
||||
unsafe impl Sync for Bytes {}
|
||||
|
||||
|
||||
但是,在实现 Send/Sync 的时候要特别小心,如果你无法保证数据结构的线程安全,错误实现 Send/Sync之后,会导致程序出现莫名其妙的还不太容易复现的崩溃。
|
||||
|
||||
比如下面的代码,强行为 Evil 实现了 Send,而 Evil 内部携带的 Rc 是不允许实现 Send 的。这段代码通过实现 Send 而规避了 Rust 的并发安全检查,使其可以编译通过(代码):
|
||||
|
||||
use std::{cell::RefCell, rc::Rc, thread};
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
struct Evil {
|
||||
data: Rc<RefCell<usize>>,
|
||||
}
|
||||
|
||||
// 为 Evil 强行实现 Send,这会让 Rc 整个紊乱
|
||||
unsafe impl Send for Evil {}
|
||||
|
||||
fn main() {
|
||||
let v = Evil::default();
|
||||
let v1 = v.clone();
|
||||
let v2 = v.clone();
|
||||
|
||||
let t1 = thread::spawn(move || {
|
||||
let v3 = v.clone();
|
||||
let mut data = v3.data.borrow_mut();
|
||||
*data += 1;
|
||||
println!("v3: {:?}", data);
|
||||
});
|
||||
|
||||
let t2 = thread::spawn(move || {
|
||||
let v4 = v1.clone();
|
||||
let mut data = v4.data.borrow_mut();
|
||||
*data += 1;
|
||||
println!("v4: {:?}", data);
|
||||
});
|
||||
|
||||
t2.join().unwrap();
|
||||
t1.join().unwrap();
|
||||
|
||||
let mut data = v2.data.borrow_mut();
|
||||
*data += 1;
|
||||
|
||||
println!("v2: {:?}", data);
|
||||
}
|
||||
|
||||
|
||||
然而在运行的时候,有一定的几率出现崩溃:
|
||||
|
||||
❯ cargo run --example rc_send
|
||||
v4: 1
|
||||
v3: 2
|
||||
v2: 3
|
||||
|
||||
❯ cargo run --example rc_send
|
||||
v4: 1
|
||||
thread '<unnamed>' panicked at 'already borrowed: BorrowMutError', examples/rc_send.rs:18:32
|
||||
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
|
||||
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Any { .. }', examples/rc_send.rs:31:15
|
||||
|
||||
|
||||
所以,如果你没有十足的把握,不宜胡乱实现 Send/Sync。
|
||||
|
||||
既然我们提到了 unsafe trait,你也许会好奇,什么 trait 会是 unsafe 呢?除了 Send/Sync 外,还会有其他 unsafe trait 么?当然会有。
|
||||
|
||||
任何 trait,只要声明成 unsafe,它就是一个 unsafe trait。而一个正常的 trait 里也可以包含 unsafe 函数,我们看下面的示例(代码):
|
||||
|
||||
// 实现这个 trait 的开发者要保证实现是内存安全的
|
||||
unsafe trait Foo {
|
||||
fn foo(&self);
|
||||
}
|
||||
|
||||
trait Bar {
|
||||
// 调用这个函数的人要保证调用是安全的
|
||||
unsafe fn bar(&self);
|
||||
}
|
||||
|
||||
struct Nonsense;
|
||||
|
||||
unsafe impl Foo for Nonsense {
|
||||
fn foo(&self) {
|
||||
println!("foo!");
|
||||
}
|
||||
}
|
||||
|
||||
impl Bar for Nonsense {
|
||||
unsafe fn bar(&self) {
|
||||
println!("bar!");
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let nonsense = Nonsense;
|
||||
// 调用者无需关心 safety
|
||||
nonsense.foo();
|
||||
|
||||
// 调用者需要为 safety 负责
|
||||
unsafe { nonsense.bar() };
|
||||
}
|
||||
|
||||
|
||||
可以看到,unsafe trait 是对 trait 的实现者的约束,它告诉 trait 的实现者:实现我的时候要小心,要保证内存安全,所以实现的时候需要加 unsafe 关键字。
|
||||
|
||||
但 unsafe trait 对于调用者来说,可以正常调用,不需要任何 unsafe block,因为这里的 safety 已经被实现者保证了,毕竟如果实现者没保证,调用者也做不了什么来保证 safety,就像我们使用 Send/Sync 一样。
|
||||
|
||||
而unsafe fn 是函数对调用者的约束,它告诉函数的调用者:如果你胡乱使用我,会带来内存安全方面的问题,请妥善使用,所以调用 unsafe fn 时,需要加 unsafe block 提醒别人注意。
|
||||
|
||||
再来看一个实现和调用都是 unsafe 的 trait:GlobalAlloc。
|
||||
|
||||
下面这段代码在智能指针的[那一讲]中我们见到过,通过 GlobalAlloc 我们可以实现自己的内存分配器。因为内存分配器对内存安全的影响很大,所以实现者需要保证每个实现都是内存安全的。同时,alloc/dealloc 这样的方法,使用不正确的姿势去调用,也会发生内存安全的问题,所以这两个方法也是 unsafe 的:
|
||||
|
||||
use std::alloc::{GlobalAlloc, Layout, System};
|
||||
|
||||
struct MyAllocator;
|
||||
|
||||
unsafe impl GlobalAlloc for MyAllocator {
|
||||
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
|
||||
let data = System.alloc(layout);
|
||||
eprintln!("ALLOC: {:p}, size {}", data, layout.size());
|
||||
data
|
||||
}
|
||||
|
||||
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
|
||||
System.dealloc(ptr, layout);
|
||||
eprintln!("FREE: {:p}, size {}", ptr, layout.size());
|
||||
}
|
||||
}
|
||||
|
||||
#[global_allocator]
|
||||
static GLOBAL: MyAllocator = MyAllocator;
|
||||
|
||||
|
||||
好,unsafe trait 就讲这么多,如果你想了解更多详情,可以看 Rust RFC2585。如果你想看一个完整的 unsafe trait 定义到实现的过程,可以看 BufMut。
|
||||
|
||||
调用已有的 unsafe 函数
|
||||
|
||||
接下来我们讲 unsafe 函数。有些时候,你会发现,标准库或者第三方库提供给你的函数本身就标明了 unsafe。比如我们之前为了打印 HashMap 结构所使用的 transmute 函数:
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn main() {
|
||||
let map = HashMap::new();
|
||||
let mut map = explain("empty", map);
|
||||
|
||||
map.insert(String::from("a"), 1);
|
||||
explain("added 1", map);
|
||||
}
|
||||
|
||||
// HashMap 结构有两个 u64 的 RandomState,然后是四个 usize,
|
||||
// 分别是 bucket_mask, ctrl, growth_left 和 items
|
||||
// 我们 transmute 打印之后,再 transmute 回去
|
||||
fn explain<K, V>(name: &str, map: HashMap<K, V>) -> HashMap<K, V> {
|
||||
let arr: [usize; 6] = unsafe { std::mem::transmute(map) };
|
||||
println!(
|
||||
"{}: bucket_mask 0x{:x}, ctrl 0x{:x}, growth_left: {}, items: {}",
|
||||
name, arr[2], arr[3], arr[4], arr[5]
|
||||
);
|
||||
|
||||
// 因为 std:mem::transmute 是一个 unsafe 函数,所以我们需要 unsafe
|
||||
unsafe { std::mem::transmute(arr) }
|
||||
}
|
||||
|
||||
|
||||
前面已经说过,要调用一个 unsafe 函数,你需要使用 unsafe block 把它包裹起来。这相当于在提醒大家,注意啊,这里有 unsafe 代码!
|
||||
|
||||
另一种调用 unsafe 函数的方法是定义 unsafe fn,然后在这个 unsafe fn 里调用其它 unsafe fn。
|
||||
|
||||
如果你阅读一些标准库的代码会发现,有时候同样的功能,Rust 会提供 unsafe 和 safe 的版本,比如,把 &[u8] 里的数据转换成字符串:
|
||||
|
||||
// safe 版本,验证合法性,如果不合法返回错误
|
||||
pub fn from_utf8(v: &[u8]) -> Result<&str, Utf8Error> {
|
||||
run_utf8_validation(v)?;
|
||||
// SAFETY: Just ran validation.
|
||||
Ok(unsafe { from_utf8_unchecked(v) })
|
||||
}
|
||||
|
||||
// 不验证合法性,调用者需要确保 &[u8] 里都是合法的字符
|
||||
pub const unsafe fn from_utf8_unchecked(v: &[u8]) -> &str {
|
||||
// SAFETY: the caller must guarantee that the bytes `v` are valid UTF-8.
|
||||
// Also relies on `&str` and `&[u8]` having the same layout.
|
||||
unsafe { mem::transmute(v) }
|
||||
}
|
||||
|
||||
|
||||
安全的 str::from_utf8() 内部做了一些检查后,实际调用了 str::from_utf8_unchecked()。如果我们不需要做这一层检查,这个调用可以高效很多(可能是一个量级的区别),因为 unsafe 的版本就只是一个类型的转换而已。
|
||||
|
||||
那么这样有两个版本的接口,我们该如何调用呢?
|
||||
|
||||
如果你并不是特别明确,一定要调用安全的版本,不要为了性能的优势而去调用不安全的版本。如果你清楚地知道,&[u8] 你之前已经做过检查,或者它本身就来源于你从 &str 转换成的 &[u8],现在只不过再转换回去,那可以调用不安全的版本,并在注释中注明为什么这里是安全的。
|
||||
|
||||
对裸指针解引用
|
||||
|
||||
unsafe trait 和 unsafe fn 的使用就了解到这里啦,我们再看裸指针。很多时候,如果需要进行一些特殊处理,我们会把得到的数据结构转换成裸指针,比如刚才的 Bytes。
|
||||
|
||||
裸指针在生成的时候无需 unsafe,因为它并没有内存不安全的操作,但裸指针的解引用操作是不安全的,潜在有风险,它也需要使用 unsafe 来明确告诉编译器,以及代码的阅读者,也就是说要使用 unsafe block 包裹起来。
|
||||
|
||||
下面是一段对裸指针解引用的操作(代码):
|
||||
|
||||
fn main() {
|
||||
let mut age = 18;
|
||||
|
||||
// 不可变指针
|
||||
let r1 = &age as *const i32;
|
||||
// 可变指针
|
||||
let r2 = &mut age as *mut i32;
|
||||
|
||||
// 使用裸指针,可以绕过 immutable/mutable borrow rule
|
||||
|
||||
// 然而,对指针解引用需要使用 unsafe
|
||||
unsafe {
|
||||
println!("r1: {}, r2: {}", *r1, *r2);
|
||||
}
|
||||
}
|
||||
|
||||
fn immutable_mutable_cant_coexist() {
|
||||
let mut age = 18;
|
||||
let r1 = &age;
|
||||
// 编译错误
|
||||
let r2 = &mut age;
|
||||
|
||||
println!("r1: {}, r2: {}", *r1, *r2);
|
||||
}
|
||||
|
||||
|
||||
我们可以看到,使用裸指针,可变指针和不可变指针可以共存,不像可变引用和不可变引用无法共存。这是因为裸指针的任何对内存的操作,无论是 ptr::read/ptr::write,还是解引用,都是unsafe 的操作,所以只要读写内存,裸指针的使用者就需要对内存安全负责。
|
||||
|
||||
你也许会觉得奇怪,这里也没有内存不安全的操作啊,为啥需要 unsafe 呢?是的,虽然在这个例子里,裸指针来源于一个可信的内存地址,所有的代码都是安全的,但是,下面的代码就是不安全的,会导致 segment fault(代码):
|
||||
|
||||
fn main() {
|
||||
// 裸指针指向一个有问题的地址
|
||||
let r1 = 0xdeadbeef as *mut u32;
|
||||
|
||||
println!("so far so good!");
|
||||
|
||||
unsafe {
|
||||
// 程序崩溃
|
||||
*r1 += 1;
|
||||
println!("r1: {}", *r1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
这也是为什么我们在撰写 unsafe Rust 的时候,要慎之又慎,并且在 unsafe 代码中添加足够的注释来阐述为何你觉得可以保证这段代码的安全。
|
||||
|
||||
使用裸指针的时候,大部分操作都是 unsafe 的(下图里表三角惊叹号的):-
|
||||
-
|
||||
如果你对此感兴趣,可以查阅 std::ptr 的文档。
|
||||
|
||||
使用 FFI
|
||||
|
||||
最后一种可以使用 unsafe 的地方是 FFI。
|
||||
|
||||
当 Rust 要使用其它语言的能力时,Rust 编译器并不能保证那些语言具备内存安全,所以和第三方语言交互的接口,一律要使用 unsafe,比如,我们调用 libc 来进行 C 语言开发者熟知的 malloc/free(代码):
|
||||
|
||||
use std::mem::transmute;
|
||||
|
||||
fn main() {
|
||||
let data = unsafe {
|
||||
let p = libc::malloc(8);
|
||||
let arr: &mut [u8; 8] = transmute(p);
|
||||
arr
|
||||
};
|
||||
|
||||
data.copy_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8]);
|
||||
|
||||
println!("data: {:?}", data);
|
||||
|
||||
unsafe { libc::free(transmute(data)) };
|
||||
}
|
||||
|
||||
|
||||
从代码中可以看到,所有的对 libc 函数的调用,都需要使用 unsafe block。下节课我们会花一讲的时间谈谈 Rust 如何做 FFI,到时候细讲。
|
||||
|
||||
不推荐的使用 unsafe 的场景
|
||||
|
||||
以上是我们可以使用 unsafe 的场景。还有一些情况可以使用 unsafe,但是,我并不推荐。比如处理未初始化数据、访问可变静态变量、使用 unsafe 提升性能。
|
||||
|
||||
虽然不推荐使用,但它们作为一种用法,在标准库和第三方库中还是会出现,我们即便自己不写,在遇到的时候,也最好能够读懂它们。
|
||||
|
||||
访问或者修改可变静态变量
|
||||
|
||||
首先是可变静态变量。之前的课程中,我们见识过全局的 static 变量,以及使用 lazy_static 来声明复杂的 static 变量。然而之前遇到的 static 变量都是不可变的。
|
||||
|
||||
Rust 还支持可变的 static 变量,可以使用 static mut 来声明。
|
||||
|
||||
显而易见的是,全局变量如果可写,会潜在有线程不安全的风险,所以如果你声明 static mut 变量,在访问时,统统都需要使用 unsafe。以下的代码就使用了 static mut,并试图在两个线程中分别改动它。你可以感受到,这个代码的危险(代码):
|
||||
|
||||
use std::thread;
|
||||
|
||||
static mut COUNTER: usize = 1;
|
||||
|
||||
fn main() {
|
||||
let t1 = thread::spawn(move || {
|
||||
unsafe { COUNTER += 10 };
|
||||
});
|
||||
|
||||
let t2 = thread::spawn(move || {
|
||||
unsafe { COUNTER *= 10 };
|
||||
});
|
||||
|
||||
t2.join().unwrap();
|
||||
t1.join().unwrap();
|
||||
|
||||
unsafe { println!("COUNTER: {}", COUNTER) };
|
||||
}
|
||||
|
||||
|
||||
其实我们完全没必要这么做。对于上面的场景,我们可以使用 AtomicXXX 来改进:
|
||||
|
||||
use std::{
|
||||
sync::atomic::{AtomicUsize, Ordering},
|
||||
thread,
|
||||
};
|
||||
|
||||
static COUNTER: AtomicUsize = AtomicUsize::new(1);
|
||||
|
||||
fn main() {
|
||||
let t1 = thread::spawn(move || {
|
||||
COUNTER.fetch_add(10, Ordering::SeqCst);
|
||||
});
|
||||
|
||||
let t2 = thread::spawn(move || {
|
||||
COUNTER
|
||||
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |v| Some(v * 10))
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
t2.join().unwrap();
|
||||
t1.join().unwrap();
|
||||
|
||||
println!("COUNTER: {}", COUNTER.load(Ordering::Relaxed));
|
||||
}
|
||||
|
||||
|
||||
有同学可能会问:如果我的数据结构比较复杂,无法使用 AtomicXXX 呢?
|
||||
|
||||
如果你需要定义全局的可变状态,那么,你还可以使用 Mutex 或者 RwLock 来提供并发安全的写访问,比如:
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use std::{collections::HashMap, sync::Mutex, thread};
|
||||
|
||||
// 使用 lazy_static 初始化复杂的结构
|
||||
lazy_static! {
|
||||
// 使用 Mutex/RwLock 来提供安全的并发写访问
|
||||
static ref STORE: Mutex<HashMap<&'static str, &'static [u8]>> = Mutex::new(HashMap::new());
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let t1 = thread::spawn(move || {
|
||||
let mut store = STORE.lock().unwrap();
|
||||
store.insert("hello", b"world");
|
||||
});
|
||||
|
||||
let t2 = thread::spawn(move || {
|
||||
let mut store = STORE.lock().unwrap();
|
||||
store.insert("goodbye", b"world");
|
||||
});
|
||||
|
||||
t2.join().unwrap();
|
||||
t1.join().unwrap();
|
||||
|
||||
println!("store: {:?}", STORE.lock().unwrap());
|
||||
}
|
||||
|
||||
|
||||
所以,我非常不建议你使用 static mut。任何需要 static mut 的地方,都可以用 AtomicXXX/Mutex/RwLock 来取代。千万不要为了一时之快,给程序种下长远的祸根。
|
||||
|
||||
在宏里使用 unsafe
|
||||
|
||||
虽然我们并没有介绍宏编程,但已经在很多场合使用过宏了,宏可以在编译时生成代码。
|
||||
|
||||
在宏中使用 unsafe,是非常危险的。
|
||||
|
||||
首先使用你的宏的开发者,可能压根不知道 unsafe 代码的存在;其次,含有 unsafe 代码的宏在被使用到的时候,相当于把 unsafe 代码注入到当前上下文中。在不知情的情况下,开发者到处调用这样的宏,会导致 unsafe 代码充斥在系统的各个角落,不好处理;最后,一旦 unsafe 代码出现问题,你可能都很难找到问题的根本原因。
|
||||
|
||||
以下是 actix_web 代码库中的 downcast_dyn 宏,你可以感受到本来就比较晦涩的宏,跟 unsafe 碰撞在一起,那种令空气都凝固了的死亡气息:
|
||||
|
||||
// Generate implementation for dyn $name
|
||||
macro_rules! downcast_dyn {
|
||||
($name:ident) => {
|
||||
/// A struct with a private constructor, for use with
|
||||
/// `__private_get_type_id__`. Its single field is private,
|
||||
/// ensuring that it can only be constructed from this module
|
||||
#[doc(hidden)]
|
||||
#[allow(dead_code)]
|
||||
pub struct PrivateHelper(());
|
||||
|
||||
impl dyn $name + 'static {
|
||||
/// Downcasts generic body to a specific type.
|
||||
#[allow(dead_code)]
|
||||
pub fn downcast_ref<T: $name + 'static>(&self) -> Option<&T> {
|
||||
if self.__private_get_type_id__(PrivateHelper(())).0
|
||||
== std::any::TypeId::of::<T>()
|
||||
{
|
||||
// SAFETY: external crates cannot override the default
|
||||
// implementation of `__private_get_type_id__`, since
|
||||
// it requires returning a private type. We can therefore
|
||||
// rely on the returned `TypeId`, which ensures that this
|
||||
// case is correct.
|
||||
unsafe { Some(&*(self as *const dyn $name as *const T)) }
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Downcasts a generic body to a mutable specific type.
|
||||
#[allow(dead_code)]
|
||||
pub fn downcast_mut<T: $name + 'static>(&mut self) -> Option<&mut T> {
|
||||
if self.__private_get_type_id__(PrivateHelper(())).0
|
||||
== std::any::TypeId::of::<T>()
|
||||
{
|
||||
// SAFETY: external crates cannot override the default
|
||||
// implementation of `__private_get_type_id__`, since
|
||||
// it requires returning a private type. We can therefore
|
||||
// rely on the returned `TypeId`, which ensures that this
|
||||
// case is correct.
|
||||
unsafe { Some(&mut *(self as *const dyn $name as *const T as *mut T)) }
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
所以,除非你是一个 unsafe 以及宏编程的老手,否则不建议这么做。
|
||||
|
||||
使用 unsafe 提升性能
|
||||
|
||||
unsafe 代码在很多 Rust 基础库中有大量的使用,比如哈希表那一讲提到的 hashbrown,如果看它的代码库,你会发现一共有 222 处使用 unsafe:
|
||||
|
||||
hashbrown on master
|
||||
❯ ag "unsafe" | wc -l
|
||||
222
|
||||
|
||||
|
||||
这些 unsafe 代码,大多是为了性能而做的妥协。
|
||||
|
||||
比如下面的代码就使用了 SIMD 指令来加速处理:
|
||||
|
||||
unsafe {
|
||||
// A byte is EMPTY or DELETED iff the high bit is set
|
||||
BitMask(x86::_mm_movemask_epi8(self.0) as u16)
|
||||
}
|
||||
|
||||
|
||||
然而,如果你不是在撰写非常基础的库,并且这个库处在系统的关键路径上,我也很不建议使用 unsafe 来提升性能。
|
||||
|
||||
性能,是一个系统级的问题。在你没有解决好架构、设计、算法、网络、存储等其他问题时,就来抠某个函数的实现细节的性能,我认为是不妥的,尤其是试图通过使用 unsafe 代码,跳过一些检查来提升性能。
|
||||
|
||||
要知道,好的算法和不好的算法可以有数量级上的性能差异。而有些时候,即便你能够使用 unsafe 让局部性能达到最优,但作为一个整体看的时候,这个局部的优化可能根本没有意义。
|
||||
|
||||
所以,如果你用 Rust 做 Web 开发、做微服务、做客户端,很可能都不需要专门撰写 unsafe 代码来提升性能。
|
||||
|
||||
撰写 unsafe 代码
|
||||
|
||||
了解了unsafe可以使用和不建议使用的具体场景,最后,我们来写一段小小的代码,看看如果实际工作中,遇到不得不写 unsafe 代码时,该怎么做。
|
||||
|
||||
需求是要实现一个 split() 函数,得到一个字符串 s,按照字符 sep 第一次出现的位置,把字符串 s 截成前后两个字符串。这里,当找到字符 sep 的位置 pos 时,我们需要使用一个函数,得到从字符串开头到 pos 的子串,以及从字符 sep 之后到字符串结尾的子串。
|
||||
|
||||
要获得这个子串,Rust 有安全的 get 方法,以及不安全的 get_unchecked 方法。正常情况下,我们应该使用 get() 方法,但这个实例,我们就强迫自己使用 get_unchecked() 来跳过检查。
|
||||
|
||||
先看这个函数的安全性要求:-
|
||||
-
|
||||
在遇到 unsafe 接口时,我们都应该仔细阅读其安全须知,然后思考如何能满足它。如果你自己对外提供 unsafe 函数,也应该在文档中详细地给出类似的安全须知,告诉调用者,怎么样调用你的函数才算安全。
|
||||
|
||||
对于 split 的需求,我们完全可以满足 get_unchecked() 的安全要求,以下是实现(代码):
|
||||
|
||||
fn main() {
|
||||
let mut s = "我爱你!中国".to_string();
|
||||
let r = s.as_mut();
|
||||
|
||||
if let Some((s1, s2)) = split(r, '!') {
|
||||
println!("s1: {}, s2: {}", s1, s2);
|
||||
}
|
||||
}
|
||||
|
||||
fn split(s: &str, sep: char) -> Option<(&str, &str)> {
|
||||
let pos = s.find(sep);
|
||||
|
||||
pos.map(|pos| {
|
||||
let len = s.len();
|
||||
let sep_len = sep.len_utf8();
|
||||
|
||||
// SAFETY: pos 是 find 得到的,它位于字符的边界处,同样 pos + sep_len 也是如此
|
||||
// 所以以下代码是安全的
|
||||
unsafe { (s.get_unchecked(0..pos), s.get_unchecked(pos + sep_len..len)) }
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
同样的,在撰写 unsafe 代码调用别人的 unsafe 函数时,我们一定要用注释声明代码的安全性,这样,别人在阅读我们的代码时,可以明白为什么此处是安全的、是符合这个 unsafe 函数的预期的。
|
||||
|
||||
小结
|
||||
|
||||
unsafe 代码,是 Rust 这样的系统级语言必须包含的部分,当 Rust 跟硬件、操作系统,以及其他语言打交道,unsafe 是必不可少的。-
|
||||
|
||||
|
||||
当我们使用 unsafe 撰写 Rust 代码时,要格外小心,因为此时编译器已经把内存安全的权杖完全交给了你,在打开 unsafe block 的那一刻,你会获得 C/C++ 代码般的自由度,但这个自由背后的代价就是安全性上的妥协。
|
||||
|
||||
好的 unsafe 代码,足够短小、精简,只包含不得不包含的内容。unsafe 代码是开发者对编译器和其它开发者的一种庄重的承诺:我宣誓,这段代码是安全的。
|
||||
|
||||
今天讲的内容里的很多代码都是反面教材,并不建议你大量使用,尤其是初学者。那为什么我们还要讲 unsafe 代码呢?老子说:知其雄守其雌。我们要知道 Rust 的阴暗面(unsafe rust),才更容易守得住它光明的那一面(safe rust)。
|
||||
|
||||
这一讲了解了 unsafe 代码的使用场景,希望你日后,在阅读 unsafe 代码的时候,不再心里发怵;同时,在撰写 unsafe 代码时,能够对其足够敬畏。
|
||||
|
||||
思考题
|
||||
|
||||
上文中,我们使用 s.get_unchecked() 来获取一个子字符串,通过使用合适的 pos,可以把一个字符串 split 成两个。如果我们需要一个 split_mut 接口怎么实现?
|
||||
|
||||
fn split_mut(s: &mut str, sep: char) -> (&mut str, &mut str)
|
||||
|
||||
|
||||
你可以尝试使用 get_unchecked_mut(),看看代码能否编译通过?想想为什么?然后,试着自己构建 unsafe 代码实现一下?
|
||||
|
||||
小提示,你可以把 s 先转换成裸指针,然后再用 std::slice::from_raw_parts_mut() 通过一个指针和一个长度,构建出一个 slice(还记得 &[u8] 其实内部就是一个 ptr + len 么?)。然后,再通过 std::str::from_utf8_unchecked_mut() 构建出 &mut str。
|
||||
|
||||
感谢你的收听,今天你完成了Rust学习的第30次打卡。如果你觉得有收获,也欢迎你分享给身边的朋友,邀他一起讨论。我们下节课见~
|
||||
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user