rust学习教程28-深入类型转换(代码片段)

孙飞Sunface 孙飞Sunface     2023-01-15     517

关键词:

本文节选自<<Rust语言圣经>>一书
欢迎大家加入Rust编程学院,一起学习交流:
QQ群:1009730433

类型转换

Rust是类型安全的语言,因此在Rust中做类型转换不是一件简单的事,这一章节我们将对Rust中的类型转换进行详尽讲解。

as转换

先来看一段代码:

fn main() 
  let a: i32 = 10;
  let b: u16 = 100;

  if a < b 
    println!("Ten is less than one hundred.");
  

能跟着这本书一直学习到这里,说明你对Rust已经有了一定的理解,那么一眼就能看出这段代码注定会报错,因为ab拥有不同的类型,Rust不允许两种不同的类型进行比较。

解决办法很简单,只要把b转换成i32类型即可,这里使用as操作符来完成:if a < (b as i32) .... 那么为什么不把a转换成u16类型呢?

因为每个类型能表达的大小不一样,如果把大的类型转换成小的类型,会造成错误, 因此我们需要把小的类型转换成大的类型,来避免这些问题的发生.

使用类型转换需要小心,因为如果执行以下操作300_i32 as i8,你将获得44这个值,而不是300,因为i8类型能表达的的最大值为2^7 - 1, 使用以下代码可以查看i8的最大值:

let a = i8::MAX;
println!("",a);

下面列出了常用的转换形式:

fn main() 
   let a = 3.1 as i8;
   let b = 100_i8 as i32;
   let c = 'a' as u8; // 将字符'a'转换为整数, 97

   println!(",,",a,b,c)

内存地址转换为指针

let mut values: [i32; 2] = [1, 2];
let p1: *mut i32 = values.as_mut_ptr();
let first_address = p1 as usize; // 将p1内存地址转换为一个整数
let second_address = first_address + 4; // 4 == std:mem::size_of::<i32>(), i32类型占用4个字节,因此将内存地址 + 4
let p2 = second_address as *mut i32; // 访问该地址指向的下一个整数p2
unsafe 
    *p2 += 1;

assert_eq!(values[1], 3);

强制类型转换的边角知识

  1. 数组切片原生指针之间的转换,不会改变数组占用的内存字节数,尽管数组元素的类型发生了改变:
fn main() 
    let a: *const [u16] = &[1,2,3,4,5];
    let b = a as *const[u8];
    assert_eq!(std::mem::size_of_val(&a),std::mem::size_of_val(&b))

  1. 转换不具有传递性
    就算e as U1 as U2是合法的,也不能说明e as U2是合法的。

TryInto转换

在一些场景中,使用as关键字会有比较大的限制,因为你想要在类型转换上拥有完全的控制,例如处理转换错误,那么你将需要TryInto:

use std::convert::TryInto;
 
fn main() 
   let a: u8 = 10;
   let b: u16 = 1500;
 
   let b_: u8 = b.try_into().unwrap();
 
   if a < b_ 
     println!("Ten is less than one hundred.");
   

上面代码中引入了std::convert::TryInto特征,但是却没有使用它,可能有些同学会为此困惑,主要原因在于如果你要使用一个特征的方法,那么你需要引入该特征到当前的作用域中,我们在上面用到了try_into方法,因此需要引入对应的特征。但是Rust又提供了一个非常便利的办法,把最常用的标准库中的特征通过std::prelude模块提前引入到当前作用域中,其中包括了std::convert::TryInto,你可以尝试删除第一行的代码use ...,看看是否会报错.

try_into会尝试进行一次转换,如果失败,则会返回一个Result,然后你可以进行相应的错误处理,但是因为我们的例子只是为了快速测试,因此使用了unwrap方法,该方法在发现错误时,会直接调用panic导致程序的崩溃退出,在实际项目中,请不要这么使用,具体见panic部分.

最主要的是try_into转换会捕获大类型向小类型转换时导致的溢出错误:

fn main() 
    let b: i16 = 1500;

    let b_: u8 = match b.try_into() 
        Ok(b1) => b1,
        Err(e) => 
            println!(":?", e.to_string());
            0
        
    ;

运行后输出如下"out of range integral type conversion attempted", 在这里我们程序捕获了错误,编译器告诉我们类型范围超出的转换是不被允许的,因为我们试图把1500_i16转换为u8类型,后者明显不足以承载这么大的值。

通用类型转换

虽然asTryInto很强大,但是只能应用在数值类型上,可是Rust有如此多的类型,想要为这些类型实现转换,我们需要另谋出路,先来看看在一个笨办法,将一个结构体转换为另外一个结构体:

struct Foo 
    x: u32,
    y: u16,


struct Bar 
    a: u32,
    b: u16,


fn reinterpret(foo: Foo) -> Bar 
    let Foo  x, y  = foo;
    Bar  a: x, b: y 

简单粗暴,但是从另外一个角度来看,也挺啰嗦的,好在Rust为我们提供了更通用的方式来完成这个目的。

强制类型转换

在某些情况下,类型是可以进行隐式强制转换的,但是这些转换其实弱化了Rust的类型系统,它们的存在是为了让Rust在大多数场景可以工作(说白了,帮助用户省事),而不是报各种类型上的编译错误。

首先,在匹配特征时,不会做任何强制转换(除了方法)。如果有一个类型T可以强制转换为U,不代表impl T可以强制转换为impl U,例如以下的代码就无法通过编译检查:

trait Trait 

fn foo<X: Trait>(t: X) 

impl<'a> Trait for &'a i32 

fn main() 
    let t: &mut i32 = &mut 0;
    foo(t);

报错如下:

error[E0277]: the trait bound `&mut i32: Trait` is not satisfied
--> src/main.rs:9:9
|
9 |     foo(t);
|         ^ the trait `Trait` is not implemented for `&mut i32`
|
= help: the following implementations were found:
        <&'a i32 as Trait>
= note: `Trait` is implemented for `&i32`, but not for `&mut i32`

&i32实现了特征Trait&mut i32可以转换为&i32,但是&mut i32依然无法作为Trait来使用。

点操作符

方法调用的点操作符看起来简单,实际上非常不简单,它在调用时,会发生很多魔法般的类型转换,例如:自动引用、自动解引用,强制类型转换直到类型能匹配等。

假设有一个方法foo,它有一个接收器(接收器就是self&sef&mut self参数)。如果调用value.foo(),编译器在调用foo之前,需要决定到底使用哪个Self类型来调用。现在假设value拥有类型T.

再进一步,我们使用完全限定语法来进行准确的函数调用:

  1. 首先,编译器检查它是否可以直接调用T::foo(value), 称之为值方法调用
  2. 如果上一步调用无法完成(例如方法类型错误或者特征没有针对Self进行实现,上文提到过特征不能进行强制转换),那么编译器会尝试增加自动引用,以为着编译器会尝试以下调用:<&T>::foo(value)<&mut T>::foo(value), 称之为引用方法调用
  3. 若上面两个方法依然不工作,编译器会试着解引用T,然后再进行尝试。这里使用了Deref特征 - 若T: Deref<Target = U>(T可以被解引用为U),那么编译器会使用U类型进行尝试,称之为解引用方法调用
  4. T不能被解引用,且T是一个定长类型(在编译器类型长度是已知的),那么编译器也会尝试将T从定长类型转为不定长类型,例如将[i32; 2]转为[i32]
  5. 若还是不行,那…没有那了,最后编译器大喊一声:汝欺我甚,不干了!

下面我们来用一个例子来解释上面的方法查找算法:

let array: Rc<Box<[T; 3]>> = ...;
let first_entry = array[0];

array数组的底层数据隐藏在了重重封锁之后,那么编译器如何使用array[0]这种数组原生访问语法通过重重封锁,准确的访问到数组中的第一个元素?

  1. 首先,array[0]只是Index特征的语法糖: 编译器会将array[0]转换为array.index(0)调用, 当然在调用之前,编译器会先检查array是否实现了Index特征.
  2. 接着,编译器检查Rc<Box<[T; 3]>>是否有否实现Index特征,结果是否,不仅如此,&Rc<Box<[T; 3]>>&mut Rc<Box<[T; 3]>>也没有实现.
  3. 上面的都不能工作,编译器开始对Rc<Box<[T; 3]>>进行解引用,把它转变成Box<[T; 3]>
  4. 此时继续对Box<[T; 3]>进行上面的操作:Box<[T; 3]>, &Box<[T; 3]>, and &mut Box<[T; 3]>都没有实现Index特征,所以编译器开始对Box<[T; 3]>进行解引用,然后我们得到了[T; 3]
  5. [T; 3]以及它的各种引用都没有实现Index索引(是不是很反直觉:D,在直觉中,数组都可以通过索引访问,实际上只有数组切片才可以!),它也不能再进行解引用,因此编译器只能祭出最后的大杀器:将定长转为不定长,因此[T; 3]被转换成[T],也就是数组切片,它实现了Index特征,因此最终我们可以通过index方法访问到对应的元素.

过程看起来很复杂,但是也还好挺好理解,如果你先不能彻底理解,也不要紧,等以后对Rust理解更深了,同时需要深入理解类型转换时,再来细细品读本章。

再来看看以下更复杂的例子:

fn do_stuff<T: Clone>(value: &T) 
    let cloned = value.clone();

上面例子中cloned的类型时什么?首先编译器检查能不能进行值方法调用, value的类型是&T,同时clone方法的签名也是&T: fn clone(&T) -> T,因此可以进行值方法调用, 再加上编译器知道了T实现了Clone,因此cloned的类型是T.

如果T: Clone的特征约束被移除呢?

fn do_stuff<T>(value: &T) 
    let cloned = value.clone();

首先,从直觉上来说,该方法会报错,因为T没有实现Clone特征,但是真实情况是什么呢?

我们先来推导一番。 首先通过值方法调用就不再可行,因此T没有实现Clone特征,也就无法调用Tclone方法。接着编译器尝试引用方法调用,此时T变成&T,在这种情况下,clone方法的签名如下:fn clone(&&T) -> &T, 记着我们现在对value进行了引用。 编译器发现&T实现了Clone类型(所有的引用类型都可以被复制,因为其实就是复制一份地址),因此可以可以推出cloned也是&T类型。

最终,我们复制出一份引用指针,这很合理,因为值类型T没有实现Clone,只能去复制一个指针了。

下面的例子也是自动引用生效的地方:

#[derive(Clone)]
struct Container<T>(Arc<T>);

fn clone_containers<T>(foo: &Container<i32>, bar: &Container<T>) 
    let foo_cloned = foo.clone();
    let bar_cloned = bar.clone();

推断下上面的foo_clonedbar_cloned是什么类型?提示: 关键在Container的泛型参数,一个是i32的具体类型,一个是泛型类型,其中i32实现了Clone,但是T并没有.

首先要复习一下复杂类型派生Clone的规则:一个复杂类型能否派生Clone,需要它内部的所有子类型都能进行Clone。因此Container<T>(Arc<T>)是否实现Clone的关键在于T类型是否实现了Clone.

上面代码中,Container<i32>实现了Clone特征,因此编译器可以直接进行值方法调用,此时相当于直接调用foo.clone,其中clone的函数签名是fn clone(&T) -> T,由此可以看出foo_cloned的类型是Container<i32>.

然而,bar_cloned的类型却是&Container<T>.这个不合理啊,明明我们为Container<T>派生了Clone特征,因此它也应该是Container<T>类型才对。万事皆有因,我们先来看下derive宏最终生成的代码大概是啥样的:

impl<T> Clone for Container<T> where T: Clone 
    fn clone(&self) -> Self 
        Self(Arc::clone(&self.0))
    

从上面代码可以看出,派生Clone能实现的根本是T实现了Clone特征:where T: Clone, 因此Container<T>就没有实现Clone特征。

编译器接着会去尝试引用方法调用,此时&Container<T>引用实现了Clone,最终可以得出bar_cloned的类型是&Container<T>,

当然,也可以为Container<T>手动实现Clone特征:

impl<T> Clone for Container<T> 
    fn clone(&self) -> Self 
        Self(Arc::clone(&self.0))
    

此时,编译器首次尝试值方法调用即可通过,因此bar_cloned的类型变成Container<T>.

这一块儿内容真的挺复杂,每一个坚持看完的读者都是真正的勇士,我也是:为了写好这块儿内容,作者足足花了4个小时!

变形记(Transmutes)

前方危险,敬请绕行!

类型系统,你让开!我要自己转换这些类型,不成功便成仁!虽然本书都是关于非安全的内容,我还是希望你能仔细考虑避免使用本章讲到的内容。这是你在 Rust 中所能做到的真真正正、彻彻底底、最最可怕的非安全行为, 在这里,所有的保护机制都形同虚设。

先让你看看深渊长什么样,开开眼,然后你再决定是否深入: mem::transmute<T, U>将类型T直接转成类型U,唯一的要求就是,这两个类型占用同样大小的字节数!我的天,这也算限制?这简直就是无底线的转换好吧?看看会导致什么问题:

  1. 首先也是最重要的,转换后创建一个任意类型的实例会造成无法想象的混乱,而且根本无法预测。不要把3转换成bool类型,就算你根本不会去使用该bool类型,也不要去这样转换。
  2. 变形后会有一个重载的返回类型,即使你没有指定返回类型,为了满足类型推导的需求,依然会产生千奇百怪的类型
  3. &变形为&mut是未定义的行为
    • 这种转换永远都是未定义的
    • 不,你不能这么做
    • 不要多想,你没有那种幸运
  4. 变形为一个未指定生命周期的引用会导致无界生命周期
  5. 在复合类型之间互相变换时,你需要保证它们的排列布局是一模一样的!一旦不一样,那么字段就会得到不可预期的值,这也是未定义的行为,至于你会不会因此愤怒,who cares,你都用了变形了,老兄!

对于第5条,你该如何知道内存的排列布局是一样的呢?对于repr(C)类型和repr(transparent)类型来说,它们的布局是有着精确定义的。但是对于你自己的"普通却自信"的Rust类型repr(Rust)来说,它可不是有着精确定义的。甚至同一个泛型类型的不同实例都可以有不同的内存布局。Vec<i32>Vec<u32>它们的字段可能有着相同的顺序,也可能没有。对于数据排列布局来说,什么能保证,什么不能保证目前还在Rust开发组的工作任务中呢.

你以为你之前凝视的是深渊吗?不,你凝视的只是深渊的大门。mem::transmute_copy<T, U>才是真正的深渊,它比之前的还要更加危险和不安全。它从T类型中拷贝出U类型所需的字节数,然后转换成Umem::transmute尚有大小检查,能保证两个数据的内存大小一致,现在这哥们干脆连这个也丢了,只不过U的尺寸若是比T大,会是一个未定义行为。

当然,你也可以通过原生指针转换获得unions(todo!)获得所有的这些功能,但是你将无法获得任何编译提示或者检查。原生指针转换和unions也不是魔法,无法逃避上面说的规则。

rust语言圣经28-深入类型转换(代码片段)

本文节选自<<Rust语言圣经>>一书欢迎大家加入Rust编程学院,一起学习交流:QQ群:1009730433类型转换Rust是类型安全的语言,因此在Rust中做类型转换不是一件简单的事,这一章节我们将对Rust中的类型转换进行... 查看详情

rust学习教程27-深入了解特征(代码片段)

本文节选自<<Rust语言圣经>>一书欢迎大家加入Rust编程学院,一起学习交流:QQ群:1009730433深入了解特征特征之于Rust更甚于接口之于其他语言,因此特征在Rust中很重要也相对较为复杂,我们决定把特征分为... 查看详情

rust学习教程27-深入了解特征(代码片段)

本文节选自<<Rust语言圣经>>一书欢迎大家加入Rust编程学院,一起学习交流:QQ群:1009730433深入了解特征特征之于Rust更甚于接口之于其他语言,因此特征在Rust中很重要也相对较为复杂,我们决定把特征分为... 查看详情

rust语言圣经08-数值类型深入剖析(代码片段)

...类型可以说是有计算机以来就有的类型,下面内容将深入讨论Rust的数值类型以及相关的运算符。整数和浮点数Rust使用一个相对传统的语法来创建整数(1,2,…)和浮点数(1.0,1.1,…)。整数、浮点数的运算和你在其它语言上见过的... 查看详情

tokio教程之深入异步(代码片段)

深入异步Tokio教程之深入异步https://tokio.rs/tokio/tutorial/async在这一点上,我们已经完成了对异步Rust和Tokio的相当全面的考察。现在我们将深入挖掘Rust的异步运行时模型。在教程的一开始,我们就暗示过,异步Rust采取了... 查看详情

rust语言圣经27-深入了解特征(代码片段)

...的Rust学习社区官网:https://college.rsQQ群:1009730433深入了解特征特征之于Rust更甚于接口之于其他语言,因此特征在Rust中很重要也相对较为复杂,我们决定把特征分为两篇进行介绍,第一篇在之前已经讲过,... 查看详情

rust极简教程(代码片段)

...录简介特性特征用途安装核心组件常用命令基础语法数据类型标量类型复合类型示例条件语句循环输出&输入输出输出花括号输出非基础类型输入所有权切片结构体枚举MatchIf-letOption集合vector容器String代码组织命名空间访问其... 查看详情

rust学习笔记-变量和类型(代码片段)

变量通常一门的语言的变量是没有啥可以说道的,无法就是变量命名规则:以字母、下划线和数字组成,并且数字不能开头,没啥好说的。但rust的变量奇怪的地方是,变量不可变譬如这样一个简单到不能再简... 查看详情

用了这么多年rust终于搞明白了内存分布!(代码片段)

...学习Rust会有很大的帮助,即使对于已经熟悉Rust的同学,深入数据结构分布也能帮助到调优Rust程序。接下来,我会由浅入深仔细介绍Rust的各个数据结构在内存中的分布情况,帮助大家学习Rust。先决条件Prerequisite在开始介绍之前... 查看详情

android深入理解jni类型转换方法签名和jnienv(代码片段)

相关文章Android深入理解JNI系列前言上一篇文章介绍了JNI的基本原理和注册,这一篇接着带领大家来学习JNI的数据类型转换、方法签名和JNIEnv。1.数据类型的转换首先给出上一篇文章中android_media_MediaRecorder.cpp中的android_media_Medi... 查看详情

rust学习教程15-元组tuple(代码片段)

本文节选自<<Rust语言圣经>>一书欢迎大家加入Rust编程学院,一起学习交流:QQ群:1009730433元组元组也是复合类型的一种,因此它是由多种类型组合到一起形成的。元组的长度是固定的,且在声明后,无... 查看详情

rust学习教程15-元组tuple(代码片段)

本文节选自<<Rust语言圣经>>一书欢迎大家加入Rust编程学院,一起学习交流:QQ群:1009730433元组元组也是复合类型的一种,因此它是由多种类型组合到一起形成的。元组的长度是固定的,且在声明后,无... 查看详情

rust学习教程30-panic原理剖析(代码片段)

本文节选自<<Rust语言圣经>>一书欢迎大家加入Rust编程学院,一起学习交流:QQ群:1009730433panic深入剖析在正式开始之前,先来思考一个问题:假设我们想要从文件读取数据,如果失败,你有没有好的办法... 查看详情

rust学习教程30-panic原理剖析(代码片段)

本文节选自<<Rust语言圣经>>一书欢迎大家加入Rust编程学院,一起学习交流:QQ群:1009730433panic深入剖析在正式开始之前,先来思考一个问题:假设我们想要从文件读取数据,如果失败,你有没有好的办法... 查看详情

rust语言教程-从熟悉的部分开始(代码片段)

...部分开始学习。一般我们写代码,使用的主要是数据类型、控制结构和函数。我们就从这三部分开始。数据类型与Go一样,Rust的定义语句数据也是放在变量名后面的& 查看详情

深入浅出rust异步编程之tokio(代码片段)

深入浅出Rust异步编程之Tokio本文以tokio为例简单介绍Rust异步编程相关的一些知识。首先让我们看看为什么使用rust来进行异步编程。这里tokio官方给出了一个性能测试的对比,可以看到tokio是性能最好,实际上运行这个基准测试的... 查看详情

typescript深入学习typescript对象类型(代码片段)

...主,学习TypeScript不迷路!好嘞,言归正传,让我们开始深入学习TypeScript对象类型吧:interfacePaintOptionsx?:number;y?:number;使用接口定义了一个对象类型,其中的属性都为可选属性,在[【TypeScript】TypeScript常用类型(上篇)]中我们已... 查看详情

rust学习教程33-hashmap(代码片段)

本文节选自<<Rust语言圣经>>一书欢迎大家加入Rust编程学院,一起学习交流:QQ群:1009730433KV存储HashMap和动态数组一样,HashMap也是Rust标准库中提供的集合类型,但是又与动态数组不同,HashMap中存储的是... 查看详情