当前位置: 首页 > news >正文

Rust 登堂 之 类型转换(三)

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 已经有了一定的理解,那么一眼就能看出这段代码注定会报错,因为a 和 b 拥有不同的类型, Rust 不允许两种不同的类型进行比较。

解决办法很简单,只要把 a 转换成 i32 类型即可, Rust 中内置了一些基本类型之间的转换,这里使用 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 += 2;
}assert_eq!(values[1],3);

强制类型转换的边角知识

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

TryInto 转换

在一些场景中,使用as 关键字会有比较大的限制,如果你想要在类型转换上拥有完全的控制而不依赖内置的转换,例如处理转换错误,那么可以使用TryInto

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

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

try_info 会尝试进行依次转换,并返回一个Result ,此时就可以对其进行响应的错误处理,由于我们的例子,只是为了快速测试,因此使用 unwrap 方法, 该方法在发现错误是,会直接调用 panic 导致程序的崩溃退出,在实际项目中,请不要这么使用,具体见panic 部分。

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

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类型,后者明显不足以承载这么大的值。

通用类型转换

虽然 as 和 TryInto 很强大,类型是可以进行隐式强制转换的,虽然这些转换弱化了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 ,&self ,&mut self 参数),如果调用 value.foo() ,编译器在调用 foo 之前, 需要决定到底使用哪个 Self 类型来调用,现在假设value 拥有类型 T 

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

        首先,编译器检查它是否可以直接调用 T::foo(value), 称职位值方法调用

        如果上一步调用无法完成(例如方法类型错误或者特征没有针对Self 进行实现,上文提到过特征不能进行强制转换),那么编译器会尝试增加自动引用,例如会尝试以下调用: <&T>::foo(value) 和 <&mut T>::foo(value) ,称之为引用方法调用

        若上面两个方法依然不工作,编译器会试着解引用T ,然后在进行尝试,这里使用了Deref特征-- 若 T: Deref<Target = U> (T 可以被解引用为 U),那么编译器会使用U 类型进行尝试,称之为解引用方法调用

        若T 不能被解引用,且 T 是一个定长类型,在编译期类型长度是已知的,那么编译器也会尝试将T 从定长类型转为不定长类型,例如将 [i32;2 ] 转为 [i32]

        若还是不行,那。。。没有那了 , 最后编译器大喊一声,汝欺我甚,

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

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

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

        首先, array[0] 只是 Index 特征的语法糖: 编译器会将 array[0] 转换为 array.index(0) 调用,当然在调用之前,编译器会先检查 array 是否实现了 Index 特征

        接着 编译器检查 Rc<Box<[T; 3]>> 是否有实现 Index 特征,结果是否,不仅如此,&Rc<Box<[T; 3]>> 与 &mut Rc<Box<[T; 3]>> 也没有实现

        上面的都不能工作,编译器开始对Rc<Box<[T;3]>> 进行解引用,把它转编程 Box<[T; 3]>

        此时继续对 Box<[T; 3]> 进行上面的操作 : Box<[T; 3]> ,&Box<[T; 3]> ,和 &mut Box<[T; 3]> 都没有实现 Index 特征,所以编译器开始对 Box<[T; 3]> 进行解引用,然后我们得到了 [T ; 3]

        [T; 3] 以及它的各种引用都没有实现 Index  索引是不是很反直觉:D ,在直觉中, 数组都可以通过索引访问,实际上只有数组切片才可以! ,它也不能再进行解引用,因此编译器只能祭出最后的大杀器: 将定长转为不定长,因此[T; 3] 被转换成[T] ,也就是数组切片,它实现了 Index 特征,因此最终我们可以通过 index 方法访问到对应的元素

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

过程看起来很复杂,但是也还好,挺好理解,如果你现在不能彻底理解,也不要紧,等以后对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 特征,也就无法调用 T 的clone 方法, 接着编译器尝试引用方法调用,此时 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_cloned 和 bar_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>

这一块儿内容真的挺复杂,每一个坚持看网的读者都是真正的勇士,

变形记(Transmutes)

前方危险,敬请绕行

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

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

        首先也是最重要的,转换后创建一个任意类型的实例会造成无法想象的混乱,而且根本无法预测,不要把 3 转换成 bool 类型,就算你根本不会去使用该bool 类型,也不要去这样转换。

        变形后会有一个重载的返回类型,即使你根本不会去使用该bool 类型,为了码住类型推导的需求,依然会产生千奇百怪的类型

        将 & 变形为 &mut 是未定义的行为

                这种转换永远都是未定义的。 

                不,你不能这么做

                不要多项,你没有那种幸运

        变形为一个未指定生命周期的引用会导致无界生命周期

        在复合类型自建互相变换是,你需要保证它们的排里布局是一摸一样的,一旦不一样,那么字段就会得到不可预期的值,这也是未定义的行为,至于你会不会因此愤怒,

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

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

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

transmute 虽然危险,但作为一本工具书,知识当然要全面,下面列举两个有用的 transmute 应用场景 :)。

将裸指针变成函数指针

fn foo() -> i32 {0
}let pointer = foo as *const();let  function = unsafe{// 将裸指针转换为函数指针std::mem::transmute::<*const() ,fn() -> i32>(pointer)
}
assert_eq!(functin(),0);

延长生命周期,或者缩短一个静态生命周期寿命

struct R<'a>(&'a i32);// 将 'b生命周期延长至 'static 生命周期 
unsafe fn extend_lifetime<'b>(r: R<'b>) -> R<'static>{std::mem::transmute::<R<'b>,R<'static>>(r)
}// 将 'static 生命周期缩短至 'c 生命周期
unsafe fn  shorten_invariant_lifetime<'b,'c>(r: &'b mut R<'static>) -> &'b mut R<'c> {std::mem::transmute::<&'b mut R<'static>,&'b mut R<'c>>(r)
}

http://www.dtcms.com/a/355344.html

相关文章:

  • 趣味学Rust基础篇(数据类型)
  • Python Discord Logo
  • 【SpringAI】快速上手,详解项目快速集成主流大模型DeepSeek,ChatGPT
  • 操作系统-基础知识面试常见篇
  • 算法之排序
  • java后端的各种注解
  • 基于 PyTorch 构建 Dataset 与 DataLoader:从 TXT 文件读取到新增类别全流程指南
  • AI大模型企业落地指南-笔记02
  • Spring 框架中事务传播行为的定义
  • 146. LRU缓存
  • python使用sqlcipher4对sqlite数据库加密
  • 【论文阅读】基于人工智能的下肢外骨骼辅助康复方法研究综述
  • 【电源专题】隐形守护者:防爆锂电池如何守护高危环境的安全防线
  • UE5提升分辨率和帧率的方法
  • 网站日志里面老是出现{pboot:if((\x22file_put_co\x22.\x22ntents\x22)(\x22temp.php\x22.....
  • Leetcode 深度优先搜索 (15)
  • 【大前端】React Native(RN)跨端的原理
  • 比较两个字符串的大小
  • 使用CDN后如何才不暴露IP
  • EtherNet/IP 转 Modbus 协议网关(三格电子)
  • SOME/IP-SD通信中的信息安全保证
  • leetcode_73 矩阵置零
  • (LeetCode 面试经典 150 题) 103. 二叉树的锯齿形层序遍历(广度优先搜索bfs)
  • [n8n] 工作流数据库管理SQLite | 数据访问层-REST API服务
  • 解决PyCharm打开PowerShell终端报错找不到conda-hook.ps1文件
  • 前端javascript在线生成excel,word模板-通用场景(免费)
  • Spring Boot 定时任务入门
  • 使用Java实现PDF文件安全检测:防止恶意内容注入
  • ubuntu20安装lammps
  • PDFMathTranslate,完全免费的电脑 PDF 文档翻译软件