内置函数在标准库中有一个std::intrinsics模块它里面包含了一系列的编译器内置函 数。这些函数都有一个externrust-intrinsic修饰它们看起来都像一种特殊的 FFI 外部函数大家打开标准库的源代码src/core/intrinsics.rs,可以看到这些函数 根本没有函数体因为它们的实现是在编译器内部而不是在标准库内部。调用它们的时候 都必须使用unsafe 才可以。编译器见到这些函数就知道应该生成什么样的代码而不是像 普通函数调用一样处理。另外intrinsics 是藏在一个feature gate后面的这个feature 可能 永远不会稳定这些函数就不是准备直接提供给用户使用的。一般标准库会在这些函数基础 上做一个更合适的封装给用户使用。下面就在这些函数中挑一部分作介绍。transmutefn transmuteT,U(e:T)-U函数可以执行强制类型转换。把一个T 类型参 数转换为U类型返回值转换过程中这个参数的内部二进制表示不变。但是有一个约束条 件即size_of::()size_of::() 。 如果不符合这个条件会发生编译错误。 transmute_copy的作用跟它类似区别是参数类型是一个借用为T。一般情况下我们也可以用as 做类型转换把T 类型指针转换为裸指针然后再转换为U类型的指针。这样也可以实现类似的功能。但是用户自己实现的泛型函数有一 个缺陷即无法在where 条件中自己表达size_of::()size_of::() 。而 transmute作为一个内置函数就可以实现这样的约束。transmute和 transmute_copy在 std::mem 块中重新导出。用户如果需要请使用这个模块而不是std::intrinsics模块。下面用一个示例演示一下Vec 类型的二进制表示是怎样的fnmain(){letxvec![1,2,3,4,5];unsafe{lett:(usize,usize,usize)std::mem::transmute_copy(x);println!({}{}{},t.0,t.1,t.2);}}上面的例子中我们调用了transmute_copy, 因此参数类型是Vec 。假如我们用 transmute 函数参数类型就必须是Vec, 区别在于参数会被move 进入这个函数中 在后面就不能继续使用了。在调用transmute_copy 函数的时候必须显示指定返回值类型因为它是泛型函数返回值类型可以有多种多样的无穷变化只要满足size_of:: ()size_of::() 条件都可以完成类型转换。所以编译器自己是无法自动推理出 返回值类型的。在上例中我们的返回值类型是包含三个usize 的 tuple类型。这是因为 Vec 中实际包含了3个成员一个是指向堆上的指针一个是指向内存空间的总大小还有 一个是实际使用了的元素个数因此这个类型转换从编译器看来是满足“占用内存空间相同” 这一条件的。编译执行我们就可以看到Vec 内部的具体内存表示了。执行结果为639392055intrinsics模块里面有几个与内存读写相关的函数比如copy 、copy_nonoverla-pping 、write_bytes 、move_val_init 、volatile_load 等。这些函数又在std::ptr/std::mem 模块中做了个简单封装然后暴露出来给用户使用。下面挑其中几个重要 的函数介绍。1.copycopy 的完整签名是unsafe fn copy(src:*const T,dst:mut T,count:usize)。 它做的就是把src 指向的内容复制到dst 中去。它跟C 语言里面的 memmove类似都假设src 和 dst 指向的内容可能有重叠。区别是memmove 的参数是 void类型第三个参数是字节长度而ptr::copy 的指针是带类型的第三个参数是对象的个数。这个模块中还提供了ptr::copy_nonoverlapping 。它 跟C 语言里面的memcpy 很像都假设用户已经保证了src 和 dst 指向的内容不可重叠。所以ptr::copy_nonoverlapping的执行速度比 ptr::copy要快一些。2.write在ptr 模块中write 的签名是unsafe fn write(dst:*mut T,src:T), 作用是把变量src 写入到dst 所指向的内存中。注意它的参数src 是使用的类型T, 执行 的 是move 语义。查看源码可知它是基于intrinsics::move_val_init 实现的。注 意在写的过程中不管dst 指向的内容是什么,都会被直接覆盖掉。而src这个对象也 不会执行析构函数。写内存还有ptr::write_bytes 、ptr::write_unaligned 、ptr::write_vol- atile 等函数。3.read在 ptr 模 块 中 read 的 签 名 是unsafe fn read(src:*const T)-T, 作用是把src 指向的内容当成类型T 返回去。查看它的内部源码可见它就是基于 ptr::copy_nonoverlapping 实现的。读内存还有ptr::read_unaligned 以 及ptr::read_volatile 两个函数大同 小异。以上这些内存读写函数都是不管语义直接操作内存中的字节。所以它们都是用 unsafe 函数。4.swap在 ptr 模块中swap 的签名是unsafe fn swap(x:*mut T,y:*mut T),作 用是把两个指针指向的内容做交换。两个指针所指向的对象都只是被修改而不会被析构。这个函数在mem 模块中又做了一次封装变成了unsafe fn swap(x:mut T, y:mut T)供用户使用。签名中的mut 型引用可以保证这两个指针是当前唯一指向该对 象的指针。某些特殊类型还有自己的swap 成员函数。比如Cell::swap(self,other:Cell)。 这个函数跟其他swap 函数最大的区别在于它的参数只要求共享引用不要 求可变引用。这是因为Cell 本身的特殊性。它具备内部可变性所以这么设计是完全安全 的。我们可以从源码看到它就是简单地调用了ptr::swap。5.drop_in_place在 ptr 模块中drop_in_place 的签名是unsafe fn drop_in_placeT:?Sized (to_drop:*mut T)。它的作用是执行当前指向对象的析构函数如果没有就不执行。6.uninitialized在 mem 模块中uninitialized 的签名是unsafe fn uninitialized()-T。 它是基于intrinsics::uninit 函数实现的。我们知道Rust 编译器要求每个变量必须在 初始化之后再使用如果在某些情况下你确实需要未初始化的变量那么必须使用unsafe 才能做到。注意任何时候读取未初始化变量都是未定义行为请大家不要这么做。即便你 在unsafe代码中创造了未初始化变量也需要自己在逻辑上保证读取这个变量之前先为它 合理地赋过值。另外这个函数有点像std::mem::forget, 调用这个函数不仅不会在程序中增加 代码反而会减少可执行代码。调用forget, 会导致编译器不再插入析构函数调用的代码 调用uninitialized会导致缺少初始化。它们没有任何运行开销。uninitialized函数也还没有稳定它有一些无法克服的缺陷将来标准库会废弃掉 这个函数而使用一个新的类型让用户在unsafe代码中创建未初始化变量。综 合 示 例下面我们用一个示例来演示一下这些unsafe函数的用途以及怎样才能正确调用unsafe 代码。示例很简单就是实现标准库中的内存交换函数std::mem::swap。我们可以确定这个函数的签名是fn swap(x:mut T,y:mut T)。关于泛 型的解释在第21章中有此处略过不提。先试一个最简单的实现fn swapT(x:mutT,y:mutT){letz:T*x;*x*y;*yZ;}编译不通过。因为let z *x;执行的是move 语义编译器不允许我们把x 指向的 内容move 出来这只是一个借用而已。如果允许执行这样的操作会导致原来的借用指针 x 指向非法数据。但是我们知道我们这个函数整体上是可以保证安全的因为我们把x 指 向的内容move 出来之后会用其他的正确数据填回去最终可以保证函数执行完之后 x 和y 都是一个正常的状态。这种时候我们就需要动用unsafe了代码如下fn swapT(x:mutT,y:mutT){unsafe{letmut t:Tmem::uninitialized();ptr::copy_nonoverlapping(*x,mut t,1);ptr::copy_nonoverlapping(*y,x,1);ptr::copy_nonoverlapping(t,y,1);mem::forget();}代码逻辑的意思如下。首先我们依然需要 一个作为中转的局部变量。这个局部变量该怎么初始化呢?其实我 们不希望它执行初始化因为我们只需要这部分内存空间而已它里面的内容马上就会被覆 盖掉做初始化是浪费性能。况且我们也不知道用什么通用的办法初始化 一个泛型类型 它 连Default约束都未必满足。所以我们要用mem::uninitialized函 数 。什么是协变Rust的生命周期参数是一种泛型类型参数。比如我们可以这样理解共享引用type StrRefaa str;这是一个指向字符串的借用指针。它是一个泛型类型接受一个泛型参数之后形成一 个完整类型。它跟Vec 很像只不过Rust 里面泛型类型参数既有生命周期又有普通 类型。下面是一个示例type StrRefaa str;fn print_strb(s:StrRefb){println!({},s);}fnmain(){lets:StrRefstatichello;print_str(S);}这个例子中演示了一种有意思的现象。大家看一下print_str接受的参数类型是StrRefb, 而实际上传进来的参数类型是StrRefstatic,这两个类型并不完全一致因为 b! static。 但 是Rust 可以接受。这种现象在类型系统中被称为“协变” (covariance) 和“逆变”(contravariance)。协变和逆变的定义如下。我们用:符号记录子类型关系对于泛型类型C,协变若 T1:T2时满足C:C,则 C 对于参数T 是协变关系。逆变若 T1:T2时满足 C T2:C,则 C 对于参数T 是逆变关系。不变上述两种都不成立。总结起来就是如果类型构造器保持了参数的子类型关系就是协变如果逆转了参数 的子类型关系就是逆变。其他情况就是不变。Rust 不支持普通泛型参数类型的协变和逆变只对生命周期泛型参数存在协变和逆变。在 Rust中泛型类型支持针对生命周期的协变是一个重要功能。大家试想一下下面这 条语句为什么能成立lets:strhello;“hello” 是一个字符串字面量它的类型是static str。而 s 是 一 个局部变量它的类型是s str。其中泛型参数在源码中省略掉了这个生命周期泛型参数代表的是这个局部变量从声明到结束的这段区域。在这句话中我们把一个生命周期更长的引用 static str, 赋值给了一个生命周期更短的引用a str,这是没问题的。原因在于 既然这边被指向的目标在更长生命周期内都是合法的那么它在一个较短生命周期内也一定 是合法的。所以我们可以说引用类型对生命周期参数具有协变关系。(此处有些争论有 人认为这里应该理解为逆变关系主要的争议来自于我们很难说清两个生命周期究竟准是 准的子类型。本书中为了行文的方便继续使用“协变”,但主要意思是“协变or 逆变”,是 跟“不变”的概念相对立的。)接下来我们可以通过一些示例继续理解其他一些泛型类型的协变关系。示例如下fn testa(s:astaticstr){letlocal:a a strs;}从这个示例我们可以看到 a static str类型可以安全地赋值给a a str类型。由于static str:a str以 及 a static str:a a str关系成立这说明引用类型针对泛型参数T 也是具备协变关系的。把上面的示例改一下试试 a mut T型指针fn testa(s:a mutstaticstr){letlocal:a mut a strs;}编译可见出现了生命周期错误。这说明从a mut static str类 型 到 a mut a str类型的转换是不安全的。此事可以说明mut 型指针针对泛型T 参数是不 变 的 。下面再试试 Box 类型fn testa(s:Boxstaticstr){letlocal:Boxa strs;}这段代码可以编译通过说明从Box static str 类型到 Boxa str类型的转换是安全的。所以 Box 类型针对 T 参数是具备协变关系的。下面再试试函数fn 类型。注意fn 类型有两个地方可以使用泛型参数 一个是参数那 里一个是返回值那里。我们写两个测试用例fn test_arga(f:fn(a str)){letlocal:fn(staticstr)f;}fn test_reta(f:fn()-a str){letlocal:fn()-staticstrf;}test_arg可以通过编译test_ret不能通过。意思是fn(a str) 类型可以转换为fn(static str)类型而fn()-a str类型不能转换为fn()-static str 类型。这意味着类型 fn(T)-U 对于泛型参数T 具备协变关系对于U不具备协变关系。如果我们把这个测试改一下尝试把生命周期参数换个位置fn test_reta(f:fn()-a str){f();}fnmain(){fns()-staticstr{return;}test_ret(s);}上面这段代码可以编译通过。这意味着 fn()-static str 类型可以安全地转换为 fn()-a str 类型。那我们可以说类型fn(T)-U 对于参数U具备逆变关系。再换成具备内部可变性的类型试验use std::cell::Cell;fn testa(s:Cellstaticstr){letlocal:Cella strs;}编译出现了生命周期不匹配的错误。这说明Cell 类型针对T 参数不具备协变关系。 至于为什么要这样设计前面已经讲过了如果具备内部可变性的类型还有生命周期协变关 系可以构造出悬空指针的情况。所以需要编译器提供的UnsafeCell 来表达针对类型参 数具备“不变”关系的泛型类型。同样我们可以试试裸指针fn testa(s :*mut staticstr){letlocal:*muta strs;}可以得出结论const T针对T 参数具备协变关系而mut T 针对T 参数是不变关系。比如标准库里面的Box 。它的内部包含了一个裸指针这个裸指针就是用的const T 而不是mut T。这是因为我们希望Box 针对T 参数具备协变关系而*mut T无法提供。在写unsafe 代码的时候特别是涉及泛型的时候往往需要我们手动告诉编译器这个 类型的泛型参数究竟应该是什么协变关系。很多情况下我们需要使用PhantomData 类型 来表达这个信息。PhantomData在写unsafe 代码的时候我们经常会碰到一种情况那就是一个类型是带有生命周期参 数的它表达的是一种借用关系。可是它内部是用裸指针实现的。请注意裸指针是不带生命周期参数的。于是就发生了下面这样的情况struct Itera,T:a{ ptr:*const T, end:*const T, }然而在Rust 中如果一个类型的泛型参数从来没有被使用过那么就是一个编译 错误。请参考RFC 0738-variance。如果一个泛型参数从来没有使用过那么编译器就不 知道这个泛型参数对于这个类型是否具备协变逆变关系那么就可能在生命周期分析的时 候做出错误的结论。所以编译器禁止未使用的泛型参数。在这种情况下我们可以使用 PhantomData 类型来告诉编译器协变逆变方面的信息。PhantomData 没有运行期开销它只在类型系统层面有意义。比如一个自定义类型 有一个泛型参数 a 没有被使用为了表达这个类型对于泛型参数 a 具备协变关系那我们 可以为它加一个成员并且把类型制定为PhantomDataaT即可。因为前面我们已经说 了 a T类型对于泛型参数 a 具备协变关系所以编译器就可以推理出来这个自定义类型 对于泛型参数 a 具备协变关系。其他用法与此类似你要表达一个什么样的协变逆变关系 就找一个现存的类似的类型拿它当作PhantomData 的泛型参数即可。// PhantomData 定义在 std::marker 模块中 #[lang phantom_data] #[stable(feature rust1,since 1.0.0)] pub struct PhantomDataT:?Sized;它是一个0大小的、特殊的泛型类型。它上面有#[lang …]属性标记这说明 它是编译器特殊照顾的类型。它主要是用于写unsafe代码时告诉编译器这个类型的语义 信息。