ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Rust By Example 类型系统指南:Casting、字面量、类型推断与类型别名实战

Rust By Example 类型系统指南:Casting、字面量、类型推断与类型别名实战 文档教程【免费下载链接】rust-by-exampleLearn Rust with examples (Live code editor included)项目地址https://gitcode.com/gh_mirrors/ru/rust-by-example点击查看免费下载Rust 的类型系统是这门语言安全与效率的基石它不做任何隐式类型转换却提供了as关键字、带后缀的字面量、强大的类型推断引擎以及type别名机制让开发者可以在显式、可预期的前提下灵活地定义和变更原始类型与用户自定义类型的形态。本文以 Rust By ExampleRBE仓库的 Types 章节 为骨架结合仓库内其他章节的源码示例完整讲解这四类类型操作机制的规则、边界行为与工程实践帮助读者在阅读完本文后能准确预测每一条类型转换的结果、写出类型意图明确且可维护的 Rust 代码。章节导览Rust 处理类型的四种途径在 src/types.md 中Rust 为改变或定义类型提供了四种互相配合的机制这也是本篇文章的四个主体小节Casting类型转换在原始类型之间进行显式转换使用as关键字Literals字面量通过后缀为数值字面量指定期望类型Inference类型推断利用编译器的推断能力让变量类型由使用方式决定Aliasing类型别名用type语句为已有类型赋予新名字。在仓库目录树中这四个主题分别对应 types/cast.md、types/literals.md、types/inference.md 和 types/alias.md 四个子页面并在 SUMMARY.md 中作为 Types 章节的子条目被收录。阅读顺序上建议从 Casting 开始因为它涉及最多的边界规则是大多数初学者最先接触也最容易踩坑的部分。Casting用as关键字做显式转换无隐式转换的设计哲学Rust 在原始类型之间不提供任何隐式类型转换coercion。这意味着下面的代码是无法编译的let decimal 65.4321_f32; // Error! No implicit conversion let integer: u8 decimal;这一点与 C 语言形成鲜明对比C 会静默地在类型之间做隐式转换容易在精度丢失、符号翻转等场景下埋下难以察觉的 bug。Rust 的立场是——类型变化必须显式声明让程序员对每一步转换都知情。显式转换通过as关键字完成例如let integer decimal as u8; // 65.4321_f32 - 65 let character integer as char;值得注意的是转换规则存在边界浮点数不能直接转换为char。decimal as char这类写法会直接触发编译错误必须像上面那样先转为整数再转为字符。整数之间的转换规则保留低位与模运算整数类型之间的转换规则总体遵循 C 语言惯例但有一个关键差异凡是 C 语言中属于未定义行为undefined behavior的情形在 Rust 中都有完全确定的定义。也就是说Rust 中所有整数间的转换行为都是良定义well-defined的。其底层机制可以概括为保留最低有效位LSB向最高有效位MSB方向截断。当目标类型无法容纳源值时相当于反复加减T::MAX 1直到值落入目标类型范围。看几个仓库 types/cast.md 中的完整示例// Suppress all errors from casts which overflow. #![allow(overflowing_literals)] fn main() { // 1000 已经能放进 u16 println!(1000 as a u16 is: {}, 1000 as u16); // 1000 // 1000 - 256 - 256 - 256 232 // 底层只保留最低 8 个有效位其余高位被截断 println!(1000 as a u8 is : {}, 1000 as u8); // 232 // -1 256 255 println!( -1 as a u8 is : {}, (-1i8) as u8); // 255 // 对于正数这等价于取模 println!(1000 mod 256 is : {}, 1000 % 256); // 232 }上面的#![allow(overflowing_literals)]是溢出字面量的 lint 抑制属性去掉它时这类会溢出的转换将产生编译错误而非运行时静默截断。该属性属于仓库 attribute 章节 讲解的 attribute 机制之一相关属性在 attribute/unused.md 等子页中也有类似应用。有符号类型补码解释的视角当目标是有符号类型时位运算层面的结果与先转为对应无符号类型完全一致区别只在于最高位符号位为 1 时按二补数twos complement解释为负数// 除非它本来就放得下 println!( 128 as a i16 is: {}, 128 as i16); // 128 // 边界情形128 在 8 位补码表示下就是 -128 println!( 128 as a i8 is : {}, 128 as i8); // -128 // 承接上面的例子1000 as u8 - 232 println!(1000 as a u8 is : {}, 1000 as u8); // 232 // 而 232 在 8 位补码表示下是 -24 println!( 232 as a i8 is : {}, 232 as i8); // -24浮点到整数的饱和转换与 unsafe 捷径从 Rust 1.45 起as关键字在浮点转整数时执行的是饱和转换saturating cast当浮点值超出目标类型上界或低于下界时结果取被越过的那个边界值NaN转换为 0println!( 300.0 as u8 is : {}, 300.0_f32 as u8); // 255饱和到上界 println!(-100.0 as u8 is : {}, (-100.0_f32) as u8); // 0饱和到下界 println!( nan as u8 is : {}, f32::NAN as u8); // 0饱和转换会带来少量运行时开销。标准库为此提供了unsafe的to_int_unchecked方法族来绕过该开销但结果可能溢出并返回非健全unsound的值必须谨慎使用unsafe { // 300.0 as u8 是 44绕过了饱和直接截断 println!( 300.0 as u8 is : {}, 300.0_f32.to_int_unchecked::u8()); // -100.0 as u8 是 156 println!(-100.0 as u8 is : {}, (-100.0_f32).to_int_unchecked::u8()); // nan as u8 是 0 println!( nan as u8 is : {}, f32::NAN.to_int_unchecked::u8()); }这体现了 Rust 的类型转换设计的一贯原则安全的默认路径as的饱和语义优先性能捷径必须显式声明为unsafe并自行承担后果。Literals用后缀把类型写进字面量带后缀的字面量数值字面量可以通过在末尾添加类型后缀来直接标注类型例如要表示类型为i32的 42写作42i32。仓库 types/literals.md 给出了完整的对比示例fn main() { // 带后缀的字面量初始化时类型即确定 let x 1u8; let y 2u32; let z 3f32; // 不带后缀的字面量类型取决于使用方式 let i 1; let f 1.0; // size_of_val 返回变量占用的字节数 println!(size of x in bytes: {}, std::mem::size_of_val(x)); println!(size of y in bytes: {}, std::mem::size_of_val(y)); println!(size of z in bytes: {}, std::mem::size_of_val(z)); println!(size of i in bytes: {}, std::mem::size_of_val(i)); println!(size of f in bytes: {}, std::mem::size_of_val(f)); }输出结果为xu8占 1 字节、yu32占 4 字节、zf32占 4 字节、ii32占 4 字节、ff64占 8 字节。无后缀字面量的默认类型不带后缀的数值字面量其类型由使用语境决定若不存在任何约束编译器默认整数使用i32、浮点数使用f64。这也解释了为什么上面的i和f的字节数分别是 4 和 8。字面量书写形式的补充仓库更早的 primitives/literals.md 对字面量写法给出了更完整的补充与类型后缀配合使用非常实用整数可用0x十六进制、0o八进制、0b二进制前缀书写数字中可插入下划线提升可读性如1_000等价于1000、0.000_001等价于0.000001Rust 支持科学计数法E 记数法如1e6、7.6e-4其关联类型为f64后缀常与进制前缀连用例如0b0011u32 0b0101、1_000_000u32。全路径调用std::mem::size_of_val示例中的std::mem::size_of_val是一个以全路径调用的函数Rust 代码可以按逻辑单元划分成模块module此处的size_of_val定义在mem模块中而mem模块又位于stdcrate之内。模块与 crate 的组织方式可分别参见仓库的 modules 章节 与 crates 章节。Inference让编译器从使用方式推断类型Rust 的类型推断引擎比只看初始化表达式的类型要智能得多它还会考察变量在初始化之后的用法据此反推变量的类型。仓库 types/inference.md 给出了一个进阶示例fn main() { // 因为有注解编译器知道 elem 的类型是 u8 let elem 5u8; // 创建一个空向量可增长数组 let mut vec Vec::new(); // 此刻编译器并不知道 vec 的具体类型只知道它是某个类型的向量Vec_ // 把 elem 插入向量 vec.push(elem); // 由此编译器推断出 vec 是 u8 的向量Vecu8 println!({:?}, vec); }关键步骤在于vec.push(elem)Vec::new()声明时没有任何类型信息Vec_但push要求参数类型与向量的元素类型一致而elem已确定是u8编译器便由此锁定vec为Vecu8。读者可以自行尝试注释掉vec.push(elem)一行此时向量元素类型无从推断println!({:?}, vec)将因无法确定Vec的元素类型而编译失败——这正是推断依赖使用方式的直观证明。这种无需手写全部类型注解的能力极大减轻了书写负担开发者只标注关键锚点如5u8其余类型由编译器推导程序依旧保持完全的类型安全。Aliasing用type声明类型别名语法与命名规则type语句可以为既有类型起一个新的名字。仓库 types/alias.md 开篇即强调命名规则别名必须使用UpperCamelCase大驼峰命名否则编译器会给出警告唯一的例外是usize、f32这类原始类型本身的小写命名。// NanoSecond、Inch、U64 都是 u64 的新名字 type NanoSecond u64; type Inch u64; type U64 u64; fn main() { // NanoSecond Inch U64 u64 let nanoseconds: NanoSecond 5 as u64; let inches: Inch 2 as U64; // 注意类型别名 *不* 提供任何额外类型安全 // 因为别名 *不是* 新类型 println!({} nanoseconds {} inches {} unit?, nanoseconds, inches, nanoseconds inches); }上面最后一行nanoseconds inches能够直接相加并输出正是因为NanoSecond、Inch与u64在类型层面完全等价——别名只是名字替换不产生新的类型身份。别名的主要用途减少样板代码别名最重要的价值在于减少样板代码boilerplate。最经典的例子是标准库中的io::ResultT它就是ResultT, io::Error的类型别名。若没有它每个涉及 I/O 的函数签名都要写全ResultT, io::Error冗长且易错。仓库 error/result/result_alias.md 把这一用法发挥到极致当某个模块内大量函数的错误类型一致时一个模块级别名即可统一概括所有相关的Resultuse std::num::ParseIntError; // 为错误类型为 ParseIntError 的 Result 定义泛型别名 type AliasedResultT ResultT, ParseIntError; // 用别名指代特定的 Result 类型 fn multiply(first_number_str: str, second_number_str: str) - AliasedResulti32 { first_number_str.parse::i32().and_then(|first_number| { second_number_str.parse::i32().map(|second_number| first_number * second_number) }) } // 这里别名同样帮我们省去了篇幅 fn print(result: AliasedResulti32) { match result { Ok(n) println!(n is {}, n), Err(e) println!(Error: {}, e), } } fn main() { print(multiply(10, 2)); print(multiply(t, 2)); }注意这里别名是泛型的type AliasedResultT ResultT, ParseIntError保留了T这个类型参数因而可以用于任意成功值类型的Result。这与 generics 章节 讲解的泛型机制一脉相承。别名 vs 新类型newtype的边界理解别名不是新类型这一点至关重要。仓库 generics/new_types.md 展示了与之互补的newtype 惯用法用struct Miles(f64);包裹原始类型从而获得编译期必须传入正确类型的保证struct Miles(f64); struct Kilometers(f64); fn is_a_marathon(distance: Miles) - bool { distance.0 26.2 } fn main() { let distance Miles(30.0); // 传入 Kilometers 会编译失败 // println!(Is a marathon? {}, is_a_marathon(distance_km)); }对比之下可以总结出两条工程选型原则类型别名type类型身份完全等价零运行时开销用于减少书写、统一命名不增加类型安全newtype单字段元组结构体创建全新的类型身份编译期强制类型区分用于防止把不同语义的量混用如英里与公里但需要额外的解包操作——通过元组字段访问distance.0或解构语法let Miles(v) distance取回原始值。与其他章节的关联类型别名与 Rust 的转换体系关系密切仓库 conversion/from_into.md 讲解的From/Intotrait 负责值层面的类型转换而本文的as转换与type别名负责类型层面的操作conversion 章节 的入口页进一步覆盖了TryFrom/TryInto以及String互转等更高级的转换路径。若想深入io::Result的实际语义可继续阅读仓库的 std/result 章节 与 error 章节。小结四种机制的组合运用Rust 类型操作的四种机制在工程中通常组合出现形成一套完整的类型管理工具链机制关键字/语法作用关键注意事项Castingas原始类型间显式转换无隐式转换整数转换良定义浮点转整数自 1.45 起为饱和转换Literals类型后缀如42i32为字面量指定类型无后缀时默认i32/f64支持0x/0o/0b、下划线、科学计数法Inference无编译器自动由使用方式推导类型变量先声明后使用关键锚点用后缀或注解固定Aliasingtype 新名 旧类型减少样板代码、统一命名别名非新类型不提供额外类型安全需强类型区分时用 newtype掌握这四类机制就能在 Rust 中做到类型意图显式、转换行为可预期、代码书写简洁——这正是 Rust 类型系统在安全与效率之间的平衡之道。文中所有代码示例均可直接在 Rust By Example 仓库对应的.md源文件中找到并在线运行仓库 book.toml 配置了mdbook的playpen可编辑运行环境建议读者对照原文逐一实验尤其是注释掉推断示例中的vec.push(elem)以及修改转换边界值亲身体验编译器的反馈。赞分享文档教程【免费下载链接】rust-by-exampleLearn Rust with examples (Live code editor included)项目地址https://gitcode.com/gh_mirrors/ru/rust-by-example点击查看免费下载相关推荐Rust By Example 基础类型全解Rust 原始类型Primitives、标量与复合类型及类型标注实战指南Rust By Example 基础类型全解Rust 原始类型Primitives、标量与复合类型及类型标注实战指南 导读 本篇围绕 Rust By Ex文档教程Rust 字面量与运算符完全指南基于 rust-by-example 的数值类型实战Rust 字面量与运算符完全指南基于 rust by example 的数值类型实战 整数 1 、浮点数 1.2 、字符 a 、字符串 abc 、布尔文档教程TractorMod深度评测如何用拖拉机模组提升50%农场效率TractorMod深度评测如何用拖拉机模组提升50%农场效率 TractorMod 是《星露谷物语》中最强大的农场管理模组之一这款拖拉机模组能让你在游戏开发上一篇Whisper-WebUI完整配置指南构建专业级语音转字幕平台下一篇HS2-HF补丁3步解锁Honey Select 2完整游戏体验的终极解决方案创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表