{"doc_urls":["about-book.html#配套练习题","about-book.html#创作感悟","about-book.html#-贡献者","into-rust.html#rust-发展历程","into-rust.html#为何又来一门新语言","into-rust.html#缓解内卷","into-rust.html#效率","into-rust.html#个人的好处","into-rust.html#团队的好处","into-rust.html#开源","into-rust.html#相比其他语言-rust-的优势","into-rust.html#使用现状","into-rust.html#rust-语言版本更新","into-rust.html#总结","first-try/sth-you-should-not-do.html#避免从入门到放弃","first-try/sth-you-should-not-do.html#避免试一试的心态","first-try/sth-you-should-not-do.html#深入学习一本好书","first-try/sth-you-should-not-do.html#千万别从链表或图开始练手","first-try/sth-you-should-not-do.html#仔细阅读编译错误","first-try/sth-you-should-not-do.html#不要强制自己使用其它编程语言的最佳实践来写-rust","first-try/sth-you-should-not-do.html#总结","community.html#rust语言中文网","first-try/intro.html#寻找牛刀以便小试","first-try/installation.html#安装-rust","first-try/installation.html#在-linux-或-macos-上安装-rustup","first-try/installation.html#安装-c-语言编译器非必需","first-try/installation.html#在-windows-上安装-rustup","first-try/installation.html#卸载","first-try/installation.html#检查安装是否成功","first-try/installation.html#本地文档","first-try/editor.html#墙推-vscode","first-try/editor.html#安装-vscode-的-rust-插件","first-try/editor.html#安装其它好用的插件","first-try/cargo.html#认识-cargo","first-try/cargo.html#创建一个你好世界项目","first-try/cargo.html#运行项目","first-try/cargo.html#cargo-check","first-try/cargo.html#cargotoml-和-cargolock","first-try/cargo.html#package-配置段落","first-try/cargo.html#定义项目依赖","first-try/cargo.html#基于-cargo-的项目组织结构","first-try/hello-world.html#不仅仅是-hello-world","first-try/hello-world.html#多国语言的世界你好","first-try/hello-world.html#rust-语言初印象","first-try/slowly-downloading.html#下载依赖很慢或卡住","first-try/slowly-downloading.html#下载很慢","first-try/slowly-downloading.html#下载卡住","basic/intro.html#rust-基本概念","basic/variable.html#变量绑定与解构","basic/variable.html#为何要手动设置变量的可变性","basic/variable.html#变量命名","basic/variable.html#变量绑定","basic/variable.html#变量可变性","basic/variable.html#使用下划线开头忽略未使用的变量","basic/variable.html#变量解构","basic/variable.html#解构式赋值","basic/variable.html#变量和常量之间的差异","basic/variable.html#变量遮蔽shadowing","basic/variable.html#课后练习","basic/base-type/index.html#基本类型","basic/base-type/index.html#类型推导与标注","basic/base-type/numbers.html#数值类型","basic/base-type/numbers.html#整数类型","basic/base-type/numbers.html#浮点类型","basic/base-type/numbers.html#数字运算","basic/base-type/numbers.html#位运算","basic/base-type/numbers.html#序列range","basic/base-type/numbers.html#有理数和复数","basic/base-type/numbers.html#总结","basic/base-type/numbers.html#课后练习","basic/base-type/char-bool.html#字符布尔单元类型","basic/base-type/char-bool.html#字符类型char","basic/base-type/char-bool.html#布尔bool","basic/base-type/char-bool.html#单元类型","basic/base-type/char-bool.html#课后练习","basic/base-type/statement-expression.html#语句和表达式","basic/base-type/statement-expression.html#语句","basic/base-type/statement-expression.html#表达式","basic/base-type/statement-expression.html#课后练习","basic/base-type/function.html#函数","basic/base-type/function.html#函数要点","basic/base-type/function.html#函数参数","basic/base-type/function.html#函数返回","basic/base-type/function.html#课后练习","basic/ownership/index.html#所有权和借用","basic/ownership/ownership.html#所有权","basic/ownership/ownership.html#一段不安全的代码","basic/ownership/ownership.html#栈stack与堆heap","basic/ownership/ownership.html#所有权原则","basic/ownership/ownership.html#变量绑定背后的数据交互","basic/ownership/ownership.html#函数传值与返回","basic/ownership/ownership.html#课后练习","basic/ownership/borrowing.html#引用与借用","basic/ownership/borrowing.html#引用与解引用","basic/ownership/borrowing.html#不可变引用","basic/ownership/borrowing.html#可变引用","basic/ownership/borrowing.html#悬垂引用dangling-references","basic/ownership/borrowing.html#借用规则总结","basic/ownership/borrowing.html#课后练习","basic/compound-type/intro.html#复合类型","basic/compound-type/string-slice.html#字符串","basic/compound-type/string-slice.html#切片slice","basic/compound-type/string-slice.html#字符串字面量是切片","basic/compound-type/string-slice.html#什么是字符串","basic/compound-type/string-slice.html#string-与-str-的转换","basic/compound-type/string-slice.html#字符串索引","basic/compound-type/string-slice.html#字符串切片","basic/compound-type/string-slice.html#操作字符串","basic/compound-type/string-slice.html#字符串转义","basic/compound-type/string-slice.html#操作-utf-8-字符串","basic/compound-type/string-slice.html#字符串深度剖析","basic/compound-type/string-slice.html#课后练习","basic/compound-type/string-slice.html#引用资料","basic/compound-type/tuple.html#元组","basic/compound-type/tuple.html#用模式匹配解构元组","basic/compound-type/tuple.html#用--来访问元组","basic/compound-type/tuple.html#元组的使用示例","basic/compound-type/tuple.html#课后练习","basic/compound-type/struct.html#结构体","basic/compound-type/struct.html#结构体语法","basic/compound-type/struct.html#结构体的内存排列","basic/compound-type/struct.html#元组结构体tuple-struct","basic/compound-type/struct.html#单元结构体unit-like-struct","basic/compound-type/struct.html#结构体数据的所有权","basic/compound-type/struct.html#使用-derivedebug-来打印结构体的信息","basic/compound-type/struct.html#课后练习","basic/compound-type/enum.html#枚举","basic/compound-type/enum.html#枚举值","basic/compound-type/enum.html#同一化类型","basic/compound-type/enum.html#option-枚举用于处理空值","basic/compound-type/enum.html#课后练习","basic/compound-type/array.html#数组","basic/compound-type/array.html#创建数组","basic/compound-type/array.html#访问数组元素","basic/compound-type/array.html#数组切片","basic/compound-type/array.html#总结","basic/compound-type/array.html#课后练习","basic/flow-control.html#流程控制","basic/flow-control.html#使用-if-来做分支控制","basic/flow-control.html#使用-else-if-来处理多重条件","basic/flow-control.html#循环控制","basic/flow-control.html#for-循环","basic/flow-control.html#continue","basic/flow-control.html#break","basic/flow-control.html#while-循环","basic/flow-control.html#loop-循环","basic/flow-control.html#课后练习","basic/match-pattern/intro.html#模式匹配","basic/match-pattern/match-if-let.html#match-和-if-let","basic/match-pattern/match-if-let.html#match-匹配","basic/match-pattern/match-if-let.html#if-let-匹配","basic/match-pattern/match-if-let.html#matches宏","basic/match-pattern/match-if-let.html#变量遮蔽","basic/match-pattern/match-if-let.html#课后练习","basic/match-pattern/option.html#解构-option","basic/match-pattern/option.html#匹配-option","basic/match-pattern/pattern-match.html#模式适用场景","basic/match-pattern/pattern-match.html#模式","basic/match-pattern/pattern-match.html#所有可能用到模式的地方","basic/match-pattern/all-patterns.html#全模式列表","basic/match-pattern/all-patterns.html#匹配字面值","basic/match-pattern/all-patterns.html#匹配命名变量","basic/match-pattern/all-patterns.html#单分支多模式","basic/match-pattern/all-patterns.html#通过序列--匹配值的范围","basic/match-pattern/all-patterns.html#解构并分解值","basic/match-pattern/all-patterns.html#忽略模式中的值","basic/match-pattern/all-patterns.html#匹配守卫提供的额外条件","basic/match-pattern/all-patterns.html#绑定","basic/match-pattern/all-patterns.html#课后练习","basic/method.html#方法-method","basic/method.html#定义方法","basic/method.html#--运算符到哪去了","basic/method.html#带有多个参数的方法","basic/method.html#关联函数","basic/method.html#多个-impl-定义","basic/method.html#为枚举实现方法","basic/method.html#课后练习","basic/trait/intro.html#泛型和特征","basic/trait/generic.html#泛型-generics","basic/trait/generic.html#泛型详解","basic/trait/generic.html#结构体中使用泛型","basic/trait/generic.html#枚举中使用泛型","basic/trait/generic.html#方法中使用泛型","basic/trait/generic.html#const-泛型rust-151-版本引入的重要特性","basic/trait/generic.html#泛型的性能","basic/trait/generic.html#课后练习","basic/trait/trait.html#特征-trait","basic/trait/trait.html#定义特征","basic/trait/trait.html#为类型实现特征","basic/trait/trait.html#使用特征作为函数参数","basic/trait/trait.html#特征约束trait-bound","basic/trait/trait.html#函数返回中的-impl-trait","basic/trait/trait.html#修复上一节中的-largest-函数","basic/trait/trait.html#通过-derive-派生特征","basic/trait/trait.html#调用方法需要引入特征","basic/trait/trait.html#几个综合例子","basic/trait/trait.html#课后练习","basic/trait/trait-object.html#特征对象","basic/trait/trait-object.html#特征对象定义","basic/trait/trait-object.html#特征对象的动态分发","basic/trait/trait-object.html#self-与-self","basic/trait/trait-object.html#特征对象的限制","basic/trait/trait-object.html#课后练习","basic/trait/advance-trait.html#深入了解特征","basic/trait/advance-trait.html#关联类型","basic/trait/advance-trait.html#默认泛型类型参数","basic/trait/advance-trait.html#调用同名的方法","basic/trait/advance-trait.html#特征定义中的特征约束","basic/trait/advance-trait.html#在外部类型上实现外部特征newtype","basic/trait/advance-trait.html#课后练习","basic/collections/intro.html#集合类型","basic/collections/vector.html#动态数组-vector","basic/collections/vector.html#创建动态数组","basic/collections/vector.html#vecnew","basic/collections/vector.html#vec","basic/collections/vector.html#更新-vector","basic/collections/vector.html#vector-与其元素共存亡","basic/collections/vector.html#从-vector-中读取元素","basic/collections/vector.html#下标索引与-get-的区别","basic/collections/vector.html#同时借用多个数组元素","basic/collections/vector.html#迭代遍历-vector-中的元素","basic/collections/vector.html#存储不同类型的元素","basic/collections/vector.html#课后练习","basic/collections/hashmap.html#kv-存储-hashmap","basic/collections/hashmap.html#创建-hashmap","basic/collections/hashmap.html#使用-new-方法创建","basic/collections/hashmap.html#使用迭代器和-collect-方法创建","basic/collections/hashmap.html#所有权转移","basic/collections/hashmap.html#查询-hashmap","basic/collections/hashmap.html#更新-hashmap-中的值","basic/collections/hashmap.html#哈希函数","basic/collections/hashmap.html#课后练习","basic/lifetime.html#认识生命周期","basic/lifetime.html#悬垂指针和生命周期","basic/lifetime.html#借用检查","basic/lifetime.html#函数中的生命周期","basic/lifetime.html#生命周期标注语法","basic/lifetime.html#结构体中的生命周期","basic/lifetime.html#生命周期消除","basic/lifetime.html#方法中的生命周期","basic/lifetime.html#静态生命周期","basic/lifetime.html#一个复杂例子-泛型特征约束","basic/lifetime.html#课后练习","basic/lifetime.html#总结","basic/result-error/intro.html#返回值和错误处理","basic/result-error/intro.html#rust-的错误哲学","basic/result-error/panic.html#panic-深入剖析","basic/result-error/panic.html#panic-与不可恢复错误","basic/result-error/panic.html#被动触发","basic/result-error/panic.html#主动调用","basic/result-error/panic.html#backtrace-栈展开","basic/result-error/panic.html#panic-时的两种终止方式","basic/result-error/panic.html#线程-panic-后程序是否会终止","basic/result-error/panic.html#何时该使用-panic","basic/result-error/panic.html#panic-原理剖析","basic/result-error/panic.html#课后练习","basic/result-error/result.html#可恢复的错误-result","basic/result-error/result.html#对返回的错误进行处理","basic/result-error/result.html#失败就-panic-unwrap-和-expect","basic/result-error/result.html#传播错误","basic/result-error/result.html#传播界的大明星-","basic/result-error/result.html#课后练习","basic/crate-module/intro.html#包和模块","basic/crate-module/crate.html#包和-package","basic/crate-module/crate.html#定义","basic/crate-module/crate.html#项目-package","basic/crate-module/crate.html#课后练习","basic/crate-module/module.html#模块-module","basic/crate-module/module.html#创建嵌套模块","basic/crate-module/module.html#模块树","basic/crate-module/module.html#用路径引用模块","basic/crate-module/module.html#代码可见性","basic/crate-module/module.html#使用-super-引用模块","basic/crate-module/module.html#使用-self-引用模块","basic/crate-module/module.html#结构体和枚举的可见性","basic/crate-module/module.html#模块与文件分离","basic/crate-module/module.html#课后练习","basic/crate-module/use.html#使用-use-及受限可见性","basic/crate-module/use.html#基本引入方式","basic/crate-module/use.html#避免同名引用","basic/crate-module/use.html#引入项再导出","basic/crate-module/use.html#使用第三方包","basic/crate-module/use.html#使用--简化引入方式","basic/crate-module/use.html#使用--引入模块下的所有项","basic/crate-module/use.html#受限的可见性","basic/crate-module/use.html#课后练习","basic/comment.html#注释和文档","basic/comment.html#注释的种类","basic/comment.html#代码注释","basic/comment.html#文档注释","basic/comment.html#包和模块级别的注释","basic/comment.html#文档测试doc-test","basic/comment.html#文档注释中的代码跳转","basic/comment.html#文档搜索别名","basic/comment.html#一个综合例子","basic/comment.html#总结","basic/comment.html#课后练习","basic/formatted-output.html#格式化输出","basic/formatted-output.html#满分初印象","basic/formatted-output.html#printprintlnformat","basic/formatted-output.html#-与-","basic/formatted-output.html#位置参数","basic/formatted-output.html#具名参数","basic/formatted-output.html#格式化参数","basic/formatted-output.html#宽度","basic/formatted-output.html#对齐","basic/formatted-output.html#精度","basic/formatted-output.html#进制","basic/formatted-output.html#指数","basic/formatted-output.html#指针地址","basic/formatted-output.html#转义","basic/formatted-output.html#在格式化字符串时捕获环境中的值rust-158-新增","basic/formatted-output.html#课后练习","basic/formatted-output.html#总结","basic-practice/intro.html#构建一个简单命令行程序","basic-practice/base-features.html#实现基本功能","basic-practice/base-features.html#接收命令行参数","basic-practice/base-features.html#不可信的输入","basic-practice/base-features.html#存储读取到的参数","basic-practice/base-features.html#文件读取","basic-practice/refactoring.html#增加模块化和错误处理","basic-practice/refactoring.html#分离-main-函数","basic-practice/refactoring.html#分离命令行解析","basic-practice/refactoring.html#聚合配置变量","basic-practice/refactoring.html#错误处理","basic-practice/refactoring.html#改进报错信息","basic-practice/refactoring.html#返回-result-来替代直接-panic","basic-practice/refactoring.html#处理返回的-result","basic-practice/refactoring.html#分离主体逻辑","basic-practice/refactoring.html#使用--和特征对象来返回错误","basic-practice/refactoring.html#处理返回的错误","basic-practice/refactoring.html#分离逻辑代码到库包中","basic-practice/tests.html#测试驱动开发","basic-practice/tests.html#注定失败的测试用例","basic-practice/tests.html#务必成功的测试用例","basic-practice/tests.html#遍历迭代每一行","basic-practice/tests.html#在每一行中查询目标字符串","basic-practice/tests.html#存储匹配到的结果","basic-practice/tests.html#在-run-函数中调用-search-函数","basic-practice/envs.html#使用环境变量来增强程序","basic-practice/envs.html#编写大小写不敏感的测试用例","basic-practice/stderr.html#重定向错误信息的输出","basic-practice/stderr.html#目前的错误输出位置","basic-practice/stderr.html#标准错误输出-stderr","basic-practice/iterators.html#使用迭代器来改进我们的程序","basic-practice/iterators.html#移除-clone-的使用","basic-practice/iterators.html#直接使用返回的迭代器","basic-practice/iterators.html#移除数组索引的使用","basic-practice/iterators.html#使用迭代器适配器让代码更简洁","basic-practice/iterators.html#总结","advance/intro.html#rust-高级进阶","advance/lifetime/intro.html#生命周期","advance/lifetime/advance.html#深入生命周期","advance/lifetime/advance.html#不太聪明的生命周期检查","advance/lifetime/advance.html#无界生命周期","advance/lifetime/advance.html#生命周期约束-hrtb","advance/lifetime/advance.html#闭包函数的消除规则","advance/lifetime/advance.html#nll-non-lexical-lifetime","advance/lifetime/advance.html#reborrow-再借用","advance/lifetime/advance.html#生命周期消除规则补充","advance/lifetime/advance.html#一个复杂的例子","advance/lifetime/static.html#static-和-t-static","advance/lifetime/static.html#static","advance/lifetime/static.html#t-static","advance/lifetime/static.html#static-到底针对谁","advance/lifetime/static.html#课后练习","advance/lifetime/static.html#总结","advance/functional-programing/intro.html#函数式编程","advance/functional-programing/closure.html#闭包-closure","advance/functional-programing/closure.html#使用闭包来简化代码","advance/functional-programing/closure.html#传统函数实现","advance/functional-programing/closure.html#闭包的类型推导","advance/functional-programing/closure.html#结构体中的闭包","advance/functional-programing/closure.html#捕获作用域中的值","advance/functional-programing/closure.html#闭包对内存的影响","advance/functional-programing/closure.html#三种-fn-特征","advance/functional-programing/closure.html#闭包作为函数返回值","advance/functional-programing/closure.html#闭包的生命周期","advance/functional-programing/closure.html#课后习题","advance/functional-programing/iterator.html#迭代器-iterator","advance/functional-programing/iterator.html#for-循环与迭代器","advance/functional-programing/iterator.html#惰性初始化","advance/functional-programing/iterator.html#next-方法","advance/functional-programing/iterator.html#intoiterator-特征","advance/functional-programing/iterator.html#消费者与适配器","advance/functional-programing/iterator.html#实现-iterator-特征","advance/functional-programing/iterator.html#迭代器的性能","advance/functional-programing/iterator.html#学习其它方法","advance/into-types/intro.html#深入类型","advance/into-types/converse.html#类型转换","advance/into-types/converse.html#as转换","advance/into-types/converse.html#tryinto-转换","advance/into-types/converse.html#通用类型转换","advance/into-types/converse.html#课后练习","advance/into-types/custom-type.html#深入-rust-类型","advance/into-types/custom-type.html#newtype","advance/into-types/custom-type.html#类型别名type-alias","advance/into-types/custom-type.html#永不返回类型","advance/into-types/sized.html#sized-和不定长类型-dst","advance/into-types/sized.html#动态大小类型-dst","advance/into-types/sized.html#sized-特征","advance/into-types/sized.html#box","advance/into-types/enum-int.html#整数转换为枚举","advance/into-types/enum-int.html#一个真实场景的需求","advance/into-types/enum-int.html#c-语言的实现","advance/into-types/enum-int.html#使用三方库","advance/into-types/enum-int.html#tryfrom--宏","advance/into-types/enum-int.html#邪恶之王-stdmemtransmute","advance/into-types/enum-int.html#总结","advance/smart-pointer/intro.html#智能指针","advance/smart-pointer/box.html#box-堆对象分配","advance/smart-pointer/box.html#rust-中的堆栈","advance/smart-pointer/box.html#box-的使用场景","advance/smart-pointer/box.html#box-内存布局","advance/smart-pointer/box.html#boxleak","advance/smart-pointer/box.html#总结","advance/smart-pointer/deref.html#deref-解引用","advance/smart-pointer/deref.html#通过--获取引用背后的值","advance/smart-pointer/deref.html#智能指针解引用","advance/smart-pointer/deref.html#-背后的原理","advance/smart-pointer/deref.html#函数和方法中的隐式-deref-转换","advance/smart-pointer/deref.html#deref-规则总结","advance/smart-pointer/deref.html#三种-deref-转换","advance/smart-pointer/deref.html#总结","advance/smart-pointer/drop.html#drop-释放资源","advance/smart-pointer/drop.html#学习目标","advance/smart-pointer/drop.html#rust-中的资源回收","advance/smart-pointer/drop.html#一个不那么简单的-drop-例子","advance/smart-pointer/drop.html#手动回收","advance/smart-pointer/drop.html#drop-使用场景","advance/smart-pointer/drop.html#互斥的-copy-和-drop","advance/smart-pointer/drop.html#总结","advance/smart-pointer/rc-arc.html#rc-与-arc","advance/smart-pointer/rc-arc.html#rc","advance/smart-pointer/rc-arc.html#多线程无力的-rc","advance/smart-pointer/rc-arc.html#arc","advance/smart-pointer/rc-arc.html#总结","advance/smart-pointer/cell-refcell.html#cell-和-refcell","advance/smart-pointer/cell-refcell.html#cell","advance/smart-pointer/cell-refcell.html#refcell","advance/smart-pointer/cell-refcell.html#选择-cell-还是-refcell","advance/smart-pointer/cell-refcell.html#内部可变性","advance/smart-pointer/cell-refcell.html#rc--refcell-组合使用","advance/smart-pointer/cell-refcell.html#通过-cellfrom_mut-解决借用冲突","advance/smart-pointer/cell-refcell.html#总结","advance/circle-self-ref/intro.html#循环引用与自引用","advance/circle-self-ref/circle-reference.html#weak-与循环引用","advance/circle-self-ref/circle-reference.html#何为循环引用","advance/circle-self-ref/circle-reference.html#weak","advance/circle-self-ref/circle-reference.html#使用-weak-解决循环引用","advance/circle-self-ref/circle-reference.html#unsafe-解决循环引用","advance/circle-self-ref/circle-reference.html#总结","advance/circle-self-ref/self-referential.html#结构体自引用","advance/circle-self-ref/self-referential.html#平平无奇的自引用","advance/circle-self-ref/self-referential.html#使用-option","advance/circle-self-ref/self-referential.html#unsafe-实现","advance/circle-self-ref/self-referential.html#无法被移动的-pin","advance/circle-self-ref/self-referential.html#使用-ouroboros","advance/circle-self-ref/self-referential.html#rc--refcell-或-arc--mutex","advance/circle-self-ref/self-referential.html#终极大法","advance/circle-self-ref/self-referential.html#学习一本书如何实现链表","advance/circle-self-ref/self-referential.html#总结","advance/concurrency-with-threads/intro.html#多线程并发编程","advance/concurrency-with-threads/concurrency-parallelism.html#并发和并行","advance/concurrency-with-threads/concurrency-parallelism.html#cpu-多核","advance/concurrency-with-threads/concurrency-parallelism.html#正式的定义","advance/concurrency-with-threads/concurrency-parallelism.html#编程语言的并发模型","advance/concurrency-with-threads/thread.html#使用线程","advance/concurrency-with-threads/thread.html#多线程编程的风险","advance/concurrency-with-threads/thread.html#创建线程","advance/concurrency-with-threads/thread.html#等待子线程的结束","advance/concurrency-with-threads/thread.html#在线程闭包中使用-move","advance/concurrency-with-threads/thread.html#线程是如何结束的","advance/concurrency-with-threads/thread.html#多线程的性能","advance/concurrency-with-threads/thread.html#线程屏障barrier","advance/concurrency-with-threads/thread.html#线程局部变量thread-local-variable","advance/concurrency-with-threads/thread.html#用条件控制线程的挂起和执行","advance/concurrency-with-threads/thread.html#只被调用一次的函数","advance/concurrency-with-threads/thread.html#总结","advance/concurrency-with-threads/message-passing.html#线程间的消息传递","advance/concurrency-with-threads/message-passing.html#消息通道","advance/concurrency-with-threads/message-passing.html#多发送者单接收者","advance/concurrency-with-threads/message-passing.html#不阻塞的-try_recv-方法","advance/concurrency-with-threads/message-passing.html#传输具有所有权的数据","advance/concurrency-with-threads/message-passing.html#使用-for-进行循环接收","advance/concurrency-with-threads/message-passing.html#使用多发送者","advance/concurrency-with-threads/message-passing.html#消息顺序","advance/concurrency-with-threads/message-passing.html#同步和异步通道","advance/concurrency-with-threads/message-passing.html#关闭通道","advance/concurrency-with-threads/message-passing.html#传输多种类型的数据","advance/concurrency-with-threads/message-passing.html#新手容易遇到的坑","advance/concurrency-with-threads/message-passing.html#mpmc-更好的性能","advance/concurrency-with-threads/sync1.html#线程同步锁condvar-和信号量","advance/concurrency-with-threads/sync1.html#该如何选择","advance/concurrency-with-threads/sync1.html#互斥锁-mutex","advance/concurrency-with-threads/sync1.html#死锁","advance/concurrency-with-threads/sync1.html#读写锁-rwlock","advance/concurrency-with-threads/sync1.html#mutex-还是-rwlock","advance/concurrency-with-threads/sync1.html#三方库提供的锁实现","advance/concurrency-with-threads/sync1.html#用条件变量condvar控制线程的同步","advance/concurrency-with-threads/sync1.html#信号量-semaphore","advance/concurrency-with-threads/sync1.html#总结","advance/concurrency-with-threads/sync2.html#线程同步atomic-原子类型与内存顺序","advance/concurrency-with-threads/sync2.html#使用-atomic-作为全局变量","advance/concurrency-with-threads/sync2.html#内存顺序","advance/concurrency-with-threads/sync2.html#多线程中使用-atomic","advance/concurrency-with-threads/sync2.html#atomic-能替代锁吗","advance/concurrency-with-threads/sync2.html#atomic-的应用场景","advance/concurrency-with-threads/send-sync.html#基于-send-和-sync-的线程安全","advance/concurrency-with-threads/send-sync.html#无法用于多线程的rc","advance/concurrency-with-threads/send-sync.html#rc-和-arc-源码对比","advance/concurrency-with-threads/send-sync.html#send-和-sync","advance/concurrency-with-threads/send-sync.html#实现send和sync的类型","advance/concurrency-with-threads/send-sync.html#为裸指针实现send","advance/concurrency-with-threads/send-sync.html#为裸指针实现sync","advance/concurrency-with-threads/send-sync.html#总结","advance/global-variable.html#全局变量","advance/global-variable.html#编译期初始化","advance/global-variable.html#运行期初始化","advance/global-variable.html#标准库中的-oncecell","advance/global-variable.html#总结","advance/errors.html#错误处理","advance/errors.html#组合器","advance/errors.html#自定义错误类型","advance/errors.html#归一化不同的错误类型","advance/errors.html#简化错误处理","advance/errors.html#总结","advance/unsafe/intro.html#unsafe-简介","advance/unsafe/intro.html#为何会有-unsafe","advance/unsafe/intro.html#unsafe-的超能力","advance/unsafe/intro.html#unsafe-的安全保证","advance/unsafe/intro.html#谈虎色变","advance/unsafe/intro.html#控制-unsafe-的使用边界","advance/unsafe/superpowers.html#五种兵器","advance/unsafe/superpowers.html#解引用裸指针","advance/unsafe/superpowers.html#调用-unsafe-函数或方法","advance/unsafe/superpowers.html#用安全抽象包裹-unsafe-代码","advance/unsafe/superpowers.html#ffi","advance/unsafe/superpowers.html#访问或修改一个可变的静态变量","advance/unsafe/superpowers.html#实现-unsafe-特征","advance/unsafe/superpowers.html#访问-union-中的字段","advance/unsafe/superpowers.html#一些实用工具库","advance/unsafe/superpowers.html#总结","advance/unsafe/superpowers.html#进一步学习","advance/unsafe/inline-asm.html#内联汇编","advance/unsafe/inline-asm.html#基本用法","advance/unsafe/inline-asm.html#输入和输出","advance/unsafe/inline-asm.html#延迟输出操作数","advance/unsafe/inline-asm.html#显式指定寄存器","advance/unsafe/inline-asm.html#clobbered-寄存器","advance/unsafe/inline-asm.html#总结","advance/macro.html#macro-宏编程","advance/macro.html#宏和函数的区别","advance/macro.html#声明式宏-macro_rules","advance/macro.html#用过程宏为属性标记生成代码","advance/macro.html#自定义-derive-过程宏","advance/macro.html#类属性宏attribute-like-macros","advance/macro.html#类函数宏function-like-macros","advance/macro.html#补充学习资料","advance/macro.html#总结","advance/async/intro.html#异步编程","advance/async/getting-started.html#async-编程简介","advance/async/getting-started.html#async-简介","advance/async/getting-started.html#async-rust-当前的进展","advance/async/getting-started.html#asyncawait-简单入门","advance/async/future-excuting.html#底层探秘-future-执行器与任务调度","advance/async/future-excuting.html#future-特征","advance/async/future-excuting.html#使用-waker-来唤醒任务","advance/async/future-excuting.html#执行器-executor","advance/async/future-excuting.html#执行器和系统-io","advance/async/pin-unpin.html#定海神针-pin-和-unpin","advance/async/pin-unpin.html#为何需要-pin","advance/async/pin-unpin.html#unpin","advance/async/pin-unpin.html#深入理解-pin","advance/async/pin-unpin.html#pin-在实践中的运用","advance/async/pin-unpin.html#总结","advance/async/async-await.html#asyncawait-和-stream-流处理","advance/async/async-await.html#async-的生命周期","advance/async/async-await.html#async-move","advance/async/async-await.html#当await-遇见多线程执行器","advance/async/async-await.html#stream-流处理","advance/async/multi-futures-simultaneous.html#使用-join-和-select-同时运行多个-future","advance/async/multi-futures-simultaneous.html#join","advance/async/multi-futures-simultaneous.html#try_join","advance/async/multi-futures-simultaneous.html#select","advance/async/multi-futures-simultaneous.html#在-select-循环中并发","advance/async/pain-points-and-workarounds.html#一些疑难问题的解决办法","advance/async/pain-points-and-workarounds.html#在-async-语句块中使用-","advance/async/pain-points-and-workarounds.html#async-函数和-send-特征","advance/async/pain-points-and-workarounds.html#递归使用-async-fn","advance/async/pain-points-and-workarounds.html#在特征中使用-async","advance/async/web-server.html#一个实践项目-web-服务器","advance/async/web-server.html#多线程版本的-web-服务器","advance/async/web-server.html#运行异步代码","advance/async/web-server.html#并发地处理连接","advance/async/web-server.html#使用多线程并行处理请求","advance/async/web-server.html#测试-handle_connection-函数","advance-practice1/intro.html#实践应用多线程web服务器","advance-practice1/web-server.html#构建单线程-web-服务器","advance-practice1/web-server.html#监听-tcp-连接","advance-practice1/web-server.html#读取请求","advance-practice1/web-server.html#http-请求长啥样","advance-practice1/web-server.html#请求应答","advance-practice1/web-server.html#返回-html-页面","advance-practice1/web-server.html#验证请求和选择性应答","advance-practice1/multi-threads.html#构建多线程-web-服务器","advance-practice1/multi-threads.html#基于单线程模拟慢请求","advance-practice1/multi-threads.html#使用线程池改善吞吐","advance-practice1/multi-threads.html#为每个请求生成一个线程","advance-practice1/multi-threads.html#限制创建线程的数量","advance-practice1/multi-threads.html#使用编译器驱动的方式开发-threadpool","advance-practice1/multi-threads.html#new-还是-build","advance-practice1/multi-threads.html#存储线程","advance-practice1/multi-threads.html#将代码从-threadpool-发送到线程中","advance-practice1/multi-threads.html#将请求发送给线程","advance-practice1/multi-threads.html#实现-execute-方法","advance-practice1/multi-threads.html#while-let-的巨大陷阱","advance-practice1/graceful-shutdown.html#优雅关闭和资源清理","advance-practice1/graceful-shutdown.html#为线程池实现-drop","advance-practice1/graceful-shutdown.html#停止工作线程","advance-practice1/graceful-shutdown.html#完整代码","advance-practice1/graceful-shutdown.html#可以做的更多","advance-practice1/graceful-shutdown.html#上一章节的遗留问题","advance-practice/intro.html#进阶实战-实现一个简单-redis","advance-practice/overview.html#tokio-概览","advance-practice/overview.html#异步运行时","advance-practice/overview.html#tokio-简介","advance-practice/overview.html#优势","advance-practice/overview.html#劣势","advance-practice/overview.html#总结","advance-practice/getting-startted.html#tokio-初印象","advance-practice/getting-startted.html#专题目标","advance-practice/getting-startted.html#环境配置","advance-practice/getting-startted.html#hello-tokio","advance-practice/getting-startted.html#原理解释","advance-practice/getting-startted.html#cargo-feature","advance-practice/getting-startted.html#总结","advance-practice/spawning.html#创建异步任务","advance-practice/spawning.html#接收-sockets","advance-practice/spawning.html#生成任务","advance-practice/spawning.html#使用-hashmap-存储数据","advance-practice/shared-state.html#共享状态","advance-practice/shared-state.html#解决方法","advance-practice/shared-state.html#添加-bytes-依赖包","advance-practice/shared-state.html#初始化-hashmap","advance-practice/shared-state.html#更新-process","advance-practice/shared-state.html#任务线程和锁竞争","advance-practice/shared-state.html#在-await-期间持有锁","advance-practice/channels.html#消息传递","advance-practice/channels.html#错误的实现","advance-practice/channels.html#消息传递","advance-practice/channels.html#tokio-的消息通道-channel-","advance-practice/channels.html#定义消息类型","advance-practice/channels.html#创建消息通道","advance-practice/channels.html#生成管理任务","advance-practice/channels.html#接收响应消息","advance-practice/channels.html#对消息通道进行限制","advance-practice/io.html#io","advance-practice/io.html#asyncread-和-asyncwrite","advance-practice/io.html#实用函数","advance-practice/io.html#回声服务-echo-","advance-practice/frame.html#解析数据帧","advance-practice/frame.html#缓冲读取buffered-reads","advance-practice/frame.html#帧解析","advance-practice/frame.html#缓冲写入buffered-writes","advance-practice/async.html#深入-tokio-背后的异步原理","advance-practice/async.html#future","advance-practice/async.html#执行器-excecutor-","advance-practice/async.html#waker","advance-practice/async.html#一些遗留问题","advance-practice/async.html#总结","advance-practice/select.html#select","advance-practice/select.html#tokioselect","advance-practice/select.html#语法","advance-practice/select.html#返回值","advance-practice/select.html#错误传播","advance-practice/select.html#模式匹配","advance-practice/select.html#借用","advance-practice/select.html#循环","advance-practice/select.html#spawn-和-select-的一些不同","advance-practice/stream.html#stream","advance-practice/stream.html#迭代","advance-practice/stream.html#适配器","advance-practice/stream.html#实现-stream-特征","advance-practice/graceful-shutdown.html#优雅的关闭","advance-practice/graceful-shutdown.html#找出合适的关闭时机","advance-practice/graceful-shutdown.html#通知程序的每一个部分开始关闭","advance-practice/graceful-shutdown.html#等待各个部分的结束","advance-practice/bridging-with-sync.html#异步跟同步共存","advance-practice/bridging-with-sync.html#tokiomain-的展开","advance-practice/bridging-with-sync.html#mini-redis-的同步接口","advance-practice/bridging-with-sync.html#其它方法","difficulties/intro.html#rust-难点攻关","difficulties/slice.html#切片和切片引用","difficulties/slice.html#无法被直接使用的切片类型","difficulties/slice.html#切片引用","difficulties/slice.html#总结","difficulties/eq.html#eq-和-partialeq","difficulties/eq.html#部分相等性","difficulties/eq.html#ord-和-partialord","difficulties/string.html#疯狂字符串","difficulties/string.html#str","difficulties/lifetime.html#作用域生命周期和-nll-todo","difficulties/move-copy.html#movecopy和clone-todo","advance/difficulties/pointer.html#裸指针引用和智能指针-todo","test/intro.html#测试","test/write-tests.html#编写测试及控制执行","test/write-tests.html#测试函数","test/write-tests.html#cargo-test","test/write-tests.html#自定义失败信息","test/write-tests.html#测试-panic","test/write-tests.html#使用-result","test/write-tests.html#使用----分割命令行参数","test/write-tests.html#测试用例的并行或顺序执行","test/write-tests.html#测试函数中的-println","test/write-tests.html#指定运行一部分测试","test/write-tests.html#dev-dependencies","test/write-tests.html#生成测试二进制文件","test/unit-integration-test.html#单元测试集成测试","test/unit-integration-test.html#单元测试","test/unit-integration-test.html#集成测试","test/unit-integration-test.html#总结","test/assertion.html#断言-assertion","test/assertion.html#断言列表","test/assertion.html#assert_eq","test/assertion.html#assert_ne","test/assertion.html#assert","test/assertion.html#debug_assert-系列","test/ci.html#用-github-actions-进行持续集成","test/ci.html#github-actions","test/ci.html#actions-基础","test/ci.html#真实示例生成-github-统计卡片","test/ci.html#使用-actions-来构建-rust-项目","test/ci.html#构建","test/benchmark.html#基准测试-benchmark","test/benchmark.html#官方-benchmark","test/benchmark.html#criterionrs","cargo/intro.html#cargo-使用指南","cargo/getting-started.html#上手使用","cargo/guide/intro.html#使用手册","cargo/guide/why-exist.html#为何会有-cargo","cargo/guide/why-exist.html#cargo","cargo/guide/download-package.html#下载并构建-package","cargo/guide/dependencies.html#添加依赖","cargo/guide/package-layout.html#标准的-package-目录结构","cargo/guide/cargo-toml-lock.html#cargotoml-vs-cargolock","cargo/guide/cargo-toml-lock.html#是否上传本地的-cargolock","cargo/guide/cargo-toml-lock.html#假设没有-cargolock","cargo/guide/cargo-toml-lock.html#当有了-cargolock-后","cargo/guide/cargo-toml-lock.html#更新依赖","cargo/guide/tests-ci.html#测试和-ci","cargo/guide/tests-ci.html#ci","cargo/guide/cargo-cache.html#cargo-缓存","cargo/guide/cargo-cache.html#cargo-home","cargo/guide/cargo-cache.html#文件","cargo/guide/cargo-cache.html#目录","cargo/guide/cargo-cache.html#在-ci-时缓存-cargo-home","cargo/guide/cargo-cache.html#清除缓存","cargo/guide/cargo-cache.html#构建时卡住blocking-waiting-for-file-lock-","cargo/guide/build-cache.html#构建-build-缓存","cargo/guide/build-cache.html#target-目录结构","cargo/guide/build-cache.html#依赖信息文件","cargo/guide/build-cache.html#共享缓存","cargo/reference/intro.html#进阶指南","cargo/reference/specify-deps.html#指定依赖项","cargo/reference/specify-deps.html#从-cratesio-引入依赖包","cargo/reference/specify-deps.html#从其它注册服务引入依赖包","cargo/reference/specify-deps.html#多引用方式混合","cargo/reference/specify-deps.html#根据平台引入依赖","cargo/reference/specify-deps.html#自定义-target-引入","cargo/reference/specify-deps.html#dev-dependencies","cargo/reference/specify-deps.html#build-dependencies","cargo/reference/specify-deps.html#选择-features","cargo/reference/specify-deps.html#在-cargotoml-中重命名依赖","cargo/reference/deps-overriding.html#依赖覆盖","cargo/reference/deps-overriding.html#测试-bugfix-版本","cargo/reference/deps-overriding.html#使用未发布的小版本","cargo/reference/deps-overriding.html#使用未发布的大版本","cargo/reference/deps-overriding.html#多版本patch","cargo/reference/deps-overriding.html#通过path来覆盖依赖","cargo/reference/deps-overriding.html#不推荐的replace","cargo/reference/manifest.html#cargotoml-格式讲解","cargo/reference/manifest.html#package","cargo/reference/manifest.html#description","cargo/reference/manifest.html#documentation","cargo/reference/manifest.html#badges","cargo/reference/manifest.html#dependencies","cargo/reference/manifest.html#profile","cargo/reference/cargo-target.html#cargo-target","cargo/reference/cargo-target.html#对象介绍","cargo/reference/cargo-target.html#配置一个对象","cargo/reference/cargo-target.html#对象自动发现","cargo/reference/workspaces.html#工作空间-workspace","cargo/reference/workspaces.html#工作空间的两种类型","cargo/reference/workspaces.html#关键特性","cargo/reference/workspaces.html#workspace","cargo/reference/workspaces.html#选择工作空间","cargo/reference/workspaces.html#选择-package","cargo/reference/workspaces.html#workspacemetadata","cargo/reference/features/intro.html#条件编译-features","cargo/reference/features/intro.html#features","cargo/reference/features/intro.html#default-feature","cargo/reference/features/intro.html#可选依赖","cargo/reference/features/intro.html#依赖库自身的-feature","cargo/reference/features/intro.html#通过命令行参数启用-feature","cargo/reference/features/intro.html#feature-同一化","cargo/reference/features/intro.html#feature-解析器-v2-版本","cargo/reference/features/intro.html#构建脚本","cargo/reference/features/intro.html#required-features","cargo/reference/features/intro.html#semver-兼容性","cargo/reference/features/intro.html#feature-文档和发现","cargo/reference/features/examples.html#features-示例","cargo/reference/features/examples.html#最小化构建时间和文件大小","cargo/reference/features/examples.html#行为扩展","cargo/reference/features/examples.html#no_std-支持","cargo/reference/features/examples.html#对依赖库的-features-进行再导出","cargo/reference/features/examples.html#feature-优先级","cargo/reference/features/examples.html#过程宏包","cargo/reference/features/examples.html#只能用于-nightly-的-feature","cargo/reference/features/examples.html#实验性-feature","cargo/reference/profiles.html#发布配置-profile","cargo/reference/profiles.html#默认的-profile","cargo/reference/profiles.html#自定义-profile","cargo/reference/profiles.html#选择-profile","cargo/reference/profiles.html#profile-设置","cargo/reference/profiles.html#默认-profile","cargo/reference/profiles.html#重写-profile","cargo/reference/configuration.html#通过-configtoml-对-cargo-进行配置","cargo/reference/configuration.html#层级结构","cargo/reference/configuration.html#配置文件概览","cargo/reference/configuration.html#环境变量","cargo/reference/publishing-on-crates.io.html#发布到-cratesio","cargo/reference/publishing-on-crates.io.html#首次发布之前","cargo/reference/publishing-on-crates.io.html#发布包之前","cargo/reference/publishing-on-crates.io.html#打包","cargo/reference/publishing-on-crates.io.html#上传包","cargo/reference/publishing-on-crates.io.html#发布已上传包的新版本","cargo/reference/publishing-on-crates.io.html#管理-cratesio-上的包","cargo/reference/build-script/intro.html#构建脚本-build-scripts","cargo/reference/build-script/intro.html#buildrs","cargo/reference/build-script/intro.html#构建脚本的输出","cargo/reference/build-script/intro.html#构建脚本的依赖","cargo/reference/build-script/intro.html#links","cargo/reference/build-script/intro.html#覆盖构建脚本","cargo/reference/build-script/examples.html#构建脚本示例","cargo/reference/build-script/examples.html#代码生成","cargo/reference/build-script/examples.html#构建本地库","cargo/reference/build-script/examples.html#链接系统库","cargo/reference/build-script/examples.html#使用其它-sys-包","cargo/reference/build-script/examples.html#条件编译","usecases/intro.html#rust的使用案例","usecases/aws-rust.html#使用-rust-来节约企业成本","usecases/aws-rust.html#云计算的能源效率","usecases/aws-rust.html#编程语言的能源效率","usecases/aws-rust.html#rust-成功案例","logs/intro.html#日志和监控","logs/about-log.html#详解日志","logs/about-log.html#日志级别和输出位置","logs/about-log.html#日志查看","logs/about-log.html#日志采集","logs/about-log.html#中心化日志存储","logs/log.html#日志门面-log","logs/log.html#log-特征","logs/log.html#日志宏","logs/log.html#日志输出在哪里","logs/log.html#使用具体的日志库","logs/log.html#rust-库的开发者","logs/log.html#应用开发者","logs/log.html#日志库开发者","logs/log.html#更多示例","logs/tracing.html#使用-tracing-记录日志","logs/tracing.html#一个简单例子","logs/tracing.html#异步编程中的挑战","logs/tracing.html#核心概念","logs/tracing.html#span","logs/tracing.html#event-事件","logs/tracing.html#collector-收集器","logs/tracing.html#使用方法","logs/tracing.html#span-宏","logs/tracing.html#instrument","logs/tracing.html#in_scope","logs/tracing.html#在-async-中使用-span","logs/tracing.html#span-嵌套","logs/tracing.html#对宏进行配置","logs/tracing.html#日志级别和目标","logs/tracing.html#记录字段","logs/tracing.html#文件输出","logs/tracing.html#一个综合例子","logs/tracing.html#总结--推荐","logs/tracing-logger.html#使用-tracing-输出自定义的-rust-日志","logs/tracing-logger.html#打地基1","logs/tracing-logger.html#捕获事件","logs/tracing-logger.html#访问者模式","logs/tracing-logger.html#构建-json-logger","logs/tracing-logger.html#何为-span","logs/tracing-logger.html#打地基2","logs/tracing-logger.html#span-的数据在哪里","logs/tracing-logger.html#自己存储-span-数据","logs/tracing-logger.html#功能齐全的-json-logger","logs/tracing-logger.html#等等你说功能齐全","logs/observe/intro.html#监控","logs/observe/about-observe.html#可观测性","logs/observe/about-observe.html#各自为战的三种模型","logs/observe/about-observe.html#模型纽带","logs/observe/about-observe.html#数据采集","logs/observe/about-observe.html#数据处理和存储","logs/observe/about-observe.html#数据查询和展示","logs/observe/trace.html#分布式追踪","practice/intro.html#rust最佳实践","practice/third-party-libs.html#日常开发三方库精选","practice/third-party-libs.html#目录","practice/third-party-libs.html#日常开发常用rust库","practice/third-party-libs.html#webhttp","practice/third-party-libs.html#日志监控","practice/third-party-libs.html#sql客户端","practice/third-party-libs.html#nosql客户端","practice/third-party-libs.html#分布式","practice/third-party-libs.html#网络通信协议","practice/third-party-libs.html#异步网络编程","practice/third-party-libs.html#搜索引擎","practice/third-party-libs.html#代码debug","practice/third-party-libs.html#性能优化","practice/third-party-libs.html#编解码","practice/third-party-libs.html#email","practice/third-party-libs.html#常用正则模版","practice/naming.html#命名规范","practice/naming.html#特征命名","practice/naming.html#类型转换要遵守-as_to_into_-命名惯例c-conv","practice/naming.html#标准库中的一些例子","practice/naming.html#读访问器getter的名称遵循-rust-的命名规范c-getter","practice/naming.html#标准库示例","practice/naming.html#一个集合上的方法如果返回迭代器需遵循命名规则iteriter_mutinto_iter-c-iter","practice/naming.html#标准库示例","practice/naming.html#迭代器的类型应该与产生它的方法名相匹配c-iter-ty","practice/naming.html#标准库示例","practice/naming.html#cargo-feature-的名称不应该包含占位词c-feature","practice/naming.html#命名要使用一致性的词序c-word-order","practice/interview.html#面试经验-doing","practice/best-pratice.html#最佳实践","practice/best-pratice.html#最佳开发流程workflow","practice/best-pratice.html#测试文件组织结构","practice/best-pratice.html#git备份","practice/best-pratice.html#code-cover","practice/best-pratice.html#clippy","practice/best-pratice.html#todo","practice/best-pratice.html#如何获知变量类型或者函数的返回类型","practice/best-pratice.html#代码风格todo","too-many-lists/intro.html#手把手带你实现链表","too-many-lists/do-we-need-it.html#我们到底需不需要链表","too-many-lists/bad-stack/intro.html#糟糕的单向链表栈","too-many-lists/bad-stack/layout.html#基本数据布局-layout-","too-many-lists/bad-stack/basic-operations.html#定义基本操作","too-many-lists/bad-stack/basic-operations.html#new","too-many-lists/bad-stack/basic-operations.html#push","too-many-lists/bad-stack/basic-operations.html#pop","too-many-lists/bad-stack/final-code.html#一些收尾工作以及最终代码","too-many-lists/bad-stack/final-code.html#单元测试","too-many-lists/bad-stack/final-code.html#drop","too-many-lists/bad-stack/final-code.html#最终代码","too-many-lists/ok-stack/intro.html#还可以的单向链表","too-many-lists/ok-stack/type-optimizing.html#优化类型定义","too-many-lists/ok-stack/type-optimizing.html#option","too-many-lists/ok-stack/type-optimizing.html#泛型","too-many-lists/ok-stack/peek.html#peek-函数","too-many-lists/ok-stack/iter.html#迭代器","too-many-lists/ok-stack/iter.html#intoiter","too-many-lists/ok-stack/iter.html#iter","too-many-lists/ok-stack/itermut.html#itermut以及完整代码","too-many-lists/ok-stack/itermut.html#完整代码","too-many-lists/persistent-stack/intro.html#持久化单向链表","too-many-lists/persistent-stack/layout.html#数据布局和基本操作","too-many-lists/persistent-stack/layout.html#数据布局","too-many-lists/persistent-stack/layout.html#基本操作","too-many-lists/persistent-stack/drop-arc.html#droparc-及完整代码","too-many-lists/persistent-stack/drop-arc.html#drop","too-many-lists/persistent-stack/drop-arc.html#arc","too-many-lists/persistent-stack/drop-arc.html#完整代码","too-many-lists/deque/intro.html#不太优秀的双端队列","too-many-lists/deque/layout.html#数据布局和构建","too-many-lists/deque/layout.html#数据布局","too-many-lists/deque/layout.html#构建","too-many-lists/deque/layout.html#push","too-many-lists/deque/layout.html#pop","too-many-lists/deque/layout.html#drop","too-many-lists/deque/peek.html#peek","too-many-lists/deque/symmetric.html#基本操作的对称镜像","too-many-lists/deque/iterator.html#迭代器","too-many-lists/deque/iterator.html#intoiter","too-many-lists/deque/iterator.html#iter","too-many-lists/deque/final-code.html#最终代码","too-many-lists/unsafe-queue/intro.html#不错的unsafe队列","too-many-lists/unsafe-queue/layout.html#数据布局","too-many-lists/unsafe-queue/basics.html#基本操作","too-many-lists/unsafe-queue/miri.html#miri","too-many-lists/unsafe-queue/stacked-borrow.html#栈借用-stacked-borrorw","too-many-lists/unsafe-queue/stacked-borrow.html#指针混叠-pointer-aliasing-","too-many-lists/unsafe-queue/stacked-borrow.html#安全地栈借用","too-many-lists/unsafe-queue/stacked-borrow.html#不安全地栈借用","too-many-lists/unsafe-queue/stacked-borrow.html#管理栈借用","too-many-lists/unsafe-queue/testing-stacked-borrow.html#测试栈借用","too-many-lists/unsafe-queue/testing-stacked-borrow.html#基本借用","too-many-lists/unsafe-queue/testing-stacked-borrow.html#测试数组","too-many-lists/unsafe-queue/testing-stacked-borrow.html#测试不可变引用","too-many-lists/unsafe-queue/testing-stacked-borrow.html#测试内部可变性","too-many-lists/unsafe-queue/testing-stacked-borrow.html#测试-box","too-many-lists/unsafe-queue/layout2.html#数据布局2-再裸一些吧","too-many-lists/unsafe-queue/layout2.html#布局","too-many-lists/unsafe-queue/layout2.html#基本操作","too-many-lists/unsafe-queue/layout2.html#示例","too-many-lists/unsafe-queue/extra-junk.html#额外的操作","too-many-lists/unsafe-queue/final-code.html#最终代码","too-many-lists/advanced-lists/intro.html#使用高级技巧实现链表","too-many-lists/advanced-lists/unsafe-deque.html#生产级可用的双向链表","too-many-lists/advanced-lists/double-singly.html#双单向链表","too-many-lists/advanced-lists/stack-allocated.html#栈上的链表","compiler/intro.html#征服编译错误","compiler/fight-with-compiler/intro.html#对抗编译检查","compiler/fight-with-compiler/lifetime/intro.html#生命周期","compiler/fight-with-compiler/lifetime/too-long1.html#生命周期声明的范围过大","compiler/fight-with-compiler/lifetime/too-long1.html#例子-1","compiler/fight-with-compiler/lifetime/too-long2.html#生命周期过大-02","compiler/fight-with-compiler/lifetime/loop.html#蠢笨编译器之循环生命周期","compiler/fight-with-compiler/lifetime/loop.html#循环中的生命周期错误","compiler/fight-with-compiler/lifetime/loop.html#尝试去掉中间变量","compiler/fight-with-compiler/lifetime/loop.html#循环展开","compiler/fight-with-compiler/lifetime/loop.html#深层原因","compiler/fight-with-compiler/lifetime/loop.html#解决方法","compiler/fight-with-compiler/lifetime/loop.html#一个更复杂的例子","compiler/fight-with-compiler/lifetime/loop.html#新编译器-polonius","compiler/fight-with-compiler/lifetime/loop.html#总结","compiler/fight-with-compiler/lifetime/closure-with-static.html#当闭包碰到特征对象-1","compiler/fight-with-compiler/lifetime/closure-with-static.html#学习目标","compiler/fight-with-compiler/lifetime/closure-with-static.html#报错的代码","compiler/fight-with-compiler/lifetime/closure-with-static.html#深入挖掘报错原因","compiler/fight-with-compiler/lifetime/closure-with-static.html#姗姗来迟的正确代码","compiler/fight-with-compiler/lifetime/closure-with-static.html#总结","compiler/fight-with-compiler/borrowing/intro.html#重复借用","compiler/fight-with-compiler/borrowing/ref-exist-in-out-fn.html#同时在函数内外使用引用导致的重复借用错误","compiler/fight-with-compiler/borrowing/ref-exist-in-out-fn.html#正确的代码","compiler/fight-with-compiler/borrowing/ref-exist-in-out-fn.html#重构后的错误代码","compiler/fight-with-compiler/borrowing/ref-exist-in-out-fn.html#大聪明编译器","compiler/fight-with-compiler/borrowing/ref-exist-in-out-fn.html#被冤枉的编译器","compiler/fight-with-compiler/borrowing/ref-exist-in-out-fn.html#解决办法","compiler/fight-with-compiler/borrowing/ref-exist-in-out-fn.html#cpu-模拟例子","compiler/fight-with-compiler/borrowing/ref-exist-in-out-fn.html#总结","compiler/fight-with-compiler/borrowing/borrow-distinct-fields-of-struct.html#智能指针引起的重复借用错误","compiler/fight-with-compiler/borrowing/borrow-distinct-fields-of-struct.html#结构体中的字段借用","compiler/fight-with-compiler/borrowing/borrow-distinct-fields-of-struct.html#refcell","compiler/fight-with-compiler/borrowing/borrow-distinct-fields-of-struct.html#被-refcell-包裹的结构体","compiler/fight-with-compiler/borrowing/borrow-distinct-fields-of-struct.html#深入分析","compiler/fight-with-compiler/borrowing/borrow-distinct-fields-of-struct.html#解决方法","compiler/fight-with-compiler/borrowing/borrow-distinct-fields-of-struct.html#不仅仅是-refcell","compiler/fight-with-compiler/borrowing/borrow-distinct-fields-of-struct.html#一个练习","compiler/fight-with-compiler/borrowing/borrow-distinct-fields-of-struct.html#总结","compiler/fight-with-compiler/unconstrained.html#类型未限制","compiler/fight-with-compiler/phantom-data.html#幽灵数据","compiler/pitfalls/index.html#rust-陷阱系列","compiler/pitfalls/use-vec-in-for.html#for-循环中使用外部数组","compiler/pitfalls/stack-overflow.html#线程类型导致的栈溢出","compiler/pitfalls/arithmetic-overflow.html#算术溢出导致的-panic","compiler/pitfalls/closure-with-lifetime.html#闭包上奇怪的生命周期","compiler/pitfalls/closure-with-lifetime.html#一段简单的代码","compiler/pitfalls/closure-with-lifetime.html#一段复杂的代码","compiler/pitfalls/closure-with-lifetime.html#深入调查","compiler/pitfalls/closure-with-lifetime.html#总结","compiler/pitfalls/the-disabled-mutability.html#失效的可变性","compiler/pitfalls/the-disabled-mutability.html#简单分析","compiler/pitfalls/the-disabled-mutability.html#一个练习","compiler/pitfalls/the-disabled-mutability.html#总结","compiler/pitfalls/multiple-mutable-references.html#代码重构导致的可变借用错误","compiler/pitfalls/multiple-mutable-references.html#欣赏下报错","compiler/pitfalls/multiple-mutable-references.html#重构前的正确代码","compiler/pitfalls/multiple-mutable-references.html#重构后的错误代码","compiler/pitfalls/multiple-mutable-references.html#大聪明编译器","compiler/pitfalls/multiple-mutable-references.html#被冤枉的编译器","compiler/pitfalls/multiple-mutable-references.html#解决办法","compiler/pitfalls/multiple-mutable-references.html#闭包中的例子","compiler/pitfalls/multiple-mutable-references.html#总结","compiler/pitfalls/lazy-iterators.html#不太勤快的迭代器","compiler/pitfalls/lazy-iterators.html#for-循环-vs-迭代器","compiler/pitfalls/lazy-iterators.html#回顾下迭代器","compiler/pitfalls/lazy-iterators.html#懒惰是根因","compiler/pitfalls/lazy-iterators.html#解决办法","compiler/pitfalls/lazy-iterators.html#总结","compiler/pitfalls/weird-ranges.html#奇怪的序列xy","compiler/pitfalls/iterator-everywhere.html#无处不在的迭代器","compiler/pitfalls/iterator-everywhere.html#报错的代码","compiler/pitfalls/iterator-everywhere.html#迭代器回顾","compiler/pitfalls/iterator-everywhere.html#深入调查","compiler/pitfalls/iterator-everywhere.html#最-rusty-的解决方法","compiler/pitfalls/iterator-everywhere.html#总结","compiler/pitfalls/main-with-channel-blocked.html#线程间传递消息导致主线程无法结束","compiler/pitfalls/main-with-channel-blocked.html#总结","compiler/pitfalls/utf8-performance.html#警惕-utf-8-引发的性能隐患","compiler/pitfalls/utf8-performance.html#问题描述--解决","compiler/pitfalls/utf8-performance.html#总结","profiling/intro.html#rust性能剖析-todo","profiling/memory/intro.html#深入内存","profiling/memory/pointer-ref.html#指针和引用todo","profiling/memory/uninit.html#未初始化内存","profiling/memory/allocation.html#内存分配todo","profiling/memory/layout.html#内存布局todo","profiling/memory/virtual.html#虚拟内存","profiling/performance/intro.html#performance","profiling/performance/intro.html#how-do-i-profile-a-rust-web-application-in-production","profiling/performance/intro.html#内存对齐","profiling/performance/intro.html#riggrep-为啥这么快","profiling/performance/intro.html#测试堆性能","profiling/performance/string.html","profiling/performance/deep-into-move.html#rust所有权转移时发生了奇怪的深拷贝","profiling/performance/deep-into-move.html#move时发生了数据的深拷贝","profiling/performance/deep-into-move.html#罪魁祸首println","profiling/performance/deep-into-move.html#栈和堆的不同move行为","profiling/performance/deep-into-move.html#编译器对move的优化","profiling/performance/deep-into-move.html#println阻止了优化","profiling/performance/deep-into-move.html#最佳实践","profiling/performance/early-optimise.html#糟糕的提前优化","profiling/performance/early-optimise.html#函数调用","profiling/performance/clone-copy.html#clone和copy","profiling/performance/runtime-check.html#减少runtime-check","profiling/performance/runtime-check.html#减少集合访问的边界检查","profiling/performance/runtime-check.html#boxleak","profiling/performance/runtime-check.html#bounds-check","profiling/performance/runtime-check.html#使用assert-优化检查性能","profiling/performance/cpu-cache.html#cpu缓存性能优化","profiling/performance/cpu-cache.html#on-a-use-of-the-repr-attribute-in-rust","profiling/performance/cpu-cache.html#动态和静态分发","profiling/performance/calculate.html#计算性能优化","profiling/performance/calculate.html#120ms","profiling/performance/calculate.html#90ms","profiling/performance/heap-stack.html#堆和栈","profiling/performance/allocator.html#内存allocator-todo","profiling/performance/tools.html#常用性能测试工具","profiling/performance/tools.html#profiling","profiling/performance/enum.html#enum内存优化-todo","profiling/compiler/intro.html#对抗编译检查","profiling/compiler/llvm.html#llvm-todo","profiling/compiler/attributes.html#常见属性标记","profiling/compiler/attributes.html#强制内存对齐","profiling/compiler/speed-up.html#优化编译速度","profiling/compiler/optimization/intro.html#编译器优化","profiling/compiler/optimization/option.html#option枚举","appendix/keywords.html#附录-a关键字","appendix/keywords.html#目前正在使用的关键字","appendix/keywords.html#保留做将来使用的关键字","appendix/keywords.html#原生标识符","appendix/operators.html#附录-b运算符与符号","appendix/operators.html#运算符","appendix/operators.html#非运算符符号","appendix/expressions.html#附录-c表达式","appendix/expressions.html#基本表达式","appendix/expressions.html#if-表达式","appendix/expressions.html#if-let-表达式","appendix/expressions.html#match-表达式","appendix/expressions.html#loop-表达式","appendix/expressions.html#语句块-","appendix/derive.html#附录-d派生特征-trait","appendix/derive.html#用于开发者输出的-debug","appendix/derive.html#等值比较的-partialeq-和-eq","appendix/derive.html#次序比较的-partialord-和-ord","appendix/derive.html#复制值的-clone-和-copy","appendix/derive.html#固定大小的值映射的-hash","appendix/derive.html#默认值的-default","appendix/prelude.html#附录-eprelude-模块","appendix/rust-version.html#附录-frust-版本发布","appendix/rust-version.html#rust-版本说明","appendix/rust-version.html#rust-自身开发流程","appendix/rust-version.html#无停滞稳定","appendix/rust-version.html#choo-choo--小火车发布流程启动","appendix/rust-version.html#不稳定功能","appendix/rust-version.html#rustup-和-rust-nightly-的职责","appendix/rust-version.html#rfc-过程和团队","appendix/rust-versions/intro.html#附录-grust-更新版本列表","appendix/rust-versions/1.58.html#rust-新版解读--158--重点-格式化字符串捕获环境中的值","appendix/rust-versions/1.58.html#在格式化字符串时捕获环境中的值","appendix/rust-versions/1.58.html#比-unwrap-更危险的-unwrap_unchecked","appendix/rust-versions/1.59.html#rust-新版解读--159--重点-内联汇编解构式赋值","appendix/rust-versions/1.59.html#内联汇编-inline-assembly-","appendix/rust-versions/1.59.html#解构式赋值-destructuring-assignments","appendix/rust-versions/1.59.html#const-泛型","appendix/rust-versions/1.59.html#减少二进制文件体积删除-debug-信息","appendix/rust-versions/1.59.html#默认关闭增量编译","appendix/rust-versions/1.59.html#稳定化的-api-列表","appendix/rust-versions/1.60.html#rust-新版解读--160--重点-查看-cargo-构建耗时详情cargo-feature-增加新语法","appendix/rust-versions/1.60.html#基于源码的代码覆盖","appendix/rust-versions/1.60.html#查看-cargo-构建耗时","appendix/rust-versions/1.60.html#cargo-feature-的新语法","appendix/rust-versions/1.60.html#增量编译重启开启","appendix/rust-versions/1.60.html#instant-单调性保证","appendix/rust-versions/1.61.html#rust-新版解读--161--重点-自定义-main-函数-exitcodeconst-fn-增强为锁定的-stdio-提供静态句柄","appendix/rust-versions/1.61.html#支持自定义-main-函数-exitcode","appendix/rust-versions/1.61.html#const-fn-增强","appendix/rust-versions/1.61.html#为锁定的-stdio-提供静态句柄","appendix/rust-versions/1.62.html#rust-新版解读--162--重点-cargo-adddefault-枚举变量linux-上更薄更快的-mutex裸机-x86_64-构架","appendix/rust-versions/1.62.html#cargo-add","appendix/rust-versions/1.62.html#default-枚举变量","appendix/rust-versions/1.62.html#linux-上更薄更快的-mutex","appendix/rust-versions/1.62.html#裸机-x86_64-构架","appendix/rust-versions/1.63.html#rust-新版解读--163--重点-socped-threads","appendix/rust-versions/1.63.html#区域线程-scoped-threads","appendix/rust-versions/1.63.html#rust-对原始文件描述符句柄的所有权","appendix/rust-versions/1.63.html#mutex-rwlock-condvar-作为静态变量","appendix/rust-versions/1.63.html#turbofish-可用于含有-impl-trait-的泛型函数上","appendix/rust-versions/1.63.html#完成了-non-lexical-lifetime-的生命周期检查器的迁移","appendix/rust-versions/1.64.html#rust-新版解读--164--重点-intofuture--cargo-优化","appendix/rust-versions/1.64.html#使用-intofuture-增强-await","appendix/rust-versions/1.64.html#core-和-alloc-中和-c-语言兼容的-ffi-类型","appendix/rust-versions/1.64.html#可以通过-rustup-来使用-rust-analyzer","appendix/rust-versions/1.64.html#cargo-优化workspace-继承和多目标构建","appendix/rust-versions/1.64.html#稳定api--others","appendix/rust-versions/1.65.html#rust-新版解读--165--重点-泛型关联类型新绑定语法","appendix/rust-versions/1.65.html#泛型关联类型-generic-associated-types-gats","appendix/rust-versions/1.65.html#let---else-语法","appendix/rust-versions/1.65.html#break-跳出标记过的代码块","appendix/rust-versions/1.65.html#others","appendix/rust-versions/1.66.html#rust-新版解读--166--重点-有字段枚举的显示判别","appendix/rust-versions/1.66.html#对有字段枚举的显示判别","appendix/rust-versions/1.66.html#黑盒方法-corehintblack_box","appendix/rust-versions/1.66.html#cargo-remove","appendix/rust-versions/1.66.html#others","appendix/rust-versions/1.67.html#rust-新版解读--167--must_use-in-async-fn","appendix/rust-versions/1.67.html#must_use-作用于-async-fn-上","appendix/rust-versions/1.67.html#stdsyncmpsc-实现更新","appendix/rust-versions/1.67.html#others","appendix/rust-versions/1.68.html#rust-新版解读--168--crates-index-优化","appendix/rust-versions/1.68.html#cargo-稀疏注册协议-sparse-protocol","appendix/rust-versions/1.68.html#局部-pin-构造","appendix/rust-versions/1.68.html#alloc-默认错误处理","appendix/rust-versions/1.68.html#others","appendix/rust-versions/1.69.html#rust-新版解读--169--cargo-fix","appendix/rust-versions/1.69.html#cargo-提供自动修复建议","appendix/rust-versions/1.69.html#构建脚步默认不再包含调试信息","appendix/rust-versions/1.69.html#others"],"index":{"documentStore":{"docInfo":{"0":{"body":14,"breadcrumbs":0,"title":0},"1":{"body":6,"breadcrumbs":0,"title":0},"10":{"body":26,"breadcrumbs":2,"title":1},"100":{"body":42,"breadcrumbs":1,"title":0},"1000":{"body":236,"breadcrumbs":1,"title":0},"1001":{"body":525,"breadcrumbs":1,"title":0},"1002":{"body":287,"breadcrumbs":1,"title":0},"1003":{"body":195,"breadcrumbs":1,"title":0},"1004":{"body":96,"breadcrumbs":2,"title":1},"1005":{"body":6,"breadcrumbs":3,"title":1},"1006":{"body":37,"breadcrumbs":2,"title":0},"1007":{"body":62,"breadcrumbs":2,"title":0},"1008":{"body":247,"breadcrumbs":2,"title":0},"1009":{"body":636,"breadcrumbs":1,"title":0},"101":{"body":142,"breadcrumbs":2,"title":1},"1010":{"body":369,"breadcrumbs":1,"title":0},"1011":{"body":3,"breadcrumbs":0,"title":0},"1012":{"body":2,"breadcrumbs":0,"title":0},"1013":{"body":380,"breadcrumbs":0,"title":0},"1014":{"body":617,"breadcrumbs":0,"title":0},"1015":{"body":0,"breadcrumbs":0,"title":0},"1016":{"body":0,"breadcrumbs":0,"title":0},"1017":{"body":0,"breadcrumbs":0,"title":0},"1018":{"body":1,"breadcrumbs":1,"title":0},"1019":{"body":226,"breadcrumbs":2,"title":1},"102":{"body":10,"breadcrumbs":1,"title":0},"1020":{"body":221,"breadcrumbs":2,"title":1},"1021":{"body":1,"breadcrumbs":0,"title":0},"1022":{"body":126,"breadcrumbs":0,"title":0},"1023":{"body":22,"breadcrumbs":0,"title":0},"1024":{"body":45,"breadcrumbs":0,"title":0},"1025":{"body":49,"breadcrumbs":0,"title":0},"1026":{"body":23,"breadcrumbs":0,"title":0},"1027":{"body":110,"breadcrumbs":0,"title":0},"1028":{"body":4,"breadcrumbs":1,"title":1},"1029":{"body":0,"breadcrumbs":0,"title":0},"103":{"body":29,"breadcrumbs":1,"title":0},"1030":{"body":1,"breadcrumbs":2,"title":1},"1031":{"body":8,"breadcrumbs":1,"title":0},"1032":{"body":103,"breadcrumbs":1,"title":0},"1033":{"body":100,"breadcrumbs":1,"title":0},"1034":{"body":9,"breadcrumbs":1,"title":0},"1035":{"body":2,"breadcrumbs":1,"title":0},"1036":{"body":5,"breadcrumbs":0,"title":0},"1037":{"body":7,"breadcrumbs":0,"title":0},"1038":{"body":25,"breadcrumbs":0,"title":0},"1039":{"body":54,"breadcrumbs":0,"title":0},"104":{"body":19,"breadcrumbs":3,"title":2},"1040":{"body":3,"breadcrumbs":0,"title":0},"1041":{"body":19,"breadcrumbs":0,"title":0},"1042":{"body":28,"breadcrumbs":0,"title":0},"1043":{"body":69,"breadcrumbs":1,"title":1},"1044":{"body":0,"breadcrumbs":0,"title":0},"1045":{"body":6,"breadcrumbs":0,"title":0},"1046":{"body":25,"breadcrumbs":0,"title":0},"1047":{"body":3,"breadcrumbs":1,"title":1},"1048":{"body":51,"breadcrumbs":1,"title":1},"1049":{"body":25,"breadcrumbs":0,"title":0},"105":{"body":59,"breadcrumbs":1,"title":0},"1050":{"body":38,"breadcrumbs":0,"title":0},"1051":{"body":11,"breadcrumbs":1,"title":1},"1052":{"body":33,"breadcrumbs":0,"title":0},"1053":{"body":1,"breadcrumbs":0,"title":0},"1054":{"body":0,"breadcrumbs":1,"title":0},"1055":{"body":0,"breadcrumbs":1,"title":0},"1056":{"body":2,"breadcrumbs":2,"title":1},"1057":{"body":37,"breadcrumbs":1,"title":0},"1058":{"body":87,"breadcrumbs":1,"title":0},"1059":{"body":93,"breadcrumbs":3,"title":1},"106":{"body":24,"breadcrumbs":1,"title":0},"1060":{"body":1,"breadcrumbs":1,"title":0},"1061":{"body":41,"breadcrumbs":1,"title":0},"1062":{"body":79,"breadcrumbs":1,"title":0},"1063":{"body":10,"breadcrumbs":1,"title":0},"1064":{"body":0,"breadcrumbs":1,"title":0},"1065":{"body":109,"breadcrumbs":1,"title":0},"1066":{"body":10,"breadcrumbs":1,"title":0},"1067":{"body":38,"breadcrumbs":1,"title":0},"1068":{"body":1,"breadcrumbs":1,"title":0},"1069":{"body":1,"breadcrumbs":1,"title":0},"107":{"body":294,"breadcrumbs":1,"title":0},"1070":{"body":7,"breadcrumbs":1,"title":0},"1071":{"body":25,"breadcrumbs":1,"title":0},"1072":{"body":54,"breadcrumbs":1,"title":0},"1073":{"body":3,"breadcrumbs":1,"title":0},"1074":{"body":19,"breadcrumbs":1,"title":0},"1075":{"body":28,"breadcrumbs":1,"title":0},"1076":{"body":110,"breadcrumbs":1,"title":0},"1077":{"body":0,"breadcrumbs":1,"title":0},"1078":{"body":2,"breadcrumbs":1,"title":0},"1079":{"body":48,"breadcrumbs":2,"title":1},"108":{"body":66,"breadcrumbs":1,"title":0},"1080":{"body":1,"breadcrumbs":1,"title":0},"1081":{"body":14,"breadcrumbs":1,"title":0},"1082":{"body":26,"breadcrumbs":1,"title":0},"1083":{"body":0,"breadcrumbs":1,"title":0},"1084":{"body":2,"breadcrumbs":3,"title":1},"1085":{"body":5,"breadcrumbs":1,"title":0},"1086":{"body":49,"breadcrumbs":1,"title":0},"1087":{"body":15,"breadcrumbs":1,"title":0},"1088":{"body":24,"breadcrumbs":1,"title":0},"1089":{"body":58,"breadcrumbs":2,"title":1},"109":{"body":27,"breadcrumbs":3,"title":2},"1090":{"body":5,"breadcrumbs":1,"title":0},"1091":{"body":69,"breadcrumbs":1,"title":0},"1092":{"body":0,"breadcrumbs":1,"title":0},"1093":{"body":25,"breadcrumbs":5,"title":2},"1094":{"body":13,"breadcrumbs":3,"title":0},"1095":{"body":12,"breadcrumbs":3,"title":0},"1096":{"body":0,"breadcrumbs":4,"title":2},"1097":{"body":4,"breadcrumbs":3,"title":0},"1098":{"body":0,"breadcrumbs":5,"title":1},"1099":{"body":2,"breadcrumbs":4,"title":0},"11":{"body":44,"breadcrumbs":1,"title":0},"110":{"body":33,"breadcrumbs":1,"title":0},"1100":{"body":1,"breadcrumbs":5,"title":1},"1101":{"body":1,"breadcrumbs":5,"title":1},"1102":{"body":0,"breadcrumbs":4,"title":0},"1103":{"body":2,"breadcrumbs":4,"title":1},"1104":{"body":2,"breadcrumbs":8,"title":5},"1105":{"body":1,"breadcrumbs":3,"title":0},"1106":{"body":1,"breadcrumbs":4,"title":1},"1107":{"body":1,"breadcrumbs":3,"title":0},"1108":{"body":1,"breadcrumbs":3,"title":2},"1109":{"body":18,"breadcrumbs":5,"title":1},"111":{"body":3,"breadcrumbs":1,"title":0},"1110":{"body":49,"breadcrumbs":5,"title":1},"1111":{"body":182,"breadcrumbs":5,"title":1},"1112":{"body":9,"breadcrumbs":5,"title":1},"1113":{"body":6,"breadcrumbs":5,"title":1},"1114":{"body":8,"breadcrumbs":5,"title":1},"1115":{"body":56,"breadcrumbs":4,"title":0},"1116":{"body":0,"breadcrumbs":4,"title":0},"1117":{"body":2,"breadcrumbs":4,"title":0},"1118":{"body":0,"breadcrumbs":7,"title":1},"1119":{"body":1,"breadcrumbs":7,"title":2},"112":{"body":1,"breadcrumbs":1,"title":0},"1120":{"body":14,"breadcrumbs":5,"title":0},"1121":{"body":1,"breadcrumbs":6,"title":1},"1122":{"body":2,"breadcrumbs":7,"title":2},"1123":{"body":1,"breadcrumbs":6,"title":1},"1124":{"body":1,"breadcrumbs":6,"title":1},"1125":{"body":656,"breadcrumbs":9,"title":4},"1126":{"body":1,"breadcrumbs":5,"title":0},"1127":{"body":78,"breadcrumbs":4,"title":0},"1128":{"body":52,"breadcrumbs":5,"title":1},"1129":{"body":50,"breadcrumbs":5,"title":1},"113":{"body":16,"breadcrumbs":1,"title":0},"1130":{"body":1,"breadcrumbs":4,"title":0},"1131":{"body":2,"breadcrumbs":7,"title":2},"1132":{"body":5,"breadcrumbs":4,"title":0},"1133":{"body":1,"breadcrumbs":5,"title":1},"1134":{"body":3,"breadcrumbs":7,"title":2},"1135":{"body":0,"breadcrumbs":3,"title":0},"1136":{"body":3,"breadcrumbs":7,"title":2},"1137":{"body":0,"breadcrumbs":4,"title":0},"1138":{"body":13,"breadcrumbs":4,"title":0},"1139":{"body":0,"breadcrumbs":4,"title":0},"114":{"body":28,"breadcrumbs":1,"title":0},"1140":{"body":1,"breadcrumbs":4,"title":0},"1141":{"body":1,"breadcrumbs":7,"title":1},"1142":{"body":1,"breadcrumbs":1,"title":0},"1143":{"body":42,"breadcrumbs":1,"title":0},"1144":{"body":15,"breadcrumbs":1,"title":0},"1145":{"body":60,"breadcrumbs":1,"title":0},"1146":{"body":1,"breadcrumbs":2,"title":1},"1147":{"body":155,"breadcrumbs":1,"title":0},"1148":{"body":135,"breadcrumbs":1,"title":0},"1149":{"body":0,"breadcrumbs":2,"title":1},"115":{"body":17,"breadcrumbs":1,"title":0},"1150":{"body":4,"breadcrumbs":1,"title":0},"1151":{"body":24,"breadcrumbs":1,"title":0},"1152":{"body":7,"breadcrumbs":1,"title":0},"1153":{"body":9,"breadcrumbs":2,"title":1},"1154":{"body":11,"breadcrumbs":2,"title":1},"1155":{"body":13,"breadcrumbs":1,"title":0},"1156":{"body":12,"breadcrumbs":4,"title":2},"1157":{"body":8,"breadcrumbs":3,"title":1},"1158":{"body":20,"breadcrumbs":4,"title":2},"1159":{"body":29,"breadcrumbs":4,"title":2},"116":{"body":32,"breadcrumbs":1,"title":0},"1160":{"body":32,"breadcrumbs":4,"title":2},"1161":{"body":12,"breadcrumbs":3,"title":1},"1162":{"body":17,"breadcrumbs":3,"title":1},"1163":{"body":0,"breadcrumbs":4,"title":1},"1164":{"body":0,"breadcrumbs":3,"title":1},"1165":{"body":40,"breadcrumbs":3,"title":1},"1166":{"body":2,"breadcrumbs":3,"title":1},"1167":{"body":7,"breadcrumbs":2,"title":0},"1168":{"body":85,"breadcrumbs":4,"title":2},"1169":{"body":10,"breadcrumbs":2,"title":0},"117":{"body":2,"breadcrumbs":1,"title":0},"1170":{"body":54,"breadcrumbs":5,"title":3},"1171":{"body":20,"breadcrumbs":3,"title":1},"1172":{"body":1,"breadcrumbs":3,"title":1},"1173":{"body":13,"breadcrumbs":5,"title":2},"1174":{"body":72,"breadcrumbs":3,"title":0},"1175":{"body":25,"breadcrumbs":5,"title":2},"1176":{"body":5,"breadcrumbs":5,"title":2},"1177":{"body":49,"breadcrumbs":5,"title":2},"1178":{"body":31,"breadcrumbs":5,"title":2},"1179":{"body":48,"breadcrumbs":4,"title":1},"118":{"body":3,"breadcrumbs":1,"title":0},"1180":{"body":36,"breadcrumbs":4,"title":1},"1181":{"body":9,"breadcrumbs":3,"title":0},"1182":{"body":1,"breadcrumbs":4,"title":1},"1183":{"body":8,"breadcrumbs":8,"title":5},"1184":{"body":75,"breadcrumbs":3,"title":0},"1185":{"body":27,"breadcrumbs":4,"title":1},"1186":{"body":93,"breadcrumbs":5,"title":2},"1187":{"body":2,"breadcrumbs":3,"title":0},"1188":{"body":24,"breadcrumbs":4,"title":1},"1189":{"body":9,"breadcrumbs":9,"title":6},"119":{"body":161,"breadcrumbs":1,"title":0},"1190":{"body":55,"breadcrumbs":5,"title":2},"1191":{"body":24,"breadcrumbs":5,"title":2},"1192":{"body":25,"breadcrumbs":4,"title":1},"1193":{"body":9,"breadcrumbs":10,"title":7},"1194":{"body":14,"breadcrumbs":5,"title":2},"1195":{"body":10,"breadcrumbs":4,"title":1},"1196":{"body":23,"breadcrumbs":5,"title":2},"1197":{"body":22,"breadcrumbs":4,"title":1},"1198":{"body":14,"breadcrumbs":7,"title":4},"1199":{"body":40,"breadcrumbs":5,"title":2},"12":{"body":14,"breadcrumbs":2,"title":1},"120":{"body":36,"breadcrumbs":1,"title":0},"1200":{"body":18,"breadcrumbs":4,"title":1},"1201":{"body":8,"breadcrumbs":6,"title":3},"1202":{"body":41,"breadcrumbs":6,"title":3},"1203":{"body":6,"breadcrumbs":6,"title":3},"1204":{"body":14,"breadcrumbs":7,"title":4},"1205":{"body":145,"breadcrumbs":5,"title":2},"1206":{"body":18,"breadcrumbs":7,"title":4},"1207":{"body":21,"breadcrumbs":6,"title":3},"1208":{"body":15,"breadcrumbs":5,"title":2},"1209":{"body":3,"breadcrumbs":5,"title":2},"121":{"body":24,"breadcrumbs":3,"title":2},"1210":{"body":14,"breadcrumbs":5,"title":2},"1211":{"body":55,"breadcrumbs":7,"title":4},"1212":{"body":65,"breadcrumbs":3,"title":0},"1213":{"body":19,"breadcrumbs":4,"title":1},"1214":{"body":2,"breadcrumbs":4,"title":1},"1215":{"body":14,"breadcrumbs":5,"title":2},"1216":{"body":34,"breadcrumbs":3,"title":0},"1217":{"body":126,"breadcrumbs":4,"title":1},"1218":{"body":5,"breadcrumbs":5,"title":2},"1219":{"body":2,"breadcrumbs":4,"title":1},"122":{"body":8,"breadcrumbs":3,"title":2},"1220":{"body":15,"breadcrumbs":8,"title":5},"1221":{"body":33,"breadcrumbs":6,"title":3},"1222":{"body":5,"breadcrumbs":4,"title":1},"1223":{"body":2,"breadcrumbs":4,"title":1},"1224":{"body":14,"breadcrumbs":7,"title":4},"1225":{"body":19,"breadcrumbs":6,"title":3},"1226":{"body":47,"breadcrumbs":4,"title":1},"1227":{"body":14,"breadcrumbs":4,"title":1},"1228":{"body":2,"breadcrumbs":4,"title":1},"1229":{"body":14,"breadcrumbs":7,"title":4},"123":{"body":78,"breadcrumbs":1,"title":0},"1230":{"body":44,"breadcrumbs":4,"title":1},"1231":{"body":12,"breadcrumbs":3,"title":0},"1232":{"body":2,"breadcrumbs":4,"title":1},"124":{"body":158,"breadcrumbs":2,"title":1},"125":{"body":2,"breadcrumbs":1,"title":0},"126":{"body":9,"breadcrumbs":1,"title":0},"127":{"body":142,"breadcrumbs":1,"title":0},"128":{"body":22,"breadcrumbs":1,"title":0},"129":{"body":119,"breadcrumbs":2,"title":1},"13":{"body":12,"breadcrumbs":1,"title":0},"130":{"body":2,"breadcrumbs":1,"title":0},"131":{"body":15,"breadcrumbs":1,"title":0},"132":{"body":45,"breadcrumbs":1,"title":0},"133":{"body":139,"breadcrumbs":1,"title":0},"134":{"body":18,"breadcrumbs":1,"title":0},"135":{"body":63,"breadcrumbs":1,"title":0},"136":{"body":2,"breadcrumbs":1,"title":0},"137":{"body":2,"breadcrumbs":1,"title":0},"138":{"body":50,"breadcrumbs":1,"title":0},"139":{"body":35,"breadcrumbs":1,"title":0},"14":{"body":5,"breadcrumbs":0,"title":0},"140":{"body":4,"breadcrumbs":1,"title":0},"141":{"body":73,"breadcrumbs":1,"title":0},"142":{"body":10,"breadcrumbs":2,"title":1},"143":{"body":9,"breadcrumbs":2,"title":1},"144":{"body":89,"breadcrumbs":1,"title":0},"145":{"body":44,"breadcrumbs":2,"title":1},"146":{"body":2,"breadcrumbs":1,"title":0},"147":{"body":4,"breadcrumbs":1,"title":0},"148":{"body":38,"breadcrumbs":3,"title":1},"149":{"body":305,"breadcrumbs":3,"title":1},"15":{"body":9,"breadcrumbs":0,"title":0},"150":{"body":17,"breadcrumbs":2,"title":0},"151":{"body":34,"breadcrumbs":3,"title":1},"152":{"body":49,"breadcrumbs":2,"title":0},"153":{"body":2,"breadcrumbs":2,"title":0},"154":{"body":15,"breadcrumbs":3,"title":1},"155":{"body":53,"breadcrumbs":3,"title":1},"156":{"body":0,"breadcrumbs":1,"title":0},"157":{"body":2,"breadcrumbs":1,"title":0},"158":{"body":160,"breadcrumbs":1,"title":0},"159":{"body":0,"breadcrumbs":1,"title":0},"16":{"body":9,"breadcrumbs":0,"title":0},"160":{"body":15,"breadcrumbs":1,"title":0},"161":{"body":60,"breadcrumbs":1,"title":0},"162":{"body":17,"breadcrumbs":1,"title":0},"163":{"body":41,"breadcrumbs":1,"title":0},"164":{"body":301,"breadcrumbs":1,"title":0},"165":{"body":227,"breadcrumbs":1,"title":0},"166":{"body":129,"breadcrumbs":1,"title":0},"167":{"body":119,"breadcrumbs":1,"title":0},"168":{"body":2,"breadcrumbs":1,"title":0},"169":{"body":9,"breadcrumbs":3,"title":1},"17":{"body":9,"breadcrumbs":0,"title":0},"170":{"body":171,"breadcrumbs":2,"title":0},"171":{"body":54,"breadcrumbs":2,"title":0},"172":{"body":45,"breadcrumbs":2,"title":0},"173":{"body":34,"breadcrumbs":2,"title":0},"174":{"body":29,"breadcrumbs":3,"title":1},"175":{"body":23,"breadcrumbs":2,"title":0},"176":{"body":2,"breadcrumbs":2,"title":0},"177":{"body":3,"breadcrumbs":1,"title":0},"178":{"body":54,"breadcrumbs":3,"title":1},"179":{"body":125,"breadcrumbs":2,"title":0},"18":{"body":10,"breadcrumbs":0,"title":0},"180":{"body":78,"breadcrumbs":2,"title":0},"181":{"body":29,"breadcrumbs":2,"title":0},"182":{"body":91,"breadcrumbs":2,"title":0},"183":{"body":172,"breadcrumbs":5,"title":3},"184":{"body":33,"breadcrumbs":2,"title":0},"185":{"body":3,"breadcrumbs":2,"title":0},"186":{"body":19,"breadcrumbs":3,"title":1},"187":{"body":15,"breadcrumbs":2,"title":0},"188":{"body":155,"breadcrumbs":2,"title":0},"189":{"body":19,"breadcrumbs":2,"title":0},"19":{"body":6,"breadcrumbs":1,"title":1},"190":{"body":138,"breadcrumbs":4,"title":2},"191":{"body":73,"breadcrumbs":4,"title":2},"192":{"body":156,"breadcrumbs":3,"title":1},"193":{"body":11,"breadcrumbs":3,"title":1},"194":{"body":24,"breadcrumbs":2,"title":0},"195":{"body":161,"breadcrumbs":2,"title":0},"196":{"body":2,"breadcrumbs":2,"title":0},"197":{"body":49,"breadcrumbs":1,"title":0},"198":{"body":273,"breadcrumbs":1,"title":0},"199":{"body":57,"breadcrumbs":1,"title":0},"2":{"body":4,"breadcrumbs":0,"title":0},"20":{"body":1,"breadcrumbs":0,"title":0},"200":{"body":31,"breadcrumbs":3,"title":2},"201":{"body":50,"breadcrumbs":1,"title":0},"202":{"body":2,"breadcrumbs":1,"title":0},"203":{"body":2,"breadcrumbs":1,"title":0},"204":{"body":111,"breadcrumbs":1,"title":0},"205":{"body":108,"breadcrumbs":1,"title":0},"206":{"body":147,"breadcrumbs":1,"title":0},"207":{"body":93,"breadcrumbs":1,"title":0},"208":{"body":87,"breadcrumbs":2,"title":1},"209":{"body":2,"breadcrumbs":1,"title":0},"21":{"body":26,"breadcrumbs":1,"title":1},"210":{"body":13,"breadcrumbs":1,"title":0},"211":{"body":1,"breadcrumbs":3,"title":1},"212":{"body":1,"breadcrumbs":2,"title":0},"213":{"body":23,"breadcrumbs":3,"title":1},"214":{"body":11,"breadcrumbs":3,"title":1},"215":{"body":7,"breadcrumbs":3,"title":1},"216":{"body":8,"breadcrumbs":3,"title":1},"217":{"body":24,"breadcrumbs":3,"title":1},"218":{"body":17,"breadcrumbs":2,"title":0},"219":{"body":78,"breadcrumbs":2,"title":0},"22":{"body":7,"breadcrumbs":0,"title":0},"220":{"body":15,"breadcrumbs":3,"title":1},"221":{"body":65,"breadcrumbs":2,"title":0},"222":{"body":2,"breadcrumbs":2,"title":0},"223":{"body":12,"breadcrumbs":5,"title":2},"224":{"body":4,"breadcrumbs":4,"title":1},"225":{"body":29,"breadcrumbs":4,"title":1},"226":{"body":73,"breadcrumbs":4,"title":1},"227":{"body":106,"breadcrumbs":3,"title":0},"228":{"body":46,"breadcrumbs":4,"title":1},"229":{"body":59,"breadcrumbs":4,"title":1},"23":{"body":9,"breadcrumbs":2,"title":1},"230":{"body":51,"breadcrumbs":3,"title":0},"231":{"body":2,"breadcrumbs":3,"title":0},"232":{"body":3,"breadcrumbs":1,"title":0},"233":{"body":52,"breadcrumbs":1,"title":0},"234":{"body":37,"breadcrumbs":1,"title":0},"235":{"body":75,"breadcrumbs":1,"title":0},"236":{"body":193,"breadcrumbs":1,"title":0},"237":{"body":74,"breadcrumbs":1,"title":0},"238":{"body":144,"breadcrumbs":1,"title":0},"239":{"body":126,"breadcrumbs":1,"title":0},"24":{"body":15,"breadcrumbs":4,"title":3},"240":{"body":20,"breadcrumbs":1,"title":0},"241":{"body":24,"breadcrumbs":1,"title":0},"242":{"body":2,"breadcrumbs":1,"title":0},"243":{"body":2,"breadcrumbs":1,"title":0},"244":{"body":5,"breadcrumbs":1,"title":0},"245":{"body":7,"breadcrumbs":2,"title":1},"246":{"body":0,"breadcrumbs":3,"title":1},"247":{"body":3,"breadcrumbs":3,"title":1},"248":{"body":43,"breadcrumbs":2,"title":0},"249":{"body":36,"breadcrumbs":2,"title":0},"25":{"body":17,"breadcrumbs":2,"title":1},"250":{"body":95,"breadcrumbs":3,"title":1},"251":{"body":8,"breadcrumbs":3,"title":1},"252":{"body":5,"breadcrumbs":3,"title":1},"253":{"body":51,"breadcrumbs":3,"title":1},"254":{"body":22,"breadcrumbs":3,"title":1},"255":{"body":2,"breadcrumbs":2,"title":0},"256":{"body":101,"breadcrumbs":3,"title":1},"257":{"body":41,"breadcrumbs":2,"title":0},"258":{"body":80,"breadcrumbs":5,"title":3},"259":{"body":50,"breadcrumbs":2,"title":0},"26":{"body":210,"breadcrumbs":3,"title":2},"260":{"body":240,"breadcrumbs":2,"title":0},"261":{"body":2,"breadcrumbs":2,"title":0},"262":{"body":8,"breadcrumbs":1,"title":0},"263":{"body":7,"breadcrumbs":3,"title":1},"264":{"body":14,"breadcrumbs":2,"title":0},"265":{"body":119,"breadcrumbs":3,"title":1},"266":{"body":2,"breadcrumbs":2,"title":0},"267":{"body":2,"breadcrumbs":3,"title":1},"268":{"body":26,"breadcrumbs":2,"title":0},"269":{"body":23,"breadcrumbs":2,"title":0},"27":{"body":5,"breadcrumbs":1,"title":0},"270":{"body":57,"breadcrumbs":2,"title":0},"271":{"body":63,"breadcrumbs":2,"title":0},"272":{"body":20,"breadcrumbs":3,"title":1},"273":{"body":16,"breadcrumbs":3,"title":1},"274":{"body":3,"breadcrumbs":2,"title":0},"275":{"body":78,"breadcrumbs":2,"title":0},"276":{"body":2,"breadcrumbs":2,"title":0},"277":{"body":3,"breadcrumbs":3,"title":1},"278":{"body":55,"breadcrumbs":2,"title":0},"279":{"body":35,"breadcrumbs":2,"title":0},"28":{"body":28,"breadcrumbs":1,"title":0},"280":{"body":27,"breadcrumbs":2,"title":0},"281":{"body":27,"breadcrumbs":2,"title":0},"282":{"body":36,"breadcrumbs":2,"title":0},"283":{"body":19,"breadcrumbs":2,"title":0},"284":{"body":281,"breadcrumbs":2,"title":0},"285":{"body":2,"breadcrumbs":2,"title":0},"286":{"body":2,"breadcrumbs":1,"title":0},"287":{"body":4,"breadcrumbs":1,"title":0},"288":{"body":18,"breadcrumbs":1,"title":0},"289":{"body":60,"breadcrumbs":1,"title":0},"29":{"body":4,"breadcrumbs":1,"title":0},"290":{"body":13,"breadcrumbs":1,"title":0},"291":{"body":161,"breadcrumbs":3,"title":2},"292":{"body":68,"breadcrumbs":1,"title":0},"293":{"body":13,"breadcrumbs":1,"title":0},"294":{"body":97,"breadcrumbs":1,"title":0},"295":{"body":1,"breadcrumbs":1,"title":0},"296":{"body":2,"breadcrumbs":1,"title":0},"297":{"body":1,"breadcrumbs":1,"title":0},"298":{"body":31,"breadcrumbs":1,"title":0},"299":{"body":34,"breadcrumbs":2,"title":1},"3":{"body":41,"breadcrumbs":2,"title":1},"30":{"body":29,"breadcrumbs":2,"title":1},"300":{"body":150,"breadcrumbs":1,"title":0},"301":{"body":26,"breadcrumbs":1,"title":0},"302":{"body":46,"breadcrumbs":1,"title":0},"303":{"body":13,"breadcrumbs":1,"title":0},"304":{"body":55,"breadcrumbs":1,"title":0},"305":{"body":23,"breadcrumbs":1,"title":0},"306":{"body":27,"breadcrumbs":1,"title":0},"307":{"body":29,"breadcrumbs":1,"title":0},"308":{"body":8,"breadcrumbs":1,"title":0},"309":{"body":7,"breadcrumbs":1,"title":0},"31":{"body":31,"breadcrumbs":3,"title":2},"310":{"body":11,"breadcrumbs":1,"title":0},"311":{"body":72,"breadcrumbs":3,"title":2},"312":{"body":2,"breadcrumbs":1,"title":0},"313":{"body":5,"breadcrumbs":1,"title":0},"314":{"body":5,"breadcrumbs":0,"title":0},"315":{"body":0,"breadcrumbs":0,"title":0},"316":{"body":41,"breadcrumbs":0,"title":0},"317":{"body":56,"breadcrumbs":0,"title":0},"318":{"body":42,"breadcrumbs":0,"title":0},"319":{"body":86,"breadcrumbs":0,"title":0},"32":{"body":16,"breadcrumbs":1,"title":0},"320":{"body":11,"breadcrumbs":0,"title":0},"321":{"body":16,"breadcrumbs":1,"title":1},"322":{"body":23,"breadcrumbs":0,"title":0},"323":{"body":78,"breadcrumbs":0,"title":0},"324":{"body":34,"breadcrumbs":0,"title":0},"325":{"body":43,"breadcrumbs":0,"title":0},"326":{"body":30,"breadcrumbs":2,"title":2},"327":{"body":106,"breadcrumbs":1,"title":1},"328":{"body":36,"breadcrumbs":0,"title":0},"329":{"body":103,"breadcrumbs":0,"title":0},"33":{"body":19,"breadcrumbs":2,"title":1},"330":{"body":18,"breadcrumbs":0,"title":0},"331":{"body":70,"breadcrumbs":0,"title":0},"332":{"body":5,"breadcrumbs":0,"title":0},"333":{"body":110,"breadcrumbs":0,"title":0},"334":{"body":2,"breadcrumbs":0,"title":0},"335":{"body":16,"breadcrumbs":0,"title":0},"336":{"body":20,"breadcrumbs":0,"title":0},"337":{"body":106,"breadcrumbs":0,"title":0},"338":{"body":78,"breadcrumbs":2,"title":2},"339":{"body":9,"breadcrumbs":0,"title":0},"34":{"body":59,"breadcrumbs":1,"title":0},"340":{"body":263,"breadcrumbs":0,"title":0},"341":{"body":7,"breadcrumbs":0,"title":0},"342":{"body":10,"breadcrumbs":0,"title":0},"343":{"body":40,"breadcrumbs":1,"title":1},"344":{"body":4,"breadcrumbs":0,"title":0},"345":{"body":29,"breadcrumbs":1,"title":1},"346":{"body":51,"breadcrumbs":0,"title":0},"347":{"body":43,"breadcrumbs":0,"title":0},"348":{"body":36,"breadcrumbs":0,"title":0},"349":{"body":3,"breadcrumbs":0,"title":0},"35":{"body":69,"breadcrumbs":1,"title":0},"350":{"body":6,"breadcrumbs":2,"title":1},"351":{"body":2,"breadcrumbs":1,"title":0},"352":{"body":0,"breadcrumbs":1,"title":0},"353":{"body":213,"breadcrumbs":1,"title":0},"354":{"body":21,"breadcrumbs":1,"title":0},"355":{"body":68,"breadcrumbs":2,"title":1},"356":{"body":70,"breadcrumbs":1,"title":0},"357":{"body":70,"breadcrumbs":5,"title":4},"358":{"body":105,"breadcrumbs":2,"title":1},"359":{"body":37,"breadcrumbs":1,"title":0},"36":{"body":21,"breadcrumbs":3,"title":2},"360":{"body":152,"breadcrumbs":1,"title":0},"361":{"body":45,"breadcrumbs":7,"title":3},"362":{"body":63,"breadcrumbs":5,"title":1},"363":{"body":112,"breadcrumbs":6,"title":2},"364":{"body":20,"breadcrumbs":5,"title":1},"365":{"body":2,"breadcrumbs":4,"title":0},"366":{"body":15,"breadcrumbs":4,"title":0},"367":{"body":4,"breadcrumbs":1,"title":0},"368":{"body":21,"breadcrumbs":3,"title":1},"369":{"body":0,"breadcrumbs":2,"title":0},"37":{"body":18,"breadcrumbs":3,"title":2},"370":{"body":141,"breadcrumbs":2,"title":0},"371":{"body":86,"breadcrumbs":2,"title":0},"372":{"body":72,"breadcrumbs":2,"title":0},"373":{"body":47,"breadcrumbs":2,"title":0},"374":{"body":0,"breadcrumbs":2,"title":0},"375":{"body":424,"breadcrumbs":3,"title":1},"376":{"body":137,"breadcrumbs":2,"title":0},"377":{"body":0,"breadcrumbs":2,"title":0},"378":{"body":1,"breadcrumbs":2,"title":0},"379":{"body":3,"breadcrumbs":3,"title":1},"38":{"body":17,"breadcrumbs":2,"title":1},"380":{"body":38,"breadcrumbs":2,"title":0},"381":{"body":12,"breadcrumbs":2,"title":0},"382":{"body":69,"breadcrumbs":3,"title":1},"383":{"body":97,"breadcrumbs":3,"title":1},"384":{"body":160,"breadcrumbs":2,"title":0},"385":{"body":150,"breadcrumbs":3,"title":1},"386":{"body":109,"breadcrumbs":2,"title":0},"387":{"body":0,"breadcrumbs":2,"title":0},"388":{"body":3,"breadcrumbs":1,"title":0},"389":{"body":3,"breadcrumbs":1,"title":0},"39":{"body":21,"breadcrumbs":1,"title":0},"390":{"body":79,"breadcrumbs":1,"title":0},"391":{"body":53,"breadcrumbs":2,"title":1},"392":{"body":356,"breadcrumbs":1,"title":0},"393":{"body":3,"breadcrumbs":1,"title":0},"394":{"body":2,"breadcrumbs":3,"title":1},"395":{"body":121,"breadcrumbs":3,"title":1},"396":{"body":95,"breadcrumbs":4,"title":2},"397":{"body":50,"breadcrumbs":2,"title":0},"398":{"body":5,"breadcrumbs":5,"title":2},"399":{"body":74,"breadcrumbs":4,"title":1},"4":{"body":3,"breadcrumbs":1,"title":0},"40":{"body":3,"breadcrumbs":2,"title":1},"400":{"body":39,"breadcrumbs":4,"title":1},"401":{"body":48,"breadcrumbs":4,"title":1},"402":{"body":2,"breadcrumbs":1,"title":0},"403":{"body":0,"breadcrumbs":1,"title":0},"404":{"body":48,"breadcrumbs":2,"title":1},"405":{"body":41,"breadcrumbs":1,"title":0},"406":{"body":96,"breadcrumbs":2,"title":1},"407":{"body":39,"breadcrumbs":2,"title":1},"408":{"body":2,"breadcrumbs":1,"title":0},"409":{"body":21,"breadcrumbs":1,"title":0},"41":{"body":5,"breadcrumbs":4,"title":2},"410":{"body":3,"breadcrumbs":3,"title":1},"411":{"body":34,"breadcrumbs":3,"title":1},"412":{"body":149,"breadcrumbs":3,"title":1},"413":{"body":47,"breadcrumbs":3,"title":1},"414":{"body":43,"breadcrumbs":3,"title":1},"415":{"body":12,"breadcrumbs":2,"title":0},"416":{"body":47,"breadcrumbs":3,"title":1},"417":{"body":37,"breadcrumbs":2,"title":0},"418":{"body":64,"breadcrumbs":2,"title":0},"419":{"body":10,"breadcrumbs":2,"title":0},"42":{"body":66,"breadcrumbs":2,"title":0},"420":{"body":85,"breadcrumbs":3,"title":1},"421":{"body":88,"breadcrumbs":3,"title":1},"422":{"body":88,"breadcrumbs":3,"title":1},"423":{"body":11,"breadcrumbs":2,"title":0},"424":{"body":3,"breadcrumbs":3,"title":1},"425":{"body":0,"breadcrumbs":2,"title":0},"426":{"body":5,"breadcrumbs":3,"title":1},"427":{"body":99,"breadcrumbs":3,"title":1},"428":{"body":61,"breadcrumbs":2,"title":0},"429":{"body":6,"breadcrumbs":3,"title":1},"43":{"body":101,"breadcrumbs":3,"title":1},"430":{"body":29,"breadcrumbs":4,"title":2},"431":{"body":5,"breadcrumbs":2,"title":0},"432":{"body":7,"breadcrumbs":6,"title":2},"433":{"body":161,"breadcrumbs":5,"title":1},"434":{"body":32,"breadcrumbs":5,"title":1},"435":{"body":34,"breadcrumbs":5,"title":1},"436":{"body":8,"breadcrumbs":4,"title":0},"437":{"body":8,"breadcrumbs":5,"title":2},"438":{"body":48,"breadcrumbs":4,"title":1},"439":{"body":62,"breadcrumbs":4,"title":1},"44":{"body":2,"breadcrumbs":0,"title":0},"440":{"body":50,"breadcrumbs":5,"title":2},"441":{"body":104,"breadcrumbs":3,"title":0},"442":{"body":85,"breadcrumbs":5,"title":2},"443":{"body":99,"breadcrumbs":4,"title":1},"444":{"body":10,"breadcrumbs":3,"title":0},"445":{"body":6,"breadcrumbs":1,"title":0},"446":{"body":4,"breadcrumbs":3,"title":1},"447":{"body":145,"breadcrumbs":2,"title":0},"448":{"body":69,"breadcrumbs":3,"title":1},"449":{"body":136,"breadcrumbs":3,"title":1},"45":{"body":47,"breadcrumbs":0,"title":0},"450":{"body":6,"breadcrumbs":3,"title":1},"451":{"body":5,"breadcrumbs":2,"title":0},"452":{"body":1,"breadcrumbs":1,"title":0},"453":{"body":32,"breadcrumbs":1,"title":0},"454":{"body":95,"breadcrumbs":2,"title":1},"455":{"body":134,"breadcrumbs":2,"title":1},"456":{"body":69,"breadcrumbs":2,"title":1},"457":{"body":117,"breadcrumbs":2,"title":1},"458":{"body":0,"breadcrumbs":5,"title":4},"459":{"body":0,"breadcrumbs":1,"title":0},"46":{"body":37,"breadcrumbs":0,"title":0},"460":{"body":9,"breadcrumbs":1,"title":0},"461":{"body":0,"breadcrumbs":1,"title":0},"462":{"body":10,"breadcrumbs":1,"title":0},"463":{"body":11,"breadcrumbs":1,"title":0},"464":{"body":31,"breadcrumbs":2,"title":1},"465":{"body":0,"breadcrumbs":1,"title":0},"466":{"body":25,"breadcrumbs":1,"title":0},"467":{"body":2,"breadcrumbs":1,"title":0},"468":{"body":5,"breadcrumbs":1,"title":0},"469":{"body":69,"breadcrumbs":1,"title":0},"47":{"body":53,"breadcrumbs":2,"title":1},"470":{"body":70,"breadcrumbs":1,"title":0},"471":{"body":128,"breadcrumbs":2,"title":1},"472":{"body":40,"breadcrumbs":1,"title":0},"473":{"body":40,"breadcrumbs":1,"title":0},"474":{"body":49,"breadcrumbs":2,"title":1},"475":{"body":110,"breadcrumbs":4,"title":3},"476":{"body":44,"breadcrumbs":1,"title":0},"477":{"body":37,"breadcrumbs":1,"title":0},"478":{"body":10,"breadcrumbs":1,"title":0},"479":{"body":10,"breadcrumbs":1,"title":0},"48":{"body":1,"breadcrumbs":1,"title":0},"480":{"body":3,"breadcrumbs":1,"title":0},"481":{"body":29,"breadcrumbs":1,"title":0},"482":{"body":26,"breadcrumbs":2,"title":1},"483":{"body":56,"breadcrumbs":1,"title":0},"484":{"body":29,"breadcrumbs":1,"title":0},"485":{"body":27,"breadcrumbs":1,"title":0},"486":{"body":1,"breadcrumbs":1,"title":0},"487":{"body":72,"breadcrumbs":1,"title":0},"488":{"body":4,"breadcrumbs":1,"title":0},"489":{"body":30,"breadcrumbs":1,"title":0},"49":{"body":8,"breadcrumbs":1,"title":0},"490":{"body":30,"breadcrumbs":1,"title":0},"491":{"body":6,"breadcrumbs":2,"title":1},"492":{"body":1,"breadcrumbs":3,"title":1},"493":{"body":0,"breadcrumbs":2,"title":0},"494":{"body":189,"breadcrumbs":3,"title":1},"495":{"body":182,"breadcrumbs":2,"title":0},"496":{"body":51,"breadcrumbs":3,"title":1},"497":{"body":14,"breadcrumbs":4,"title":2},"498":{"body":4,"breadcrumbs":2,"title":0},"499":{"body":84,"breadcrumbs":3,"title":1},"5":{"body":15,"breadcrumbs":1,"title":0},"50":{"body":4,"breadcrumbs":1,"title":0},"500":{"body":30,"breadcrumbs":3,"title":1},"501":{"body":1,"breadcrumbs":2,"title":0},"502":{"body":12,"breadcrumbs":3,"title":1},"503":{"body":83,"breadcrumbs":3,"title":1},"504":{"body":198,"breadcrumbs":2,"title":0},"505":{"body":27,"breadcrumbs":3,"title":1},"506":{"body":6,"breadcrumbs":3,"title":1},"507":{"body":5,"breadcrumbs":3,"title":1},"508":{"body":1,"breadcrumbs":5,"title":2},"509":{"body":35,"breadcrumbs":4,"title":1},"51":{"body":9,"breadcrumbs":1,"title":0},"510":{"body":27,"breadcrumbs":5,"title":2},"511":{"body":26,"breadcrumbs":5,"title":2},"512":{"body":13,"breadcrumbs":4,"title":1},"513":{"body":46,"breadcrumbs":4,"title":1},"514":{"body":75,"breadcrumbs":4,"title":1},"515":{"body":7,"breadcrumbs":3,"title":0},"516":{"body":2,"breadcrumbs":1,"title":0},"517":{"body":85,"breadcrumbs":1,"title":0},"518":{"body":241,"breadcrumbs":1,"title":0},"519":{"body":69,"breadcrumbs":2,"title":1},"52":{"body":113,"breadcrumbs":1,"title":0},"520":{"body":3,"breadcrumbs":1,"title":0},"521":{"body":3,"breadcrumbs":1,"title":0},"522":{"body":575,"breadcrumbs":1,"title":0},"523":{"body":324,"breadcrumbs":1,"title":0},"524":{"body":139,"breadcrumbs":1,"title":0},"525":{"body":96,"breadcrumbs":1,"title":0},"526":{"body":1,"breadcrumbs":1,"title":0},"527":{"body":2,"breadcrumbs":4,"title":1},"528":{"body":18,"breadcrumbs":4,"title":1},"529":{"body":26,"breadcrumbs":4,"title":1},"53":{"body":31,"breadcrumbs":1,"title":0},"530":{"body":10,"breadcrumbs":4,"title":1},"531":{"body":11,"breadcrumbs":3,"title":0},"532":{"body":13,"breadcrumbs":4,"title":1},"533":{"body":1,"breadcrumbs":3,"title":0},"534":{"body":134,"breadcrumbs":3,"title":0},"535":{"body":33,"breadcrumbs":4,"title":1},"536":{"body":208,"breadcrumbs":4,"title":1},"537":{"body":89,"breadcrumbs":4,"title":1},"538":{"body":0,"breadcrumbs":3,"title":0},"539":{"body":27,"breadcrumbs":4,"title":1},"54":{"body":17,"breadcrumbs":1,"title":0},"540":{"body":15,"breadcrumbs":4,"title":1},"541":{"body":96,"breadcrumbs":3,"title":0},"542":{"body":6,"breadcrumbs":3,"title":0},"543":{"body":3,"breadcrumbs":3,"title":0},"544":{"body":14,"breadcrumbs":3,"title":0},"545":{"body":7,"breadcrumbs":3,"title":0},"546":{"body":76,"breadcrumbs":3,"title":0},"547":{"body":62,"breadcrumbs":3,"title":0},"548":{"body":76,"breadcrumbs":3,"title":0},"549":{"body":177,"breadcrumbs":4,"title":1},"55":{"body":39,"breadcrumbs":1,"title":0},"550":{"body":5,"breadcrumbs":3,"title":0},"551":{"body":31,"breadcrumbs":3,"title":1},"552":{"body":10,"breadcrumbs":2,"title":0},"553":{"body":121,"breadcrumbs":3,"title":1},"554":{"body":20,"breadcrumbs":2,"title":0},"555":{"body":471,"breadcrumbs":3,"title":1},"556":{"body":23,"breadcrumbs":4,"title":2},"557":{"body":16,"breadcrumbs":4,"title":2},"558":{"body":19,"breadcrumbs":2,"title":0},"559":{"body":8,"breadcrumbs":2,"title":0},"56":{"body":9,"breadcrumbs":1,"title":0},"560":{"body":7,"breadcrumbs":2,"title":0},"561":{"body":15,"breadcrumbs":4,"title":1},"562":{"body":120,"breadcrumbs":4,"title":1},"563":{"body":49,"breadcrumbs":5,"title":2},"564":{"body":218,"breadcrumbs":4,"title":1},"565":{"body":4,"breadcrumbs":4,"title":1},"566":{"body":241,"breadcrumbs":4,"title":1},"567":{"body":105,"breadcrumbs":4,"title":1},"568":{"body":184,"breadcrumbs":4,"title":1},"569":{"body":113,"breadcrumbs":4,"title":1},"57":{"body":90,"breadcrumbs":2,"title":1},"570":{"body":25,"breadcrumbs":6,"title":2},"571":{"body":108,"breadcrumbs":5,"title":1},"572":{"body":39,"breadcrumbs":5,"title":1},"573":{"body":139,"breadcrumbs":5,"title":1},"574":{"body":338,"breadcrumbs":5,"title":1},"575":{"body":29,"breadcrumbs":4,"title":0},"576":{"body":40,"breadcrumbs":6,"title":2},"577":{"body":109,"breadcrumbs":5,"title":1},"578":{"body":31,"breadcrumbs":6,"title":2},"579":{"body":16,"breadcrumbs":5,"title":1},"58":{"body":2,"breadcrumbs":1,"title":0},"580":{"body":134,"breadcrumbs":5,"title":1},"581":{"body":2,"breadcrumbs":6,"title":3},"582":{"body":54,"breadcrumbs":4,"title":1},"583":{"body":70,"breadcrumbs":4,"title":1},"584":{"body":172,"breadcrumbs":4,"title":1},"585":{"body":129,"breadcrumbs":4,"title":1},"586":{"body":2,"breadcrumbs":2,"title":0},"587":{"body":63,"breadcrumbs":3,"title":1},"588":{"body":110,"breadcrumbs":4,"title":2},"589":{"body":71,"breadcrumbs":4,"title":2},"59":{"body":18,"breadcrumbs":1,"title":0},"590":{"body":72,"breadcrumbs":3,"title":1},"591":{"body":3,"breadcrumbs":5,"title":1},"592":{"body":98,"breadcrumbs":5,"title":1},"593":{"body":111,"breadcrumbs":4,"title":0},"594":{"body":52,"breadcrumbs":4,"title":0},"595":{"body":28,"breadcrumbs":4,"title":0},"596":{"body":166,"breadcrumbs":5,"title":1},"597":{"body":12,"breadcrumbs":3,"title":1},"598":{"body":9,"breadcrumbs":3,"title":1},"599":{"body":63,"breadcrumbs":3,"title":1},"6":{"body":13,"breadcrumbs":1,"title":0},"60":{"body":32,"breadcrumbs":1,"title":0},"600":{"body":115,"breadcrumbs":2,"title":0},"601":{"body":24,"breadcrumbs":3,"title":1},"602":{"body":47,"breadcrumbs":2,"title":0},"603":{"body":64,"breadcrumbs":3,"title":1},"604":{"body":95,"breadcrumbs":2,"title":0},"605":{"body":0,"breadcrumbs":3,"title":1},"606":{"body":53,"breadcrumbs":2,"title":0},"607":{"body":13,"breadcrumbs":2,"title":0},"608":{"body":11,"breadcrumbs":2,"title":0},"609":{"body":16,"breadcrumbs":2,"title":0},"61":{"body":9,"breadcrumbs":1,"title":0},"610":{"body":186,"breadcrumbs":3,"title":1},"611":{"body":39,"breadcrumbs":4,"title":2},"612":{"body":54,"breadcrumbs":2,"title":0},"613":{"body":54,"breadcrumbs":3,"title":1},"614":{"body":191,"breadcrumbs":2,"title":0},"615":{"body":165,"breadcrumbs":3,"title":1},"616":{"body":22,"breadcrumbs":2,"title":0},"617":{"body":4,"breadcrumbs":2,"title":0},"618":{"body":183,"breadcrumbs":3,"title":1},"619":{"body":175,"breadcrumbs":2,"title":0},"62":{"body":102,"breadcrumbs":1,"title":0},"620":{"body":199,"breadcrumbs":2,"title":0},"621":{"body":4,"breadcrumbs":2,"title":0},"622":{"body":29,"breadcrumbs":2,"title":0},"623":{"body":6,"breadcrumbs":3,"title":1},"624":{"body":5,"breadcrumbs":4,"title":1},"625":{"body":28,"breadcrumbs":3,"title":0},"626":{"body":9,"breadcrumbs":4,"title":1},"627":{"body":21,"breadcrumbs":3,"title":0},"628":{"body":33,"breadcrumbs":3,"title":0},"629":{"body":2,"breadcrumbs":3,"title":0},"63":{"body":178,"breadcrumbs":1,"title":0},"630":{"body":5,"breadcrumbs":3,"title":1},"631":{"body":8,"breadcrumbs":2,"title":0},"632":{"body":40,"breadcrumbs":2,"title":0},"633":{"body":57,"breadcrumbs":4,"title":2},"634":{"body":104,"breadcrumbs":2,"title":0},"635":{"body":14,"breadcrumbs":4,"title":2},"636":{"body":4,"breadcrumbs":2,"title":0},"637":{"body":26,"breadcrumbs":2,"title":0},"638":{"body":70,"breadcrumbs":3,"title":1},"639":{"body":274,"breadcrumbs":2,"title":0},"64":{"body":55,"breadcrumbs":1,"title":0},"640":{"body":70,"breadcrumbs":3,"title":1},"641":{"body":1,"breadcrumbs":2,"title":0},"642":{"body":6,"breadcrumbs":2,"title":0},"643":{"body":13,"breadcrumbs":3,"title":1},"644":{"body":72,"breadcrumbs":3,"title":1},"645":{"body":44,"breadcrumbs":3,"title":1},"646":{"body":42,"breadcrumbs":2,"title":0},"647":{"body":162,"breadcrumbs":3,"title":1},"648":{"body":21,"breadcrumbs":2,"title":0},"649":{"body":34,"breadcrumbs":2,"title":0},"65":{"body":43,"breadcrumbs":1,"title":0},"650":{"body":22,"breadcrumbs":2,"title":0},"651":{"body":13,"breadcrumbs":4,"title":2},"652":{"body":14,"breadcrumbs":2,"title":0},"653":{"body":52,"breadcrumbs":2,"title":0},"654":{"body":57,"breadcrumbs":2,"title":0},"655":{"body":92,"breadcrumbs":2,"title":0},"656":{"body":25,"breadcrumbs":2,"title":0},"657":{"body":12,"breadcrumbs":4,"title":1},"658":{"body":122,"breadcrumbs":5,"title":2},"659":{"body":29,"breadcrumbs":3,"title":0},"66":{"body":19,"breadcrumbs":2,"title":1},"660":{"body":226,"breadcrumbs":4,"title":1},"661":{"body":70,"breadcrumbs":2,"title":0},"662":{"body":159,"breadcrumbs":4,"title":2},"663":{"body":40,"breadcrumbs":2,"title":0},"664":{"body":100,"breadcrumbs":4,"title":2},"665":{"body":4,"breadcrumbs":4,"title":1},"666":{"body":226,"breadcrumbs":4,"title":1},"667":{"body":118,"breadcrumbs":4,"title":1},"668":{"body":287,"breadcrumbs":4,"title":1},"669":{"body":190,"breadcrumbs":3,"title":0},"67":{"body":36,"breadcrumbs":1,"title":0},"670":{"body":8,"breadcrumbs":3,"title":0},"671":{"body":1,"breadcrumbs":4,"title":1},"672":{"body":189,"breadcrumbs":4,"title":1},"673":{"body":77,"breadcrumbs":3,"title":0},"674":{"body":24,"breadcrumbs":3,"title":0},"675":{"body":46,"breadcrumbs":3,"title":0},"676":{"body":41,"breadcrumbs":3,"title":0},"677":{"body":77,"breadcrumbs":3,"title":0},"678":{"body":257,"breadcrumbs":3,"title":0},"679":{"body":6,"breadcrumbs":5,"title":2},"68":{"body":9,"breadcrumbs":1,"title":0},"680":{"body":17,"breadcrumbs":4,"title":1},"681":{"body":132,"breadcrumbs":3,"title":0},"682":{"body":105,"breadcrumbs":3,"title":0},"683":{"body":117,"breadcrumbs":4,"title":1},"684":{"body":8,"breadcrumbs":2,"title":0},"685":{"body":23,"breadcrumbs":2,"title":0},"686":{"body":15,"breadcrumbs":2,"title":0},"687":{"body":46,"breadcrumbs":2,"title":0},"688":{"body":12,"breadcrumbs":2,"title":0},"689":{"body":20,"breadcrumbs":3,"title":1},"69":{"body":2,"breadcrumbs":1,"title":0},"690":{"body":178,"breadcrumbs":4,"title":2},"691":{"body":214,"breadcrumbs":2,"title":0},"692":{"body":1,"breadcrumbs":2,"title":1},"693":{"body":3,"breadcrumbs":1,"title":0},"694":{"body":36,"breadcrumbs":1,"title":0},"695":{"body":21,"breadcrumbs":1,"title":0},"696":{"body":6,"breadcrumbs":1,"title":0},"697":{"body":93,"breadcrumbs":5,"title":2},"698":{"body":60,"breadcrumbs":3,"title":0},"699":{"body":5,"breadcrumbs":5,"title":2},"7":{"body":7,"breadcrumbs":1,"title":0},"70":{"body":0,"breadcrumbs":1,"title":0},"700":{"body":5,"breadcrumbs":4,"title":0},"701":{"body":57,"breadcrumbs":5,"title":1},"702":{"body":0,"breadcrumbs":5,"title":2},"703":{"body":0,"breadcrumbs":6,"title":2},"704":{"body":0,"breadcrumbs":3,"title":1},"705":{"body":13,"breadcrumbs":0,"title":0},"706":{"body":3,"breadcrumbs":0,"title":0},"707":{"body":40,"breadcrumbs":0,"title":0},"708":{"body":167,"breadcrumbs":2,"title":2},"709":{"body":69,"breadcrumbs":0,"title":0},"71":{"body":34,"breadcrumbs":2,"title":1},"710":{"body":158,"breadcrumbs":1,"title":1},"711":{"body":23,"breadcrumbs":2,"title":2},"712":{"body":16,"breadcrumbs":0,"title":0},"713":{"body":9,"breadcrumbs":0,"title":0},"714":{"body":86,"breadcrumbs":1,"title":1},"715":{"body":398,"breadcrumbs":0,"title":0},"716":{"body":44,"breadcrumbs":2,"title":2},"717":{"body":53,"breadcrumbs":0,"title":0},"718":{"body":1,"breadcrumbs":0,"title":0},"719":{"body":75,"breadcrumbs":0,"title":0},"72":{"body":14,"breadcrumbs":2,"title":1},"720":{"body":218,"breadcrumbs":0,"title":0},"721":{"body":8,"breadcrumbs":0,"title":0},"722":{"body":2,"breadcrumbs":2,"title":1},"723":{"body":7,"breadcrumbs":1,"title":0},"724":{"body":52,"breadcrumbs":2,"title":1},"725":{"body":13,"breadcrumbs":2,"title":1},"726":{"body":61,"breadcrumbs":2,"title":1},"727":{"body":43,"breadcrumbs":2,"title":1},"728":{"body":7,"breadcrumbs":4,"title":2},"729":{"body":51,"breadcrumbs":4,"title":2},"73":{"body":12,"breadcrumbs":1,"title":0},"730":{"body":207,"breadcrumbs":3,"title":1},"731":{"body":71,"breadcrumbs":3,"title":1},"732":{"body":106,"breadcrumbs":4,"title":2},"733":{"body":174,"breadcrumbs":2,"title":0},"734":{"body":5,"breadcrumbs":2,"title":1},"735":{"body":261,"breadcrumbs":2,"title":1},"736":{"body":124,"breadcrumbs":2,"title":1},"737":{"body":7,"breadcrumbs":2,"title":1},"738":{"body":89,"breadcrumbs":1,"title":0},"739":{"body":3,"breadcrumbs":1,"title":0},"74":{"body":2,"breadcrumbs":1,"title":0},"740":{"body":13,"breadcrumbs":3,"title":1},"741":{"body":11,"breadcrumbs":3,"title":1},"742":{"body":20,"breadcrumbs":3,"title":1},"743":{"body":106,"breadcrumbs":1,"title":0},"744":{"body":56,"breadcrumbs":3,"title":1},"745":{"body":8,"breadcrumbs":7,"title":3},"746":{"body":16,"breadcrumbs":5,"title":1},"747":{"body":34,"breadcrumbs":5,"title":1},"748":{"body":38,"breadcrumbs":5,"title":1},"749":{"body":16,"breadcrumbs":4,"title":0},"75":{"body":17,"breadcrumbs":1,"title":0},"750":{"body":40,"breadcrumbs":3,"title":1},"751":{"body":63,"breadcrumbs":3,"title":1},"752":{"body":3,"breadcrumbs":3,"title":1},"753":{"body":13,"breadcrumbs":4,"title":2},"754":{"body":12,"breadcrumbs":2,"title":0},"755":{"body":31,"breadcrumbs":2,"title":0},"756":{"body":15,"breadcrumbs":5,"title":3},"757":{"body":8,"breadcrumbs":2,"title":0},"758":{"body":18,"breadcrumbs":6,"title":4},"759":{"body":8,"breadcrumbs":3,"title":1},"76":{"body":48,"breadcrumbs":1,"title":0},"760":{"body":89,"breadcrumbs":3,"title":1},"761":{"body":11,"breadcrumbs":2,"title":0},"762":{"body":14,"breadcrumbs":2,"title":0},"763":{"body":1,"breadcrumbs":1,"title":0},"764":{"body":2,"breadcrumbs":1,"title":0},"765":{"body":90,"breadcrumbs":2,"title":1},"766":{"body":110,"breadcrumbs":1,"title":0},"767":{"body":25,"breadcrumbs":1,"title":0},"768":{"body":67,"breadcrumbs":1,"title":0},"769":{"body":18,"breadcrumbs":2,"title":1},"77":{"body":47,"breadcrumbs":1,"title":0},"770":{"body":27,"breadcrumbs":3,"title":2},"771":{"body":14,"breadcrumbs":3,"title":2},"772":{"body":13,"breadcrumbs":2,"title":1},"773":{"body":69,"breadcrumbs":2,"title":1},"774":{"body":6,"breadcrumbs":1,"title":0},"775":{"body":126,"breadcrumbs":2,"title":1},"776":{"body":84,"breadcrumbs":1,"title":0},"777":{"body":46,"breadcrumbs":1,"title":0},"778":{"body":36,"breadcrumbs":2,"title":1},"779":{"body":14,"breadcrumbs":2,"title":1},"78":{"body":2,"breadcrumbs":1,"title":0},"780":{"body":14,"breadcrumbs":2,"title":1},"781":{"body":93,"breadcrumbs":3,"title":1},"782":{"body":137,"breadcrumbs":3,"title":1},"783":{"body":8,"breadcrumbs":3,"title":1},"784":{"body":198,"breadcrumbs":3,"title":1},"785":{"body":21,"breadcrumbs":3,"title":1},"786":{"body":5,"breadcrumbs":3,"title":1},"787":{"body":1,"breadcrumbs":3,"title":1},"788":{"body":11,"breadcrumbs":5,"title":2},"789":{"body":79,"breadcrumbs":3,"title":0},"79":{"body":15,"breadcrumbs":1,"title":0},"790":{"body":95,"breadcrumbs":3,"title":0},"791":{"body":25,"breadcrumbs":3,"title":0},"792":{"body":4,"breadcrumbs":3,"title":1},"793":{"body":54,"breadcrumbs":2,"title":0},"794":{"body":9,"breadcrumbs":2,"title":0},"795":{"body":36,"breadcrumbs":3,"title":1},"796":{"body":9,"breadcrumbs":2,"title":0},"797":{"body":24,"breadcrumbs":3,"title":1},"798":{"body":15,"breadcrumbs":3,"title":1},"799":{"body":2,"breadcrumbs":3,"title":1},"8":{"body":14,"breadcrumbs":1,"title":0},"80":{"body":5,"breadcrumbs":1,"title":0},"800":{"body":70,"breadcrumbs":3,"title":1},"801":{"body":28,"breadcrumbs":4,"title":2},"802":{"body":44,"breadcrumbs":2,"title":0},"803":{"body":73,"breadcrumbs":3,"title":1},"804":{"body":18,"breadcrumbs":3,"title":1},"805":{"body":132,"breadcrumbs":3,"title":1},"806":{"body":28,"breadcrumbs":4,"title":2},"807":{"body":5,"breadcrumbs":2,"title":0},"808":{"body":3,"breadcrumbs":4,"title":2},"809":{"body":13,"breadcrumbs":3,"title":1},"81":{"body":90,"breadcrumbs":1,"title":0},"810":{"body":17,"breadcrumbs":3,"title":1},"811":{"body":0,"breadcrumbs":4,"title":1},"812":{"body":14,"breadcrumbs":3,"title":0},"813":{"body":8,"breadcrumbs":3,"title":0},"814":{"body":19,"breadcrumbs":4,"title":1},"815":{"body":8,"breadcrumbs":4,"title":1},"816":{"body":6,"breadcrumbs":4,"title":1},"817":{"body":6,"breadcrumbs":3,"title":0},"818":{"body":20,"breadcrumbs":5,"title":2},"819":{"body":8,"breadcrumbs":4,"title":1},"82":{"body":111,"breadcrumbs":1,"title":0},"820":{"body":5,"breadcrumbs":3,"title":1},"821":{"body":58,"breadcrumbs":3,"title":1},"822":{"body":31,"breadcrumbs":3,"title":1},"823":{"body":30,"breadcrumbs":3,"title":1},"824":{"body":145,"breadcrumbs":3,"title":1},"825":{"body":112,"breadcrumbs":3,"title":1},"826":{"body":36,"breadcrumbs":3,"title":1},"827":{"body":5,"breadcrumbs":5,"title":2},"828":{"body":37,"breadcrumbs":3,"title":0},"829":{"body":357,"breadcrumbs":3,"title":0},"83":{"body":2,"breadcrumbs":1,"title":0},"830":{"body":14,"breadcrumbs":3,"title":0},"831":{"body":5,"breadcrumbs":3,"title":1},"832":{"body":16,"breadcrumbs":2,"title":0},"833":{"body":12,"breadcrumbs":2,"title":0},"834":{"body":39,"breadcrumbs":2,"title":0},"835":{"body":2,"breadcrumbs":2,"title":0},"836":{"body":5,"breadcrumbs":2,"title":0},"837":{"body":63,"breadcrumbs":3,"title":1},"838":{"body":5,"breadcrumbs":4,"title":2},"839":{"body":25,"breadcrumbs":3,"title":1},"84":{"body":11,"breadcrumbs":1,"title":0},"840":{"body":99,"breadcrumbs":2,"title":0},"841":{"body":15,"breadcrumbs":2,"title":0},"842":{"body":19,"breadcrumbs":3,"title":1},"843":{"body":43,"breadcrumbs":2,"title":0},"844":{"body":16,"breadcrumbs":2,"title":0},"845":{"body":61,"breadcrumbs":2,"title":0},"846":{"body":125,"breadcrumbs":2,"title":0},"847":{"body":94,"breadcrumbs":2,"title":0},"848":{"body":56,"breadcrumbs":3,"title":1},"849":{"body":70,"breadcrumbs":2,"title":0},"85":{"body":4,"breadcrumbs":1,"title":0},"850":{"body":3,"breadcrumbs":1,"title":1},"851":{"body":27,"breadcrumbs":3,"title":1},"852":{"body":23,"breadcrumbs":2,"title":0},"853":{"body":47,"breadcrumbs":2,"title":0},"854":{"body":40,"breadcrumbs":3,"title":1},"855":{"body":2,"breadcrumbs":0,"title":0},"856":{"body":1,"breadcrumbs":0,"title":0},"857":{"body":24,"breadcrumbs":0,"title":0},"858":{"body":4,"breadcrumbs":0,"title":0},"859":{"body":5,"breadcrumbs":0,"title":0},"86":{"body":19,"breadcrumbs":1,"title":0},"860":{"body":2,"breadcrumbs":0,"title":0},"861":{"body":13,"breadcrumbs":2,"title":1},"862":{"body":22,"breadcrumbs":2,"title":1},"863":{"body":86,"breadcrumbs":1,"title":0},"864":{"body":3,"breadcrumbs":1,"title":0},"865":{"body":1,"breadcrumbs":1,"title":0},"866":{"body":22,"breadcrumbs":2,"title":1},"867":{"body":131,"breadcrumbs":1,"title":0},"868":{"body":45,"breadcrumbs":1,"title":0},"869":{"body":1,"breadcrumbs":1,"title":0},"87":{"body":7,"breadcrumbs":2,"title":1},"870":{"body":14,"breadcrumbs":2,"title":1},"871":{"body":53,"breadcrumbs":1,"title":0},"872":{"body":4,"breadcrumbs":1,"title":0},"873":{"body":3,"breadcrumbs":1,"title":0},"874":{"body":19,"breadcrumbs":2,"title":1},"875":{"body":51,"breadcrumbs":2,"title":1},"876":{"body":12,"breadcrumbs":2,"title":1},"877":{"body":0,"breadcrumbs":1,"title":0},"878":{"body":22,"breadcrumbs":2,"title":1},"879":{"body":29,"breadcrumbs":2,"title":1},"88":{"body":39,"breadcrumbs":1,"title":0},"880":{"body":10,"breadcrumbs":2,"title":1},"881":{"body":49,"breadcrumbs":3,"title":2},"882":{"body":85,"breadcrumbs":2,"title":1},"883":{"body":0,"breadcrumbs":1,"title":0},"884":{"body":58,"breadcrumbs":1,"title":0},"885":{"body":229,"breadcrumbs":1,"title":0},"886":{"body":3,"breadcrumbs":1,"title":0},"887":{"body":72,"breadcrumbs":1,"title":0},"888":{"body":7,"breadcrumbs":1,"title":0},"889":{"body":19,"breadcrumbs":3,"title":2},"89":{"body":178,"breadcrumbs":1,"title":0},"890":{"body":65,"breadcrumbs":2,"title":1},"891":{"body":49,"breadcrumbs":1,"title":0},"892":{"body":148,"breadcrumbs":1,"title":0},"893":{"body":150,"breadcrumbs":3,"title":2},"894":{"body":12,"breadcrumbs":2,"title":1},"895":{"body":131,"breadcrumbs":2,"title":1},"896":{"body":67,"breadcrumbs":2,"title":1},"897":{"body":83,"breadcrumbs":2,"title":1},"898":{"body":103,"breadcrumbs":3,"title":2},"899":{"body":61,"breadcrumbs":1,"title":0},"9":{"body":29,"breadcrumbs":1,"title":0},"90":{"body":72,"breadcrumbs":1,"title":0},"900":{"body":2,"breadcrumbs":0,"title":0},"901":{"body":16,"breadcrumbs":0,"title":0},"902":{"body":5,"breadcrumbs":0,"title":0},"903":{"body":10,"breadcrumbs":0,"title":0},"904":{"body":4,"breadcrumbs":0,"title":0},"905":{"body":11,"breadcrumbs":0,"title":0},"906":{"body":14,"breadcrumbs":0,"title":0},"907":{"body":0,"breadcrumbs":0,"title":0},"908":{"body":1,"breadcrumbs":2,"title":1},"909":{"body":2,"breadcrumbs":1,"title":0},"91":{"body":2,"breadcrumbs":1,"title":0},"910":{"body":7,"breadcrumbs":1,"title":0},"911":{"body":0,"breadcrumbs":2,"title":1},"912":{"body":25,"breadcrumbs":2,"title":1},"913":{"body":17,"breadcrumbs":1,"title":0},"914":{"body":28,"breadcrumbs":2,"title":1},"915":{"body":20,"breadcrumbs":2,"title":1},"916":{"body":17,"breadcrumbs":1,"title":0},"917":{"body":34,"breadcrumbs":1,"title":0},"918":{"body":15,"breadcrumbs":1,"title":0},"919":{"body":12,"breadcrumbs":1,"title":0},"92":{"body":4,"breadcrumbs":1,"title":0},"920":{"body":8,"breadcrumbs":2,"title":1},"921":{"body":8,"breadcrumbs":1,"title":0},"922":{"body":35,"breadcrumbs":1,"title":0},"923":{"body":3,"breadcrumbs":2,"title":1},"924":{"body":0,"breadcrumbs":1,"title":0},"925":{"body":66,"breadcrumbs":1,"title":0},"926":{"body":4,"breadcrumbs":1,"title":0},"927":{"body":56,"breadcrumbs":4,"title":3},"928":{"body":4,"breadcrumbs":1,"title":0},"929":{"body":59,"breadcrumbs":5,"title":4},"93":{"body":43,"breadcrumbs":1,"title":0},"930":{"body":6,"breadcrumbs":1,"title":0},"931":{"body":34,"breadcrumbs":4,"title":3},"932":{"body":5,"breadcrumbs":1,"title":0},"933":{"body":6,"breadcrumbs":4,"title":3},"934":{"body":15,"breadcrumbs":1,"title":0},"935":{"body":19,"breadcrumbs":5,"title":4},"936":{"body":9,"breadcrumbs":4,"title":3},"937":{"body":6,"breadcrumbs":2,"title":1},"938":{"body":3,"breadcrumbs":2,"title":0},"939":{"body":3,"breadcrumbs":3,"title":1},"94":{"body":74,"breadcrumbs":1,"title":0},"940":{"body":1,"breadcrumbs":2,"title":0},"941":{"body":1,"breadcrumbs":3,"title":1},"942":{"body":1,"breadcrumbs":4,"title":2},"943":{"body":1,"breadcrumbs":3,"title":1},"944":{"body":2,"breadcrumbs":3,"title":1},"945":{"body":30,"breadcrumbs":2,"title":0},"946":{"body":1,"breadcrumbs":3,"title":1},"947":{"body":10,"breadcrumbs":0,"title":0},"948":{"body":67,"breadcrumbs":0,"title":0},"949":{"body":7,"breadcrumbs":0,"title":0},"95":{"body":176,"breadcrumbs":1,"title":0},"950":{"body":270,"breadcrumbs":1,"title":1},"951":{"body":1,"breadcrumbs":0,"title":0},"952":{"body":16,"breadcrumbs":1,"title":1},"953":{"body":148,"breadcrumbs":1,"title":1},"954":{"body":173,"breadcrumbs":1,"title":1},"955":{"body":1,"breadcrumbs":0,"title":0},"956":{"body":85,"breadcrumbs":0,"title":0},"957":{"body":136,"breadcrumbs":1,"title":1},"958":{"body":127,"breadcrumbs":0,"title":0},"959":{"body":11,"breadcrumbs":0,"title":0},"96":{"body":77,"breadcrumbs":3,"title":2},"960":{"body":6,"breadcrumbs":0,"title":0},"961":{"body":193,"breadcrumbs":1,"title":1},"962":{"body":104,"breadcrumbs":0,"title":0},"963":{"body":195,"breadcrumbs":2,"title":1},"964":{"body":36,"breadcrumbs":2,"title":0},"965":{"body":79,"breadcrumbs":3,"title":1},"966":{"body":609,"breadcrumbs":3,"title":1},"967":{"body":218,"breadcrumbs":2,"title":1},"968":{"body":300,"breadcrumbs":1,"title":0},"969":{"body":13,"breadcrumbs":0,"title":0},"97":{"body":0,"breadcrumbs":1,"title":0},"970":{"body":1,"breadcrumbs":0,"title":0},"971":{"body":60,"breadcrumbs":0,"title":0},"972":{"body":273,"breadcrumbs":0,"title":0},"973":{"body":0,"breadcrumbs":2,"title":1},"974":{"body":117,"breadcrumbs":2,"title":1},"975":{"body":8,"breadcrumbs":2,"title":1},"976":{"body":151,"breadcrumbs":1,"title":0},"977":{"body":17,"breadcrumbs":0,"title":0},"978":{"body":4,"breadcrumbs":0,"title":0},"979":{"body":24,"breadcrumbs":0,"title":0},"98":{"body":2,"breadcrumbs":1,"title":0},"980":{"body":28,"breadcrumbs":0,"title":0},"981":{"body":96,"breadcrumbs":1,"title":1},"982":{"body":228,"breadcrumbs":1,"title":1},"983":{"body":14,"breadcrumbs":1,"title":1},"984":{"body":197,"breadcrumbs":2,"title":1},"985":{"body":241,"breadcrumbs":0,"title":0},"986":{"body":0,"breadcrumbs":0,"title":0},"987":{"body":113,"breadcrumbs":1,"title":1},"988":{"body":281,"breadcrumbs":1,"title":1},"989":{"body":338,"breadcrumbs":0,"title":0},"99":{"body":66,"breadcrumbs":1,"title":0},"990":{"body":23,"breadcrumbs":2,"title":1},"991":{"body":580,"breadcrumbs":1,"title":0},"992":{"body":369,"breadcrumbs":1,"title":0},"993":{"body":410,"breadcrumbs":3,"title":1},"994":{"body":4,"breadcrumbs":3,"title":2},"995":{"body":0,"breadcrumbs":3,"title":2},"996":{"body":87,"breadcrumbs":1,"title":0},"997":{"body":39,"breadcrumbs":1,"title":0},"998":{"body":6,"breadcrumbs":1,"title":0},"999":{"body":13,"breadcrumbs":1,"title":0}},"docs":{"0":{"body":"Rust 语言真的好:连续七年成为全世界最受欢迎的语言、没有 GC 也无需手动内存管理、性能比肩 C++/C 还能直接调用它们的代码、安全性极高 - 总有公司说使用 Rust 后以前的大部分 bug 都将自动消失、全世界最好的包管理工具 Cargo 等等。但... 有人说: \"Rust 太难了,学了也没用\"。 对于后面一句话我们持保留意见,如果以找工作为标准,那国内环境确实还不好,但如果你想成为更优秀的程序员或者是玩转开源,那 Rust 还真是不错的选择,具体原因见 下一章 。 至于 Rust 难学,那正是本书要解决的问题,如果看完后,你觉得没有学会 Rust,可以找我们退款,哦抱歉,这是开源书,那就退 🌟 吧 :) 如果看到这里,大家觉得这本书的介绍并没有吸引到你,不要立即放弃,强烈建议读一下 进入 Rust 编程世界 ,那里会有不一样的精彩。 对于学习编程而言,读一篇文章不如做几道练习题,此话虽然夸张,但是也不无道理。既然如此,即读书又做练习题,效果会不会更好?再加上练习题是书本的配套呢? :P Rust 语言实战 , Rust 语言圣经配套习题,支持中英双语,可以在右上角切换","breadcrumbs":"关于本书 » 配套练习题","id":"0","title":"配套练习题"},"1":{"body":"截至目前,Rust 语言圣经已写了 170 余章,110 余万字,历经 1000 多个小时,每一个章节都是手动写就,没有任何机翻和质量上的妥协( 相信深入阅读过的读者都能体会到这一点 )。 曾经有读者问过 \"这么好的书为何要开源,而不是出版?\",原因很简单: 只有完全开源才能完美地呈现出我想要的教学效果 。 总之,Rust 要在国内真正发展起来,必须得有一些追逐梦想的人在做着不计付出的事情,而我希望自己能贡献一份微薄之力。 但是要说完全无欲无求,那也是不可能的,看到项目多了一颗 🌟,那感觉...棒极了,因为它代表了读者的认可和称赞。 你们用指尖绘制的星空,那里繁星点点,每一颗都在鼓励着怀揣着开源梦想的程序员披荆斩棘、不断前行,不夸张的说,没有你们,开源世界就没有星光,自然也就不会有今天的开源盛世。 因此, 我恳请大家,如果觉得书还可以,就在你的指尖星空绘制一颗新的 🌟,指引我们继续砥砺前行 。这个人世间,因善意而美好。 最后,能通过开源在茫茫人海中与大家相识,这感觉真好 :D","breadcrumbs":"关于本书 » 创作感悟","id":"1","title":"创作感悟"},"10":{"body":"由于篇幅有限,我们这里不会讲述详细的对比,就是简单介绍下 Rust 的优势,并不是说 Rust 优于这些语言,大家轻喷:) Go Rust 语言表达能力更强,性能更高。同时线程安全方面 Rust 也更强,不容易写出错误的代码。包管理 Rust 也更好,Go 虽然在 1.10 版本后提供了包管理,但是目前还比不上 Rust 。 C++ Rust 与 C++ 的性能旗鼓相当,但是在安全性方面 Rust 会更优,特别是使用第三方库时,Rust 的严格要求会让三方库的质量明显高很多。 语言本身的学习,Rust 的前中期学习曲线会更陡峭,但是在实际的项目开发过程中,C++ 会更难,代码也更难以维护。 Java 除了极少数纯粹的数字计算性能,Rust 的性能全面领先于 Java 。同时 Rust 占用内存小的多,因此实现同等规模的服务,Rust 所需的硬件成本会显著降低。 Python 性能自然是 Rust 完胜,同时 Rust 对运行环境要求较低,这两点差不多就足够抉择了。不过 Python 和 Rust 的彼此适用面其实也不太冲突。","breadcrumbs":"进入 Rust 编程世界 » 相比其他语言 Rust 的优势","id":"10","title":"相比其他语言 Rust 的优势"},"100":{"body":"在其他语言中,字符串往往是送分题,因为实在是太简单了,例如 \"hello, world\" 就是字符串章节的几乎全部内容了,但是如果你带着同样的想法来学 Rust,我保证,绝对会栽跟头, 因此这一章大家一定要重视,仔细阅读,这里有很多其它 Rust 书籍中没有的内容 。 首先来看段很简单的代码: fn main() { let my_name = \"Pascal\"; greet(my_name);\n} fn greet(name: String) { println!(\"Hello, {}!\", name);\n} greet 函数接受一个字符串类型的 name 参数,然后打印到终端控制台中,非常好理解,你们猜猜,这段代码能否通过编译? error[E0308]: mismatched types --> src/main.rs:3:11 |\n3 | greet(my_name); | ^^^^^^^ | | | expected struct `std::string::String`, found `&str` | help: try using a conversion method: `my_name.to_string()` error: aborting due to previous error Bingo,果然报错了,编译器提示 greet 函数需要一个 String 类型的字符串,却传入了一个 &str 类型的字符串,相信读者心中现在一定有几头草泥马呼啸而过,怎么字符串也能整出这么多花活? 在讲解字符串之前,先来看看什么是切片?","breadcrumbs":"Rust 基础入门 » 复合类型 » 字符串与切片 » 字符串","id":"100","title":"字符串"},"1000":{"body":"在上一章节中,借用检查器似乎不喜欢以下代码: let mut data = 10;\nlet ref1 = &mut data;\nlet ref2 = &mut *ref1; *ref1 += 1;\n*ref2 += 2; println!(\"{}\", data); 它违背了再借用的原则,大家可以用借用栈的分析方式去验证下上一章节所学的知识。 下面来看看,如果使用裸指针会怎么样: unsafe { let mut data = 10; let ref1 = &mut data; let ptr2 = ref1 as *mut _; *ref1 += 1; *ptr2 += 2; println!(\"{}\", data);\n} $ cargo run Compiling miri-sandbox v0.1.0 Finished dev [unoptimized + debuginfo] target(s) in 0.71s Running `target\\debug\\miri-sandbox.exe`\n13 嗯,编译器看起来很满意:不仅获取了预期的结果,还没有任何警告。那么再来征求下 Miri 的意见: MIRIFLAGS=\"-Zmiri-tag-raw-pointers\" cargo +nightly-2022-01-21 miri run Finished dev [unoptimized + debuginfo] target(s) in 0.00s Running cargo-miri.exe target\\miri error: Undefined Behavior: no item granting read access to tag at alloc748 found in borrow stack. --> src\\main.rs:9:9 |\n9 | *ptr2 += 2; | ^^^^^^^^^^ no item granting read access to tag | at alloc748 found in borrow stack. | = help: this indicates a potential bug in the program: it performed an invalid operation, but the rules it violated are still experimental 喔,果然出问题了。下面再来试试更复杂的 &mut -> *mut -> &mut -> *mut : unsafe { let mut data = 10; let ref1 = &mut data; let ptr2 = ref1 as *mut _; let ref3 = &mut *ptr2; let ptr4 = ref3 as *mut _; // 首先访问第一个裸指针 *ptr2 += 2; // 接着按照借用栈的顺序来访问 *ptr4 += 4; *ref3 += 3; *ptr2 += 2; *ref1 += 1; println!(\"{}\", data);\n} $ cargo run 22 MIRIFLAGS=\"-Zmiri-tag-raw-pointers\" cargo +nightly-2022-01-21 miri run error: Undefined Behavior: no item granting read access to tag <1621> at alloc748 found in borrow stack. --> src\\main.rs:13:5 |\n13 | *ptr4 += 4; | ^^^^^^^^^^ no item granting read access to tag <1621> | at alloc748 found in borrow stack. | 不错,可以看出 miri 有能力分辨两个裸指针的使用限制:当使用第二个时,需要先让之前的失效。 再来移除乱入的那一行,让借用栈可以真正顺利的工作: unsafe { let mut data = 10; let ref1 = &mut data; let ptr2 = ref1 as *mut _; let ref3 = &mut *ptr2; let ptr4 = ref3 as *mut _; // Access things in \"borrow stack\" order *ptr4 += 4; *ref3 += 3; *ptr2 += 2; *ref1 += 1; println!(\"{}\", data);\n} $ cargo run 20 MIRIFLAGS=\"-Zmiri-tag-raw-pointers\" cargo +nightly-2022-01-21 miri run\n20 我现在可以负责任的说:在座的各位,都是...可以获取编程语言内存模型设计博士学位的存在,编译器?那是什么东东!简单的很。 旁白:那个..关于博士的一切,请不要当真,但是我依然为你们骄傲","breadcrumbs":"手把手带你实现链表 » 不错的 unsafe 队列 » 测试栈借用 » 基本借用","id":"1000","title":"基本借用"},"1001":{"body":"下面来干一票大的:使用指针偏移来搞乱一个数组。 unsafe { let mut data = [0; 10]; let ref1_at_0 = &mut data[0]; // 获取第 1 个元素的引用 let ptr2_at_0 = ref1_at_0 as *mut i32; // 裸指针 ptr 指向第 1 个元素 let ptr3_at_1 = ptr2_at_0.add(1); // 对裸指针进行运算,指向第 2 个元素 *ptr3_at_1 += 3; *ptr2_at_0 += 2; *ref1_at_0 += 1; // Should be [3, 3, 0, ...] println!(\"{:?}\", &data[..]);\n} $ cargo run [3, 3, 0, 0, 0, 0, 0, 0, 0, 0] MIRIFLAGS=\"-Zmiri-tag-raw-pointers\" cargo +nightly-2022-01-21 miri run error: Undefined Behavior: no item granting read access to tag <1619> at alloc748+0x4 found in borrow stack. --> src\\main.rs:8:5 |\n8 | *ptr3_at_1 += 3; | ^^^^^^^^^^^^^^^ no item granting read access to tag <1619> | at alloc748+0x4 found in borrow stack. 咦?我们命名按照借用栈的方式来完美使用了,为何 miri 还是提示了 UB 风险?难道是因为 ptr -> ptr 的过程中发生了什么奇怪的事情?如果我们只是拷贝指针,让它们都指向同一个位置呢? unsafe { let mut data = [0; 10]; let ref1_at_0 = &mut data[0]; let ptr2_at_0 = ref1_at_0 as *mut i32; let ptr3_at_0 = ptr2_at_0; *ptr3_at_0 += 3; *ptr2_at_0 += 2; *ref1_at_0 += 1; // Should be [6, 0, 0, ...] println!(\"{:?}\", &data[..]);\n} $ cargo run [6, 0, 0, 0, 0, 0, 0, 0, 0, 0] MIRIFLAGS=\"-Zmiri-tag-raw-pointers\" cargo +nightly-2022-01-21 miri run\n[6, 0, 0, 0, 0, 0, 0, 0, 0, 0] 果然,顺利通过,下面我们还是让它们指向同一个位置,但是来首名为混乱的 BGM: unsafe { let mut data = [0; 10]; let ref1_at_0 = &mut data[0]; // Reference to 0th element let ptr2_at_0 = ref1_at_0 as *mut i32; // Ptr to 0th element let ptr3_at_0 = ptr2_at_0; // Ptr to 0th element let ptr4_at_0 = ptr2_at_0.add(0); // Ptr to 0th element let ptr5_at_0 = ptr3_at_0.add(1).sub(1); // Ptr to 0th element *ptr3_at_0 += 3; *ptr2_at_0 += 2; *ptr4_at_0 += 4; *ptr5_at_0 += 5; *ptr3_at_0 += 3; *ptr2_at_0 += 2; *ref1_at_0 += 1; // Should be [20, 0, 0, ...] println!(\"{:?}\", &data[..]);\n} $ cargo run\n[20, 0, 0, 0, 0, 0, 0, 0, 0, 0] MIRIFLAGS=\"-Zmiri-tag-raw-pointers\" cargo +nightly-2022-01-21 miri run\n[20, 0, 0, 0, 0, 0, 0, 0, 0, 0] 可以看出,miri 对于这种裸指针派生是相当纵容的:当它们都共享同一个借用时(borrowing, 也可以用 miri 的称呼: tag)。 当代码足够简单时,编译器是有可能介入跟踪所有派生的裸指针,并尽可能去优化它们的。但是这套规则比引用的那套脆弱得多! 那么问题来了:真正的问题到底是什么? 对于部分数据结构,Rust 允许对其中的字段进行独立借用,例如一个结构体,它的多个字段可以被分开借用,来试试这里的数组可不可以。 unsafe { let mut data = [0; 10]; let ref1_at_0 = &mut data[0]; // Reference to 0th element let ref2_at_1 = &mut data[1]; // Reference to 1th element let ptr3_at_0 = ref1_at_0 as *mut i32; // Ptr to 0th element let ptr4_at_1 = ref2_at_1 as *mut i32; // Ptr to 1th element *ptr4_at_1 += 4; *ptr3_at_0 += 3; *ref2_at_1 += 2; *ref1_at_0 += 1; // Should be [3, 3, 0, ...] println!(\"{:?}\", &data[..]);\n} error[E0499]: cannot borrow `data[_]` as mutable more than once at a time --> src\\main.rs:5:21 |\n4 | let ref1_at_0 = &mut data[0]; // Reference to 0th element | ------------ first mutable borrow occurs here\n5 | let ref2_at_1 = &mut data[1]; // Reference to 1th element | ^^^^^^^^^^^^ second mutable borrow occurs here\n6 | let ptr3_at_0 = ref1_at_0 as *mut i32; // Ptr to 0th element | --------- first borrow later used here | = help: consider using `.split_at_mut(position)` or similar method to obtain two mutable non-overlapping sub-slices 显然..不行,Rust 不允许我们对数组的不同元素进行单独的借用,注意到提示了吗?可以使用 .split_at_mut(position) 来将一个数组分成多个部分: unsafe { let mut data = [0; 10]; let slice1 = &mut data[..]; let (slice2_at_0, slice3_at_1) = slice1.split_at_mut(1); let ref4_at_0 = &mut slice2_at_0[0]; // Reference to 0th element let ref5_at_1 = &mut slice3_at_1[0]; // Reference to 1th element let ptr6_at_0 = ref4_at_0 as *mut i32; // Ptr to 0th element let ptr7_at_1 = ref5_at_1 as *mut i32; // Ptr to 1th element *ptr7_at_1 += 7; *ptr6_at_0 += 6; *ref5_at_1 += 5; *ref4_at_0 += 4; // Should be [10, 12, 0, ...] println!(\"{:?}\", &data[..]);\n} $ cargo run\n[10, 12, 0, 0, 0, 0, 0, 0, 0, 0] MIRIFLAGS=\"-Zmiri-tag-raw-pointers\" cargo +nightly-2022-01-21 miri run\n[10, 12, 0, 0, 0, 0, 0, 0, 0, 0] 将数组切分成两个部分后,代码就成功了,如果我们将一个切片转换成指针呢?那指针是否还拥有访问整个切片的权限? unsafe { let mut data = [0; 10]; let slice1_all = &mut data[..]; // Slice for the entire array let ptr2_all = slice1_all.as_mut_ptr(); // Pointer for the entire array let ptr3_at_0 = ptr2_all; // Pointer to 0th elem (the same) let ptr4_at_1 = ptr2_all.add(1); // Pointer to 1th elem let ref5_at_0 = &mut *ptr3_at_0; // Reference to 0th elem let ref6_at_1 = &mut *ptr4_at_1; // Reference to 1th elem *ref6_at_1 += 6; *ref5_at_0 += 5; *ptr4_at_1 += 4; *ptr3_at_0 += 3; // 在循环中修改所有元素( 仅仅为了有趣 ) // (可以使用任何裸指针,它们共享同一个借用!) for idx in 0..10 { *ptr2_all.add(idx) += idx; } // 同样为了有趣,再实现下安全版本的循环 for (idx, elem_ref) in slice1_all.iter_mut().enumerate() { *elem_ref += idx; } // Should be [8, 12, 4, 6, 8, 10, 12, 14, 16, 18] println!(\"{:?}\", &data[..]);\n} $ cargo run\n[8, 12, 4, 6, 8, 10, 12, 14, 16, 18] MIRIFLAGS=\"-Zmiri-tag-raw-pointers\" cargo +nightly-2022-01-21 miri run\n[8, 12, 4, 6, 8, 10, 12, 14, 16, 18]","breadcrumbs":"手把手带你实现链表 » 不错的 unsafe 队列 » 测试栈借用 » 测试数组","id":"1001","title":"测试数组"},"1002":{"body":"在之前的例子中,我们使用的都是可变引用,而 Rust 中还有不可变引用。那么它将如何工作呢? 我们已经见过裸指针可以被简单的拷贝只要它们共享同一个借用,那不可变引用是不是也可以这么做? 注意,下面的 println 会自动对待打印的目标值进行 ref/deref 等操作,因此为了保证测试的正确性,我们将其放入一个函数中。 fn opaque_read(val: &i32) { println!(\"{}\", val);\n} unsafe { let mut data = 10; let mref1 = &mut data; let sref2 = &mref1; let sref3 = sref2; let sref4 = &*sref2; // Random hash of shared reference reads opaque_read(sref3); opaque_read(sref2); opaque_read(sref4); opaque_read(sref2); opaque_read(sref3); *mref1 += 1; opaque_read(&data);\n} $ cargo run warning: unnecessary `unsafe` block --> src\\main.rs:6:1 |\n6 | unsafe { | ^^^^^^ unnecessary `unsafe` block | = note: `#[warn(unused_unsafe)]` on by default warning: `miri-sandbox` (bin \"miri-sandbox\") generated 1 warning 10\n10\n10\n10\n10\n11 虽然这里没有使用裸指针,但是可以看到对于不可变引用而言,上面的使用方式不存在任何问题。下面来增加一些裸指针: fn opaque_read(val: &i32) { println!(\"{}\", val);\n} unsafe { let mut data = 10; let mref1 = &mut data; let ptr2 = mref1 as *mut i32; let sref3 = &*mref1; let ptr4 = sref3 as *mut i32; *ptr4 += 4; opaque_read(sref3); *ptr2 += 2; *mref1 += 1; opaque_read(&data);\n} $ cargo run error[E0606]: casting `&i32` as `*mut i32` is invalid --> src/main.rs:11:20 |\n11 | let ptr4 = sref3 as *mut i32; | ^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^ 可以看出,我们无法将一个不可变的引用转换成可变的裸指针,只能曲线救国了: let ptr4 = sref3 as *const i32 as *mut i32; 如上,先将不可变引用转换成不可变的裸指针,然后再转换成可变的裸指针。 $ cargo run 14\n17 编译器又一次满意了,再来看看 miri : MIRIFLAGS=\"-Zmiri-tag-raw-pointers\" cargo +nightly-2022-01-21 miri run error: Undefined Behavior: no item granting write access to tag <1621> at alloc742 found in borrow stack. --> src\\main.rs:13:5 |\n13 | *ptr4 += 4; | ^^^^^^^^^^ no item granting write access to tag <1621> | at alloc742 found in borrow stack. 果然,miri 提示了,原因是当我们使用不可变引用时,就相当于承诺不会去修改其中的值,那 miri 发现了这种修改行为,自然会给予相应的提示。 对此,可以用一句话来简单总结: 在借用栈中,一个不可变引用,它上面的所有引用( 在它之后被推入借用栈的引用 )都只能拥有只读的权限。 但是我们可以这样做: fn opaque_read(val: &i32) { println!(\"{}\", val);\n} unsafe { let mut data = 10; let mref1 = &mut data; let ptr2 = mref1 as *mut i32; let sref3 = &*mref1; let ptr4 = sref3 as *const i32 as *mut i32; opaque_read(&*ptr4); opaque_read(sref3); *ptr2 += 2; *mref1 += 1; opaque_read(&data);\n} 可以看到,我们其实可以创建一个可变的裸指针,只要不去使用写操作,而是只使用读操作。 $ cargo run 10\n10\n13 MIRIFLAGS=\"-Zmiri-tag-raw-pointers\" cargo +nightly-2022-01-21 miri run\n10\n10\n13 再来检查下不可变的引用是否可以像平时一样正常弹出: fn opaque_read(val: &i32) { println!(\"{}\", val);\n} unsafe { let mut data = 10; let mref1 = &mut data; let ptr2 = mref1 as *mut i32; let sref3 = &*mref1; *ptr2 += 2; opaque_read(sref3); // Read in the wrong order? *mref1 += 1; opaque_read(&data);\n} $ cargo run 12\n13 MIRIFLAGS=\"-Zmiri-tag-raw-pointers\" cargo +nightly-2022-01-21 miri run error: Undefined Behavior: trying to reborrow for SharedReadOnly at alloc742, but parent tag <1620> does not have an appropriate item in the borrow stack --> src\\main.rs:13:17 |\n13 | opaque_read(sref3); // Read in the wrong order? | ^^^^^ trying to reborrow for SharedReadOnly | at alloc742, but parent tag <1620> | does not have an appropriate item | in the borrow stack | 细心的同学可能会发现,我们这次获得了一个相当具体的 miri 提示,而不是之前的某个 tag 。真是令人感动...毕竟这种错误信息会更有帮助。","breadcrumbs":"手把手带你实现链表 » 不错的 unsafe 队列 » 测试栈借用 » 测试不可变引用","id":"1002","title":"测试不可变引用"},"1003":{"body":"还记得之前我们试图用 RefCell + Rc 去实现的那个糟糕的链表吗?这两个组合在一起就可以实现内部可变性。与 RefCell 类似的还有 Cell : use std::cell::Cell; unsafe { let mut data = Cell::new(10); let mref1 = &mut data; let ptr2 = mref1 as *mut Cell; let sref3 = &*mref1; sref3.set(sref3.get() + 3); (*ptr2).set((*ptr2).get() + 2); mref1.set(mref1.get() + 1); println!(\"{}\", data.get());\n} 地狱一般的代码,就等着 miri 来优化你吧。 $ cargo run 16 MIRIFLAGS=\"-Zmiri-tag-raw-pointers\" cargo +nightly-2022-01-21 miri run\n16 等等,竟然没有任何问题,我们需要深入调查下原因: pub struct Cell { value: UnsafeCell,\n} 以上是标准库中的 Cell 源码,可以看到里面有一个 UnsafeCell,通过名字都能猜到,这个数据结构相当的不安全,在 标准库 中有以下描述: Rust 中用于内部可变性的核心原语( primitive )。 如果你拥有一个引用 &T,那一般情况下, Rust编译器会基于 &T 指向不可变的数据这一事实来进行相关的优化。通过别名或者将 &T 强制转换成 &mut T 是一种 UB 行为。 而 UnsafeCell 移除了 &T 的不可变保证:一个不可变引用 &UnsafeCell 指向一个可以改变的数据。,这就是内部可变性。 感觉像是魔法,那下面就用该魔法让 miri happy 下: use std::cell::UnsafeCell; fn opaque_read(val: &i32) { println!(\"{}\", val);\n} unsafe { let mut data = UnsafeCell::new(10); let mref1 = &mut data; // Mutable ref to the *outside* let ptr2 = mref1.get(); // Get a raw pointer to the insides let sref3 = &*mref1; // Get a shared ref to the *outside* *ptr2 += 2; // Mutate with the raw pointer opaque_read(&*sref3.get()); // Read from the shared ref *sref3.get() += 3; // Write through the shared ref *mref1.get() += 1; // Mutate with the mutable ref println!(\"{}\", *data.get());\n} $ cargo run 12\n16 MIRIFLAGS=\"-Zmiri-tag-raw-pointers\" cargo +nightly-2022-01-21 miri run\n12\n16 这段代码非常成功!但是等等..这里的代码顺序有问题:我们首先获取了内部的裸指针 ptr2,然后获取了一个不可变引用 sref3,接着我们使用了裸指针,然后是 sref3,这不就是标准的借用栈错误典范吗?既然如此,为何 miri 没有给出提示? 现在有两个解释: Miri 并不完美,它依然会有所遗漏,也会误判 我们的简化模型貌似过于简化了 大家选择哪个?..我不管,反正我选择第二个。不过,虽然我们的借用栈过于简单,但是依然是亲孩子嘛,最后再基于它来实现一个真正正确的版本: use std::cell::UnsafeCell; fn opaque_read(val: &i32) { println!(\"{}\", val);\n} unsafe { let mut data = UnsafeCell::new(10); let mref1 = &mut data; // These two are swapped so the borrows are *definitely* totally stacked let sref2 = &*mref1; // Derive the ptr from the shared ref to be super safe! let ptr3 = sref2.get(); *ptr3 += 3; opaque_read(&*sref2.get()); *sref2.get() += 2; *mref1.get() += 1; println!(\"{}\", *data.get());\n} $ cargo run 13\n16 MIRIFLAGS=\"-Zmiri-tag-raw-pointers\" cargo +nightly-2022-01-21 miri run\n13\n16","breadcrumbs":"手把手带你实现链表 » 不错的 unsafe 队列 » 测试栈借用 » 测试内部可变性","id":"1003","title":"测试内部可变性"},"1004":{"body":"大家还记得为何我们讲了这么长的两章借用栈吗?原因就在于 Box 和裸指针混合使用时出了问题。 Box 在某种程度上类似 &mut,因为对于它指向的内存区域,它拥有唯一的所有权。 unsafe { let mut data = Box::new(10); let ptr1 = (&mut *data) as *mut i32; *data += 10; *ptr1 += 1; // Should be 21 println!(\"{}\", data);\n} $ cargo run 21 MIRIFLAGS=\"-Zmiri-tag-raw-pointers\" cargo +nightly-2022-01-21 miri run error: Undefined Behavior: no item granting read access to tag <1707> at alloc763 found in borrow stack. --> src\\main.rs:7:5 |\n7 | *ptr1 += 1; | ^^^^^^^^^^ no item granting read access to tag <1707> | at alloc763 found in borrow stack. | 现在到现在为止,大家一眼就能看出来这种代码不符合借用栈的规则。当然, miri 也讨厌这一点,因此我们来改正下。 unsafe { let mut data = Box::new(10); let ptr1 = (&mut *data) as *mut i32; *ptr1 += 1; *data += 10; // Should be 21 println!(\"{}\", data);\n} $ cargo run 21 MIRIFLAGS=\"-Zmiri-tag-raw-pointers\" cargo +nightly-2022-01-21 miri run\n21 在经过这么长的旅程后,我们终于完成了借用栈的学习,兄弟们我已经累趴了,你们呢? 但是,话说回来,该如何使用 Box 来解决栈借用的问题?当然,我们可以像之前的测试例子一样写一些玩具代码,但是在实际链表中中,将 Box 存储在某个地方,然后长时间持有一个裸指针才是经常遇到的。 等等,你说链表?天呐,我都忘记了我们还在学习链表,那接下来,继续实现之前未完成的链表吧。","breadcrumbs":"手把手带你实现链表 » 不错的 unsafe 队列 » 测试栈借用 » 测试 Box","id":"1004","title":"测试 Box"},"1005":{"body":"TL;DR 在之前部分中,将安全的指针 & 、&mut 和 Box 跟不安全的裸指针 *mut 和 *const 混用是 UB 的根源之一,原因是安全指针会引入额外的约束,但是裸指针并不会遵守这些约束。 一个好消息,一个坏消息。坏消息是我们又要开始写链表了,悲剧 = , = 好消息呢是之前我们已经讨论过该如何设计了,之前做的工作基本都是正确的,除了混用安全指针和不安全指针的部分。","breadcrumbs":"手把手带你实现链表 » 不错的 unsafe 队列 » 数据布局 2 » 数据布局2: 再裸一些吧","id":"1005","title":"数据布局2: 再裸一些吧"},"1006":{"body":"在新的布局中我们将只使用裸指针,然后大家就等着好消息吧! 下面是之前的\"破代码\" : pub struct List { head: Link, tail: *mut Node, // 好人一枚\n} type Link = Option>>; // 恶魔一只 struct Node { elem: T, next: Link,\n} 现在删除恶魔: pub struct List { head: Link, tail: *mut Node,\n} type Link = *mut Node; // 嘀,新的好人卡,请查收 struct Node { elem: T, next: Link,\n} 请大家牢记:当使用裸指针时,Option 对我们是相当不友好的,所以这里不再使用。在后面还将引入 NonNull 类型,但是现在还无需操心。","breadcrumbs":"手把手带你实现链表 » 不错的 unsafe 队列 » 数据布局 2 » 布局","id":"1006","title":"布局"},"1007":{"body":"List::new 与之前几乎没有区别: use ptr; impl List { pub fn new() -> Self { List { head: ptr::null_mut(), tail: ptr::null_mut() } }\n} Push 也几乎没区... pub fn push(&mut self, elem: T) { let mut new_tail = Box::new( 等等,我们不再使用 Box 了,既然如此,该怎么分配内存呢? 也许我们可以使用 std::alloc::alloc,但是大家想象一下拿着武士刀进厨房切菜的场景,所以,还是算了吧。 我们想要 Box 又不想要,这里有一个也许很野但是管用的方法: struct Node { elem: T, real_next: Option>>, next: *mut Node,\n} 先创建一个 Box ,并使用一个裸指针指向 Box 中的 Node,然后就一直使用该裸指针直到我们处理完 Node 且可以销毁它之时。最后,可以将 Box 从 real_next 中 take 出来,并 drop 掉。 从上面来看,这个非常符合我们之前的简化版借用栈模型?借用 Box,再借用一个裸指针,然后先弹出该裸指针,再弹出 Box,嗯,果然很符合。 但是问题来了,这样做看上去有趣,但是你能保证这个简化版借用栈顺利的工作吗?所以,我们还是使用 Box::into_raw 函数吧! pub fn into_raw(b: Box) -> *mut T 消费掉 Box (拿走所有权),返回一个裸指针。该指针会被正确的对齐且不为 null 在调用该函数后,调用者需要对之前被 Box 所管理的内存负责,特别地,调用者需要正确的清理 T 并释放相应的内存。最简单的方式是通过 Box::from_raw 函数将裸指针再转回到 Box,然后 Box 的析构器就可以自动执行清理了。 注意:这是一个关联函数,因此 b.into_raw() 是不正确的,我们得使用 Box::into_raw(b)。因此该函数不会跟内部类型的同名方法冲突。","breadcrumbs":"手把手带你实现链表 » 不错的 unsafe 队列 » 数据布局 2 » 基本操作","id":"1007","title":"基本操作"},"1008":{"body":"将裸指针转换成 Box 以实现自动的清理: let x = Box::new(String::from(\"Hello\"));\nlet ptr = Box::into_raw(x);\nlet x = unsafe { Box::from_raw(ptr) }; 太棒了,简直为我们量身定制。而且它还很符合我们试图遵循的规则: 从安全的东东开始,将其转换成裸指针,最后再将裸指针转回安全的东东以实现安全的 drop。 现在,我们就可以到处使用裸指针,也无需再注意 unsafe 的范围,反正现在都是 unsafe 了,无所谓。 pub fn push(&mut self, elem: T) { unsafe { // 一开始就将 Box 转换成裸指针 let new_tail = Box::into_raw(Box::new(Node { elem: elem, next: ptr::null_mut(), })); if !self.tail.is_null() { (*self.tail).next = new_tail; } else { self.head = new_tail; } self.tail = new_tail; }\n} 嘿,都说 unsafe 不应该使用,但没想到 unsafe 真的是好!现在代码整体看起来简洁多了。 继续实现 pop,它跟之前区别不大,但是我们不要忘了使用 Box::from_raw 来清理内存: pub fn pop(&mut self) -> Option { unsafe { if self.head.is_null() { None } else { let head = Box::from_raw(self.head); self.head = head.next; if self.head.is_null() { self.tail = ptr::null_mut(); } Some(head.elem) } }\n} 纪念下死去的 take 和 map,现在我们得手动检查和设置 null 了。 然后再实现下析构器,直接循环 pop 即可,怎么说,简单可爱,谁不爱呢? impl Drop for List { fn drop(&mut self) { while let Some(_) = self.pop() { } }\n} 现在到了检验正确性的时候: #[cfg(test)]\nmod test { use super::List; #[test] fn basics() { let mut list = List::new(); // Check empty list behaves right assert_eq!(list.pop(), None); // Populate list list.push(1); list.push(2); list.push(3); // Check normal removal assert_eq!(list.pop(), Some(1)); assert_eq!(list.pop(), Some(2)); // Push some more just to make sure nothing's corrupted list.push(4); list.push(5); // Check normal removal assert_eq!(list.pop(), Some(3)); assert_eq!(list.pop(), Some(4)); // Check exhaustion assert_eq!(list.pop(), Some(5)); assert_eq!(list.pop(), None); // Check the exhaustion case fixed the pointer right list.push(6); list.push(7); // Check normal removal assert_eq!(list.pop(), Some(6)); assert_eq!(list.pop(), Some(7)); assert_eq!(list.pop(), None); }\n} $ cargo test running 12 tests\ntest fifth::test::basics ... ok\ntest first::test::basics ... ok\ntest fourth::test::basics ... ok\ntest fourth::test::peek ... ok\ntest second::test::basics ... ok\ntest fourth::test::into_iter ... ok\ntest second::test::into_iter ... ok\ntest second::test::iter ... ok\ntest second::test::iter_mut ... ok\ntest second::test::peek ... ok\ntest third::test::basics ... ok\ntest third::test::iter ... ok test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured 测试没问题,还有一个拦路虎 miri 呢。 MIRIFLAGS=\"-Zmiri-tag-raw-pointers\" cargo +nightly-2022-01-21 miri test running 12 tests\ntest fifth::test::basics ... ok\ntest first::test::basics ... ok\ntest fourth::test::basics ... ok\ntest fourth::test::peek ... ok\ntest second::test::basics ... ok\ntest fourth::test::into_iter ... ok\ntest second::test::into_iter ... ok\ntest second::test::iter ... ok\ntest second::test::iter_mut ... ok\ntest second::test::peek ... ok\ntest third::test::basics ... ok\ntest third::test::iter ... ok test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured 苦尽甘来,苦尽甘来啊!我们这些章节的努力没有白费,它终于成功的工作了。","breadcrumbs":"手把手带你实现链表 » 不错的 unsafe 队列 » 数据布局 2 » 示例","id":"1008","title":"示例"},"1009":{"body":"在搞定 push、pop 后,剩下的基本跟栈链表的实现没有啥区别。只有会改变链表长度的操作才会使用尾tail指针。 当然,现在一切都是裸指针,因此我们要重写代码来使用它们,在此过程中必须要确保没有遗漏地修改所有地方。 首先,先从栈链表实现中拷贝以下代码: // ... pub struct IntoIter(List); pub struct Iter<'a, T> { next: Option<&'a Node>,\n} pub struct IterMut<'a, T> { next: Option<&'a mut Node>,\n} 这里的 Iter 和 IterMut 并没有实现裸指针,先来修改下: pub struct IntoIter(List); pub struct Iter<'a, T> { next: *mut Node,\n} pub struct IterMut<'a, T> { next: *mut Node,\n} impl List { pub fn into_iter(self) -> IntoIter { IntoIter(self) } pub fn iter(&self) -> Iter<'_, T> { Iter { next: self.head } } pub fn iter_mut(&mut self) -> IterMut<'_, T> { IterMut { next: self.head } }\n} 看起来不错! error[E0392]: parameter `'a` is never used --> src\\fifth.rs:17:17 |\n17 | pub struct Iter<'a, T> { | ^^ unused parameter | = help: consider removing `'a`, referring to it in a field, or using a marker such as `PhantomData` error[E0392]: parameter `'a` is never used --> src\\fifth.rs:21:20 |\n21 | pub struct IterMut<'a, T> { | ^^ unused parameter | = help: consider removing `'a`, referring to it in a field, or using a marker such as `PhantomData` 咦?这里的 PhantomData 是什么? PhantomData 是零大小zero sized的类型 在你的类型中添加一个 PhantomData 字段,可以告诉编译器你的类型对 T 进行了使用,虽然并没有。说白了,就是让编译器不再给出 T 未被使用的警告或者错误。 如果想要更深入的了解,可以看下 Nomicon 大概最适用于 PhantomData 的场景就是一个结构体拥有未使用的生命周期,典型的就是在 unsafe 中使用。 总之,之前的错误是可以通过 PhantomData 来解决的,但是我想将这个秘密武器留到下一章中的双向链表,它才是真正的需要。 那现在只能破坏我们之前的豪言壮语了,灰溜溜的继续使用引用貌似也是不错的选择。能使用引用的原因是:我们可以创建一个迭代器,在其中使用安全引用,然后再丢弃迭代器。一旦迭代器被丢弃后,就可以继续使用 push 和 pop 了。 事实上,在迭代期间,我们还是需要解引用大量的裸指针,但是可以把引用看作裸指针的再借用。 偷偷的说一句:对于这个方法,我不敢保证一定能成功,先来试试吧.. pub struct IntoIter(List); pub struct Iter<'a, T> { next: Option<&'a Node>,\n} pub struct IterMut<'a, T> { next: Option<&'a mut Node>,\n} impl List { pub fn into_iter(self) -> IntoIter { IntoIter(self) } pub fn iter(&self) -> Iter<'_, T> { unsafe { Iter { next: self.head.as_ref() } } } pub fn iter_mut(&mut self) -> IterMut<'_, T> { unsafe { IterMut { next: self.head.as_mut() } } }\n} 为了存储引用,这里使用 Option 来包裹,并通过 ptr::as_ref 和 ptr::as_mut 来将裸指针转换成引用。 通常,我会尽量避免使用 as_ref 这类方法,因为它们在做一些不可思议的转换!但是上面却是极少数可以使用的场景之一。 这两个方法的使用往往会伴随很多警告,其中最有趣的是: 你必须要遵循混叠(Aliasing)的规则,原因是返回的生命周期 'a 只是任意选择的,并不能代表数据真实的生命周期。特别的,在这段生命周期的过程中,指针指向的内存区域绝不能被其它指针所访问。 好消息是,我们貌似不存在这个问题,因为混叠是我们一直在讨论和避免的问题。除此之外,还有一个恶魔: pub unsafe fn as_mut<'a>(self) -> Option<&'a mut T> 大家注意到这个凭空出现的 'a 吗?这里 self 是一个值类型,按照生命周期的规则,'a 无根之木,它就是 无界生命周期 。 兄弟们,我很紧张,但是该继续的还是得继续,让我们从栈链表中再复制一些代码过来: impl Iterator for IntoIter { type Item = T; fn next(&mut self) -> Option { self.0.pop() }\n} impl<'a, T> Iterator for Iter<'a, T> { type Item = &'a T; fn next(&mut self) -> Option { unsafe { self.next.map(|node| { self.next = node.next.as_ref(); &node.elem }) } }\n} impl<'a, T> Iterator for IterMut<'a, T> { type Item = &'a mut T; fn next(&mut self) -> Option { unsafe { self.next.take().map(|node| { self.next = node.next.as_mut(); &mut node.elem }) } }\n} 验证下测试用例: cargo test running 15 tests\ntest fifth::test::basics ... ok\ntest fifth::test::into_iter ... ok\ntest fifth::test::iter ... ok\ntest fifth::test::iter_mut ... ok\ntest first::test::basics ... ok\ntest fourth::test::basics ... ok\ntest fourth::test::into_iter ... ok\ntest fourth::test::peek ... ok\ntest second::test::basics ... ok\ntest second::test::into_iter ... ok\ntest second::test::iter ... ok\ntest second::test::iter_mut ... ok\ntest second::test::peek ... ok\ntest third::test::iter ... ok\ntest third::test::basics ... ok test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; 还有 miri: MIRIFLAGS=\"-Zmiri-tag-raw-pointers\" cargo +nightly-2022-01-21 miri test running 15 tests\ntest fifth::test::basics ... ok\ntest fifth::test::into_iter ... ok\ntest fifth::test::iter ... ok\ntest fifth::test::iter_mut ... ok\ntest first::test::basics ... ok\ntest fourth::test::basics ... ok\ntest fourth::test::into_iter ... ok\ntest fourth::test::peek ... ok\ntest second::test::basics ... ok\ntest second::test::into_iter ... ok\ntest second::test::iter ... ok\ntest second::test::iter_mut ... ok\ntest second::test::peek ... ok\ntest third::test::basics ... ok\ntest third::test::iter ... ok test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out 嗯,还有 peek 和 peek_mut 的实现: pub fn peek(&self) -> Option<&T> { unsafe { self.head.as_ref() }\n} pub fn peek_mut(&mut self) -> Option<&mut T> { unsafe { self.head.as_mut() }\n} 实现这么简单,运行起来肯定没问题: $ cargo build\nerror[E0308]: mismatched types --> src\\fifth.rs:66:13 |\n25 | impl List { | - this type parameter\n...\n64 | pub fn peek(&self) -> Option<&T> { | ---------- expected `Option<&T>` | because of return type\n65 | unsafe {\n66 | self.head.as_ref() | ^^^^^^^^^^^^^^^^^^ expected type parameter `T`, | found struct `fifth::Node` | = note: expected enum `Option<&T>` found enum `Option<&fifth::Node>` 哦,这个简单,map 以下就可以了: pub fn peek(&self) -> Option<&T> { unsafe { self.head.as_ref().map(|node| &node.elem) }\n} pub fn peek_mut(&mut self) -> Option<&mut T> { unsafe { self.head.as_mut().map(|node| &mut node.elem) }\n} 我感觉有很多错误正在赶来的路上,因此大家需要提高警惕,要么先写一个测试吧:把我们的 API 都混合在一起,让 miri 来享用 - miri food! #[test]\nfn miri_food() { let mut list = List::new(); list.push(1); list.push(2); list.push(3); assert!(list.pop() == Some(1)); list.push(4); assert!(list.pop() == Some(2)); list.push(5); assert!(list.peek() == Some(&3)); list.push(6); list.peek_mut().map(|x| *x *= 10); assert!(list.peek() == Some(&30)); assert!(list.pop() == Some(30)); for elem in list.iter_mut() { *elem *= 100; } let mut iter = list.iter(); assert_eq!(iter.next(), Some(&400)); assert_eq!(iter.next(), Some(&500)); assert_eq!(iter.next(), Some(&600)); assert_eq!(iter.next(), None); assert_eq!(iter.next(), None); assert!(list.pop() == Some(400)); list.peek_mut().map(|x| *x *= 10); assert!(list.peek() == Some(&5000)); list.push(7); // Drop it on the ground and let the dtor exercise itself\n} cargo test running 16 tests\ntest fifth::test::basics ... ok\ntest fifth::test::into_iter ... ok\ntest fifth::test::iter ... ok\ntest fifth::test::iter_mut ... ok\ntest fifth::test::miri_food ... ok\ntest first::test::basics ... ok\ntest fourth::test::basics ... ok\ntest fourth::test::into_iter ... ok\ntest fourth::test::peek ... ok\ntest second::test::into_iter ... ok\ntest second::test::basics ... ok\ntest second::test::iter_mut ... ok\ntest second::test::peek ... ok\ntest third::test::iter ... ok\ntest second::test::iter ... ok\ntest third::test::basics ... ok test result: ok. 16 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out MIRIFLAGS=\"-Zmiri-tag-raw-pointers\" cargo +nightly-2022-01-21 miri test running 16 tests\ntest fifth::test::basics ... ok\ntest fifth::test::into_iter ... ok\ntest fifth::test::iter ... ok\ntest fifth::test::iter_mut ... ok\ntest fifth::test::miri_food ... ok\ntest first::test::basics ... ok\ntest fourth::test::basics ... ok\ntest fourth::test::into_iter ... ok\ntest fourth::test::peek ... ok\ntest second::test::into_iter ... ok\ntest second::test::basics ... ok\ntest second::test::iter_mut ... ok\ntest second::test::peek ... ok\ntest third::test::iter ... ok\ntest second::test::iter ... ok\ntest third::test::basics ... ok test result: ok. 16 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out 完美。","breadcrumbs":"手把手带你实现链表 » 不错的 unsafe 队列 » 额外的操作 » 额外的操作","id":"1009","title":"额外的操作"},"101":{"body":"切片并不是 Rust 独有的概念,在 Go 语言中就非常流行,它允许你引用集合中部分连续的元素序列,而不是引用整个集合。 对于字符串而言,切片就是对 String 类型中某一部分的引用,它看起来像这样: let s = String::from(\"hello world\"); let hello = &s[0..5];\nlet world = &s[6..11]; hello 没有引用整个 String s,而是引用了 s 的一部分内容,通过 [0..5] 的方式来指定。 这就是创建切片的语法,使用方括号包括的一个序列: [开始索引..终止索引] ,其中开始索引是切片中第一个元素的索引位置,而终止索引是最后一个元素后面的索引位置,也就是这是一个 右半开区间。在切片数据结构内部会保存开始的位置和切片的长度,其中长度是通过 终止索引 - 开始索引 的方式计算得来的。 对于 let world = &s[6..11]; 来说,world 是一个切片,该切片的指针指向 s 的第 7 个字节(索引从 0 开始, 6 是第 7 个字节),且该切片的长度是 5 个字节。 在使用 Rust 的 .. range 序列 语法时,如果你想从索引 0 开始,可以使用如下的方式,这两个是等效的: let s = String::from(\"hello\"); let slice = &s[0..2];\nlet slice = &s[..2]; 同样的,如果你的切片想要包含 String 的最后一个字节,则可以这样使用: let s = String::from(\"hello\"); let len = s.len(); let slice = &s[4..len];\nlet slice = &s[4..]; 你也可以截取完整的 String 切片: let s = String::from(\"hello\"); let len = s.len(); let slice = &s[0..len];\nlet slice = &s[..]; 在对字符串使用切片语法时需要格外小心,切片的索引必须落在字符之间的边界位置,也就是 UTF-8 字符的边界,例如中文在 UTF-8 中占用三个字节,下面的代码就会崩溃: let s = \"中国人\"; let a = &s[0..2]; println!(\"{}\",a); 因为我们只取 s 字符串的前两个字节,但是本例中每个汉字占用三个字节,因此没有落在边界处,也就是连 中 字都取不完整,此时程序会直接崩溃退出,如果改成 &s[0..3],则可以正常通过编译。 因此,当你需要对字符串做切片索引操作时,需要格外小心这一点, 关于该如何操作 UTF-8 字符串,参见 这里 。 字符串切片的类型标识是 &str,因此我们可以这样声明一个函数,输入 String 类型,返回它的切片: fn first_word(s: &String) -> &str 。 有了切片就可以写出这样的代码: fn main() { let mut s = String::from(\"hello world\"); let word = first_word(&s); s.clear(); // error! println!(\"the first word is: {}\", word);\n}\nfn first_word(s: &String) -> &str { &s[..1]\n} 编译器报错如下: error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable --> src/main.rs:18:5 |\n16 | let word = first_word(&s); | -- immutable borrow occurs here\n17 |\n18 | s.clear(); // error! | ^^^^^^^^^ mutable borrow occurs here\n19 |\n20 | println!(\"the first word is: {}\", word); | ---- immutable borrow later used here 回忆一下借用的规则:当我们已经有了可变借用时,就无法再拥有不可变的借用。因为 clear 需要清空改变 String,因此它需要一个可变借用(利用 VSCode 可以看到该方法的声明是 pub fn clear(&mut self) ,参数是对自身的可变借用 );而之后的 println! 又使用了不可变借用,也就是在 s.clear() 处可变借用与不可变借用试图同时生效,因此编译无法通过。 从上述代码可以看出,Rust 不仅让我们的 API 更加容易使用,而且也在编译期就消除了大量错误! 其它切片 因为切片是对集合的部分引用,因此不仅仅字符串有切片,其它集合类型也有,例如数组: let a = [1, 2, 3, 4, 5]; let slice = &a[1..3]; assert_eq!(slice, &[2, 3]); 该数组切片的类型是 &[i32],数组切片和字符串切片的工作方式是一样的,例如持有一个引用指向原始数组的某个元素和长度。","breadcrumbs":"Rust 基础入门 » 复合类型 » 字符串与切片 » 切片(slice)","id":"101","title":"切片(slice)"},"1010":{"body":"得益于不安全代码的引入,新的实现可以获得线性的性能提升,同时我们还设法复用了栈链表的很多代码。 当然,这个过程中,我们还引入了新的概念,例如借用栈,相信直到现在有些同学还晕乎乎的。不管如何,我们不用再去写一大堆嵌套来嵌套去的 Rc 和 RefCell。 下面来看看咱们这个不安全链表的全貌吧。 use std::ptr; pub struct List { head: Link, tail: *mut Node,\n} type Link = *mut Node; struct Node { elem: T, next: Link,\n} pub struct IntoIter(List); pub struct Iter<'a, T> { next: Option<&'a Node>,\n} pub struct IterMut<'a, T> { next: Option<&'a mut Node>,\n} impl List { pub fn new() -> Self { List { head: ptr::null_mut(), tail: ptr::null_mut() } } pub fn push(&mut self, elem: T) { unsafe { let new_tail = Box::into_raw(Box::new(Node { elem: elem, next: ptr::null_mut(), })); if !self.tail.is_null() { (*self.tail).next = new_tail; } else { self.head = new_tail; } self.tail = new_tail; } } pub fn pop(&mut self) -> Option { unsafe { if self.head.is_null() { None } else { let head = Box::from_raw(self.head); self.head = head.next; if self.head.is_null() { self.tail = ptr::null_mut(); } Some(head.elem) } } } pub fn peek(&self) -> Option<&T> { unsafe { self.head.as_ref().map(|node| &node.elem) } } pub fn peek_mut(&mut self) -> Option<&mut T> { unsafe { self.head.as_mut().map(|node| &mut node.elem) } } pub fn into_iter(self) -> IntoIter { IntoIter(self) } pub fn iter(&self) -> Iter<'_, T> { unsafe { Iter { next: self.head.as_ref() } } } pub fn iter_mut(&mut self) -> IterMut<'_, T> { unsafe { IterMut { next: self.head.as_mut() } } }\n} impl Drop for List { fn drop(&mut self) { while let Some(_) = self.pop() { } }\n} impl Iterator for IntoIter { type Item = T; fn next(&mut self) -> Option { self.0.pop() }\n} impl<'a, T> Iterator for Iter<'a, T> { type Item = &'a T; fn next(&mut self) -> Option { unsafe { self.next.map(|node| { self.next = node.next.as_ref(); &node.elem }) } }\n} impl<'a, T> Iterator for IterMut<'a, T> { type Item = &'a mut T; fn next(&mut self) -> Option { unsafe { self.next.take().map(|node| { self.next = node.next.as_mut(); &mut node.elem }) } }\n} #[cfg(test)]\nmod test { use super::List; #[test] fn basics() { let mut list = List::new(); // Check empty list behaves right assert_eq!(list.pop(), None); // Populate list list.push(1); list.push(2); list.push(3); // Check normal removal assert_eq!(list.pop(), Some(1)); assert_eq!(list.pop(), Some(2)); // Push some more just to make sure nothing's corrupted list.push(4); list.push(5); // Check normal removal assert_eq!(list.pop(), Some(3)); assert_eq!(list.pop(), Some(4)); // Check exhaustion assert_eq!(list.pop(), Some(5)); assert_eq!(list.pop(), None); // Check the exhaustion case fixed the pointer right list.push(6); list.push(7); // Check normal removal assert_eq!(list.pop(), Some(6)); assert_eq!(list.pop(), Some(7)); assert_eq!(list.pop(), None); } #[test] fn into_iter() { let mut list = List::new(); list.push(1); list.push(2); list.push(3); let mut iter = list.into_iter(); assert_eq!(iter.next(), Some(1)); assert_eq!(iter.next(), Some(2)); assert_eq!(iter.next(), Some(3)); assert_eq!(iter.next(), None); } #[test] fn iter() { let mut list = List::new(); list.push(1); list.push(2); list.push(3); let mut iter = list.iter(); assert_eq!(iter.next(), Some(&1)); assert_eq!(iter.next(), Some(&2)); assert_eq!(iter.next(), Some(&3)); assert_eq!(iter.next(), None); } #[test] fn iter_mut() { let mut list = List::new(); list.push(1); list.push(2); list.push(3); let mut iter = list.iter_mut(); assert_eq!(iter.next(), Some(&mut 1)); assert_eq!(iter.next(), Some(&mut 2)); assert_eq!(iter.next(), Some(&mut 3)); assert_eq!(iter.next(), None); } #[test] fn miri_food() { let mut list = List::new(); list.push(1); list.push(2); list.push(3); assert!(list.pop() == Some(1)); list.push(4); assert!(list.pop() == Some(2)); list.push(5); assert!(list.peek() == Some(&3)); list.push(6); list.peek_mut().map(|x| *x *= 10); assert!(list.peek() == Some(&30)); assert!(list.pop() == Some(30)); for elem in list.iter_mut() { *elem *= 100; } let mut iter = list.iter(); assert_eq!(iter.next(), Some(&400)); assert_eq!(iter.next(), Some(&500)); assert_eq!(iter.next(), Some(&600)); assert_eq!(iter.next(), None); assert_eq!(iter.next(), None); assert!(list.pop() == Some(400)); list.peek_mut().map(|x| *x *= 10); assert!(list.peek() == Some(&5000)); list.push(7); // Drop it on the ground and let the dtor exercise itself }\n}","breadcrumbs":"手把手带你实现链表 » 不错的 unsafe 队列 » 最终代码 » 最终代码","id":"1010","title":"最终代码"},"1011":{"body":"说句实话,我们之前实现的链表都达不到生产级可用的程度,而且也没有用到一些比较时髦的技巧。 本章我们一起来看一些更时髦的链表实现: 生产级可用的双向链表 双重单向链表 栈分配的链表 自引用和Arena分配器实现( 原文作者还未实现,所以... Todo ) GhostCell 实现( 同上 )","breadcrumbs":"手把手带你实现链表 » 使用高级技巧实现链表 » 使用高级技巧实现链表","id":"1011","title":"使用高级技巧实现链表"},"1012":{"body":"打开 原文 ,发现这一篇只有两行,我以为自己看花了眼,揉了揉眼,定睛一看,还是两行。 没错,貌似作者想要偷懒,而且为了掩饰,他还提供了标准库的实现:) 如果大家想要学习,看 标准库 吧 :D 为了能更好的看懂标准库实现,你可能还需要这本书的辅助: Rustonomicon","breadcrumbs":"手把手带你实现链表 » 使用高级技巧实现链表 » 生产级可用的双向链表 » 生产级可用的双向链表","id":"1012","title":"生产级可用的双向链表"},"1013":{"body":"在之前的双向链表章节中,我们一度非常纠结,原因来自同样纠结成一团的所有权依赖。还有一个重要原因就是:先入为主的链表定义。 谁说所有的链接一定要一个方向呢?这里一起来尝试下新的东东:链表的其中一半朝左,另一半朝右。 新规矩( 老规矩是创建文件 ),创建一个新的模块: // lib.rs\n// ...\npub mod silly1; // NEW! // silly1.rs\nuse crate::second::List as Stack; struct List { left: Stack, right: Stack,\n} 这里将之前的 List 引入进来,并重命名为 Stack,接着,创建一个新的链表。现在既可以向左增长又可以向右增长。 pub struct Stack { head: Link,\n} type Link = Option>>; struct Node { elem: T, next: Link,\n} impl Stack { pub fn new() -> Self { Stack { head: None } } pub fn push(&mut self, elem: T) { let new_node = Box::new(Node { elem: elem, next: self.head.take(), }); self.head = Some(new_node); } pub fn pop(&mut self) -> Option { self.head.take().map(|node| { let node = *node; self.head = node.next; node.elem }) } pub fn peek(&self) -> Option<&T> { self.head.as_ref().map(|node| { &node.elem }) } pub fn peek_mut(&mut self) -> Option<&mut T> { self.head.as_mut().map(|node| { &mut node.elem }) }\n} impl Drop for Stack { fn drop(&mut self) { let mut cur_link = self.head.take(); while let Some(mut boxed_node) = cur_link { cur_link = boxed_node.next.take(); } }\n} 稍微修改下 push 和 pop: pub fn push(&mut self, elem: T) { let new_node = Box::new(Node { elem: elem, next: None, }); self.push_node(new_node);\n} fn push_node(&mut self, mut node: Box>) { node.next = self.head.take(); self.head = Some(node);\n} pub fn pop(&mut self) -> Option { self.pop_node().map(|node| { node.elem })\n} fn pop_node(&mut self) -> Option>> { self.head.take().map(|mut node| { self.head = node.next.take(); node })\n} 现在可以开始构造新的链表: pub struct List { left: Stack, right: Stack,\n} impl List { fn new() -> Self { List { left: Stack::new(), right: Stack::new() } }\n} 当然,还有一大堆左左右右类型的操作: pub fn push_left(&mut self, elem: T) { self.left.push(elem) }\npub fn push_right(&mut self, elem: T) { self.right.push(elem) }\npub fn pop_left(&mut self) -> Option { self.left.pop() }\npub fn pop_right(&mut self) -> Option { self.right.pop() }\npub fn peek_left(&self) -> Option<&T> { self.left.peek() }\npub fn peek_right(&self) -> Option<&T> { self.right.peek() }\npub fn peek_left_mut(&mut self) -> Option<&mut T> { self.left.peek_mut() }\npub fn peek_right_mut(&mut self) -> Option<&mut T> { self.right.peek_mut() } 其中最有趣的是:还可以来回闲逛了。 pub fn go_left(&mut self) -> bool { self.left.pop_node().map(|node| { self.right.push_node(node); }).is_some()\n} pub fn go_right(&mut self) -> bool { self.right.pop_node().map(|node| { self.left.push_node(node); }).is_some()\n} 这里返回 bool 是为了告诉调用者我们是否成功的移动。最后,再来测试下: #[cfg(test)]\nmod test { use super::List; #[test] fn walk_aboot() { let mut list = List::new(); // [_] list.push_left(0); // [0,_] list.push_right(1); // [0, _, 1] assert_eq!(list.peek_left(), Some(&0)); assert_eq!(list.peek_right(), Some(&1)); list.push_left(2); // [0, 2, _, 1] list.push_left(3); // [0, 2, 3, _, 1] list.push_right(4); // [0, 2, 3, _, 4, 1] while list.go_left() {} // [_, 0, 2, 3, 4, 1] assert_eq!(list.pop_left(), None); assert_eq!(list.pop_right(), Some(0)); // [_, 2, 3, 4, 1] assert_eq!(list.pop_right(), Some(2)); // [_, 3, 4, 1] list.push_left(5); // [5, _, 3, 4, 1] assert_eq!(list.pop_right(), Some(3)); // [5, _, 4, 1] assert_eq!(list.pop_left(), Some(5)); // [_, 4, 1] assert_eq!(list.pop_right(), Some(4)); // [_, 1] assert_eq!(list.pop_right(), Some(1)); // [_] assert_eq!(list.pop_right(), None); assert_eq!(list.pop_left(), None); }\n} $ cargo test Running target/debug/lists-5c71138492ad4b4a running 16 tests\ntest fifth::test::into_iter ... ok\ntest fifth::test::basics ... ok\ntest fifth::test::iter ... ok\ntest fifth::test::iter_mut ... ok\ntest fourth::test::into_iter ... ok\ntest fourth::test::basics ... ok\ntest fourth::test::peek ... ok\ntest first::test::basics ... ok\ntest second::test::into_iter ... ok\ntest second::test::basics ... ok\ntest second::test::iter ... ok\ntest second::test::iter_mut ... ok\ntest third::test::basics ... ok\ntest third::test::iter ... ok\ntest second::test::peek ... ok\ntest silly1::test::walk_aboot ... ok test result: ok. 16 passed; 0 failed; 0 ignored; 0 measured 上上下下,左左右右,BABA,哦耶,这个链表无敌了! 以上是一个非常典型的手指型数据结构finger data structure,在其中维护一个手指,然后操作所需的时间与手指的距离成正比。","breadcrumbs":"手把手带你实现链表 » 使用高级技巧实现链表 » 双单向链表 » 双单向链表","id":"1013","title":"双单向链表"},"1014":{"body":"在之前的章节中,无一例外,我们创建的都是数据存储在堆上的链表,这种链表最常见也最实用:堆内存在动态分配的场景非常好用。 但是,既然是高级技巧章节,那栈链表也应该拥有一席之地。但与堆内存的简单分配相比,栈内存就没那么友好了,你们猜大名鼎鼎的 C 语言的 alloca 是因为什么而出名的 :) 限于章节篇幅,这里我们使用一个简单的栈分配方法:调用一个函数,获取一个新的、拥有更多空间的栈帧。说实话,该解决方法要多愚蠢有多愚蠢,但是它确实相当实用,甚至...有用。 任何时候,当我们在做一些递归的任务时,都可以将当前步骤状态的指针传递给下一个步骤。如果指针本身就是状态的一部分,那恭喜你:你在创建一个栈上分配的链表! 新的链表类型本身就是一个 Node,并且包含一个引用指向另一个 Node: pub struct List<'a, T> { pub data: T, pub prev: Option<&'a List<'a, T>>,\n} 该链表只有一个操作 push,需要注意的是,跟其它链表不同,这里的 push 是通过回调的方式来完成新元素推入,并将回调返回的值直接返回给 push 的调用者: impl<'a, T> List<'a, T> { pub fn push( prev: Option<&'a List<'a, T>>, data: T, callback: impl FnOnce(&List<'a, T>) -> U, ) -> U { let list = List { data, prev }; callback(&list) }\n} 搞定,提前问一句:你见过回调地狱吗? List::push(None, 3, |list| { println!(\"{}\", list.data); List::push(Some(list), 5, |list| { println!(\"{}\", list.data); List::push(Some(list), 13, |list| { println!(\"{}\", list.data); }) })\n}) 不禁让人感叹,这段回调代码多么的美丽动人😿。 用户还可以简单地使用 while-let 的方式来编译遍历链表,但是为了增加一些趣味,咱们还是继续使用迭代器: impl<'a, T> List<'a, T> { pub fn iter(&'a self) -> Iter<'a, T> { Iter { next: Some(self) } }\n} impl<'a, T> Iterator for Iter<'a, T> { type Item = &'a T; fn next(&mut self) -> Option { self.next.map(|node| { self.next = node.prev; &node.data }) }\n} 测试下: #[cfg(test)]\nmod test { use super::List; #[test] fn elegance() { List::push(None, 3, |list| { assert_eq!(list.iter().copied().sum::(), 3); List::push(Some(list), 5, |list| { assert_eq!(list.iter().copied().sum::(), 5 + 3); List::push(Some(list), 13, |list| { assert_eq!(list.iter().copied().sum::(), 13 + 5 + 3); }) }) }) }\n} $ cargo test running 18 tests\ntest fifth::test::into_iter ... ok\ntest fifth::test::iter ... ok\ntest fifth::test::iter_mut ... ok\ntest fifth::test::basics ... ok\ntest fifth::test::miri_food ... ok\ntest first::test::basics ... ok\ntest second::test::into_iter ... ok\ntest fourth::test::peek ... ok\ntest fourth::test::into_iter ... ok\ntest second::test::iter_mut ... ok\ntest fourth::test::basics ... ok\ntest second::test::basics ... ok\ntest second::test::iter ... ok\ntest third::test::basics ... ok\ntest silly1::test::walk_aboot ... ok\ntest silly2::test::elegance ... ok\ntest second::test::peek ... ok\ntest third::test::iter ... ok test result: ok. 18 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; 部分读者此时可能会有一些大胆的想法:咦?我能否修改 Node 中的值?大胆但貌似可行,不妨来试试。 pub struct List<'a, T> { pub data: T, pub prev: Option<&'a mut List<'a, T>>,\n} pub struct Iter<'a, T> { next: Option<&'a List<'a, T>>,\n} impl<'a, T> List<'a, T> { pub fn push( prev: Option<&'a mut List<'a, T>>, data: T, callback: impl FnOnce(&mut List<'a, T>) -> U, ) -> U { let mut list = List { data, prev }; callback(&mut list) } pub fn iter(&'a self) -> Iter<'a, T> { Iter { next: Some(self) } }\n} impl<'a, T> Iterator for Iter<'a, T> { type Item = &'a T; fn next(&mut self) -> Option { self.next.map(|node| { self.next = node.prev.as_ref().map(|prev| &**prev); &node.data }) }\n} $ cargo test error[E0521]: borrowed data escapes outside of closure --> src\\silly2.rs:47:32 |\n46 | List::push(Some(list), 13, |list| { | ---- | | | `list` declared here, outside of the closure body | `list` is a reference that is only valid in the closure body\n47 | assert_eq!(list.iter().copied().sum::(), 13 + 5 + 3); | ^^^^^^^^^^^ `list` escapes the closure body here error[E0521]: borrowed data escapes outside of closure --> src\\silly2.rs:45:28 |\n44 | List::push(Some(list), 5, |list| { | ---- | | | `list` declared here, outside of the closure body | `list` is a reference that is only valid in the closure body\n45 | assert_eq!(list.iter().copied().sum::(), 5 + 3); | ^^^^^^^^^^^ `list` escapes the closure body here 嗯,没想到是浓眉大眼的迭代器背叛了我们,为了验证到底是哪里出了问题,我们来修改下测试: #[test]\nfn elegance() { List::push(None, 3, |list| { assert_eq!(list.data, 3); List::push(Some(list), 5, |list| { assert_eq!(list.data, 5); List::push(Some(list), 13, |list| { assert_eq!(list.data, 13); }) }) })\n} $ cargo test error[E0521]: borrowed data escapes outside of closure --> src\\silly2.rs:46:17 |\n44 | List::push(Some(list), 5, |list| { | ---- | | | `list` declared here, outside of the closure body | `list` is a reference that is only valid in the closure body\n45 | assert_eq!(list.data, 5);\n46 | / List::push(Some(list), 13, |list| {\n47 | | assert_eq!(list.data, 13);\n48 | | }) | |______^ `list` escapes the closure body here error[E0521]: borrowed data escapes outside of closure --> src\\silly2.rs:44:13 |\n42 | List::push(None, 3, |list| { | ---- | | | `list` declared here, outside of the closure body | `list` is a reference that is only valid in the closure body\n43 | assert_eq!(list.data, 3);\n44 | / List::push(Some(list), 5, |list| {\n45 | | assert_eq!(list.data, 5);\n46 | | List::push(Some(list), 13, |list| {\n47 | | assert_eq!(list.data, 13);\n48 | | })\n49 | | }) | |______________^ `list` escapes the closure body here 原因在于我们的链表不小心依赖了型变variance。型变是一个 相当复杂的概念 ,下面来简单了解下。 每一个节点( Node )都包含一个引用,该引用指向另一个节点, 且这两个节点是同一个类型。如果从最里面的节点角度来看,那所有外部的节点都在使用和它一样的生命周期,但这个显然是不对的:链表中的每一个节点都会比它指向的节点活得更久,因为它们的作用域是嵌套存在的。 那之前的不可变引用版本为何可以正常工作呢?原因是在大多数时候,编译器都能自己判断:虽然某些东东活得太久了,但是这是安全的。当我们把一个 List 塞入另一个时,编译器会迅速将生命周期进行收缩以满足新的 List 的需求, 这种生命周期收缩就是一种型变 。 如果大家还是觉得不太理解,我们来考虑下其它拥有继承特性的编程语言。在该语言中,当你将一个 Cat 传递给需要 Animal 的地方时( Animal 是 Cat 的父类型),型变就发生了。从字面来说,将一只猫传给需要动物的地方,也是合适的,毕竟猫确实是动物的一种。 总之,可以看出无论是从大的生命周期收缩为小的生命周期,还是从 Cat 到 Animal,型变的典型特征就是:范围在减小,毕竟子类型的功能肯定是比父类型多的。 既然有型变,为何可变引用的版本会报错呢?其实在于型变不总是安全的,假如之前的代码可以编译,那我们可以写出释放后再使用use-after-free 的代码: List::push(None, 3, |list| { List::push(Some(list), 5, |list| { List::push(Some(list), 13, |list| { // 哈哈,好爽,由于所有的生命周期都是相同的,因此编译器允许我重写父节点,并让它持有一个可变指针指向我自己。 // 我将创建所有的 use-after-free ! *list.prev.as_mut().unwrap().prev = Some(list); }) })\n}) 一旦引入可变性,型变就会造成这样的隐患:意外修改了不该被修改的代码,但这些代码的调用者还在期待着和往常一样的结果!例如以下例子: let mut my_kitty = Cat; // Make a Cat (long lifetime)\nlet animal: &mut Animal = &mut my_kitty; // Forget it's a Cat (shorten lifetime)\n*animal = Dog; // Write a Dog (short lifetime)\nmy_kitty.meow(); // Meowing Dog! (Use After Free) 我们将长生命周期的猫转换成短生命周期的动物,可变的!然后通过短生命周期的动物将指针重新指向一只狗。此时我们想去撸软萌猫的时候,就听到:旺旺...呜嗷嗷嗷,对,你没听错,不仅没有了猫叫,甚至于狗还没叫完,就可能在某个地方又被修改成狼了。 因此, 虽然你可以修改可变引用的生命周期,但是一旦开始嵌套,它们就将失去型变,变成不变( invariant ) 。此时,就再也无法对生命周期进行收缩了。 具体来说: &mut &'big mut T 无法被转换成 &mut &'small mut T,这里 'big 代表比 'small 更大的生命周期。或者用更正式的说法:&'a mut T 对于 'a 来说是协变( covariant )的,但是对于 T 是不变的( invariant )。 说了这么多高深的理论,那么该如何改变链表的数据呢?答案就是:使用老本行 - 内部可变性。 下面让我们回滚到之前的不可变版本,然后使用 Cell 来替代 &mut。 #[test]\nfn cell() { use std::cell::Cell; List::push(None, Cell::new(3), |list| { List::push(Some(list), Cell::new(5), |list| { List::push(Some(list), Cell::new(13), |list| { // Multiply every value in the list by 10 for val in list.iter() { val.set(val.get() * 10) } let mut vals = list.iter(); assert_eq!(vals.next().unwrap().get(), 130); assert_eq!(vals.next().unwrap().get(), 50); assert_eq!(vals.next().unwrap().get(), 30); assert_eq!(vals.next(), None); assert_eq!(vals.next(), None); }) }) })\n} $ cargo test running 19 tests\ntest fifth::test::into_iter ... ok\ntest fifth::test::basics ... ok\ntest fifth::test::iter_mut ... ok\ntest fifth::test::iter ... ok\ntest fourth::test::basics ... ok\ntest fourth::test::into_iter ... ok\ntest second::test::into_iter ... ok\ntest first::test::basics ... ok\ntest fourth::test::peek ... ok\ntest second::test::basics ... ok\ntest fifth::test::miri_food ... ok\ntest silly2::test::cell ... ok\ntest third::test::iter ... ok\ntest second::test::iter_mut ... ok\ntest second::test::peek ... ok\ntest silly1::test::walk_aboot ... ok\ntest silly2::test::elegance ... ok\ntest third::test::basics ... ok\ntest second::test::iter ... ok test result: ok. 19 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; 简简单单搞定,虽然之前我们嫌弃内部可变性,但是在这里:真香!","breadcrumbs":"手把手带你实现链表 » 使用高级技巧实现链表 » 栈上的链表 » 栈上的链表","id":"1014","title":"栈上的链表"},"1015":{"body":"","breadcrumbs":"征服编译错误 » 征服编译错误","id":"1015","title":"征服编译错误"},"1016":{"body":"","breadcrumbs":"征服编译错误 » 对抗编译检查 » 对抗编译检查","id":"1016","title":"对抗编译检查"},"1017":{"body":"本章并不讲太多的概念,主要是用例子来引导大家去思考该如何对抗编译检查。","breadcrumbs":"征服编译错误 » 对抗编译检查 » 生命周期 » 生命周期","id":"1017","title":"生命周期"},"1018":{"body":"在大多时候,Rust 的生命周期你只要标识了,即可以通过编译,但是总是存在一些情况,会导致编译无法通过,本文就讲述这样一种情况:因为生命周期声明的范围过大,导致了编译无法通过,希望大家喜欢","breadcrumbs":"征服编译错误 » 对抗编译检查 » 生命周期 » 生命周期过大-01 » 生命周期声明的范围过大","id":"1018","title":"生命周期声明的范围过大"},"1019":{"body":"struct Interface<'a> { manager: &'a mut Manager<'a>\n} impl<'a> Interface<'a> { pub fn noop(self) { println!(\"interface consumed\"); }\n} struct Manager<'a> { text: &'a str\n} struct List<'a> { manager: Manager<'a>,\n} impl<'a> List<'a> { pub fn get_interface(&'a mut self) -> Interface { Interface { manager: &mut self.manager } }\n} fn main() { let mut list = List { manager: Manager { text: \"hello\" } }; list.get_interface().noop(); println!(\"Interface should be dropped here and the borrow released\"); // this fails because inmutable/mutable borrow // but Interface should be already dropped here and the borrow released use_list(&list);\n} fn use_list(list: &List) { println!(\"{}\", list.manager.text);\n} 运行后报错: error[E0502]: cannot borrow `list` as immutable because it is also borrowed as mutable // `list`无法被借用,因为已经被可变借用 --> src/main.rs:40:14 |\n34 | list.get_interface().noop(); | ---- mutable borrow occurs here // 可变借用发生在这里\n...\n40 | use_list(&list); | ^^^^^ | | | immutable borrow occurs here // 新的不可变借用发生在这 | mutable borrow later used here // 可变借用在这里结束 这段代码看上去并不复杂,实际上难度挺高的,首先在直觉上,list.get_interface()借用的可变引用,按理来说应该在这行代码结束后,就归还了,为何能持续到use_list(&list)后面呢? 这是因为我们在get_interface方法中声明的lifetime有问题,该方法的参数的生明周期是'a,而List的生命周期也是'a,说明该方法至少活得跟List一样久,再回到main函数中,list可以活到main函数的结束,因此list.get_interface()借用的可变引用也会活到main函数的结束,在此期间,自然无法再进行借用了。 要解决这个问题,我们需要为get_interface方法的参数给予一个不同于List<'a>的生命周期'b,最终代码如下: struct Interface<'b, 'a: 'b> { manager: &'b mut Manager<'a>\n} impl<'b, 'a: 'b> Interface<'b, 'a> { pub fn noop(self) { println!(\"interface consumed\"); }\n} struct Manager<'a> { text: &'a str\n} struct List<'a> { manager: Manager<'a>,\n} impl<'a> List<'a> { pub fn get_interface<'b>(&'b mut self) -> Interface<'b, 'a> where 'a: 'b { Interface { manager: &mut self.manager } }\n} fn main() { let mut list = List { manager: Manager { text: \"hello\" } }; list.get_interface().noop(); println!(\"Interface should be dropped here and the borrow released\"); // this fails because inmutable/mutable borrow // but Interface should be already dropped here and the borrow released use_list(&list);\n} fn use_list(list: &List) { println!(\"{}\", list.manager.text);\n} 当然,咱还可以给生命周期给予更有意义的名称: struct Interface<'text, 'manager> { manager: &'manager mut Manager<'text>\n} impl<'text, 'manager> Interface<'text, 'manager> { pub fn noop(self) { println!(\"interface consumed\"); }\n} struct Manager<'text> { text: &'text str\n} struct List<'text> { manager: Manager<'text>,\n} impl<'text> List<'text> { pub fn get_interface<'manager>(&'manager mut self) -> Interface<'text, 'manager> where 'text: 'manager { Interface { manager: &mut self.manager } }\n} fn main() { let mut list = List { manager: Manager { text: \"hello\" } }; list.get_interface().noop(); println!(\"Interface should be dropped here and the borrow released\"); // this fails because inmutable/mutable borrow // but Interface should be already dropped here and the borrow released use_list(&list);\n} fn use_list(list: &List) { println!(\"{}\", list.manager.text);\n}","breadcrumbs":"征服编译错误 » 对抗编译检查 » 生命周期 » 生命周期过大-01 » 例子 1","id":"1019","title":"例子 1"},"102":{"body":"之前提到过字符串字面量,但是没有提到它的类型: let s = \"Hello, world!\"; 实际上,s 的类型是 &str,因此你也可以这样声明: let s: &str = \"Hello, world!\"; 该切片指向了程序可执行文件中的某个点,这也是为什么字符串字面量是不可变的,因为 &str 是一个不可变引用。 了解完切片,可以进入本节的正题了。","breadcrumbs":"Rust 基础入门 » 复合类型 » 字符串与切片 » 字符串字面量是切片","id":"102","title":"字符串字面量是切片"},"1020":{"body":"继上篇文章后,我们再来看一段 可能 涉及生命周期过大导致的无法编译问题: fn bar(writer: &mut Writer) { baz(writer.indent()); writer.write(\"world\");\n} fn baz(writer: &mut Writer) { writer.write(\"hello\");\n} pub struct Writer<'a> { target: &'a mut String, indent: usize,\n} impl<'a> Writer<'a> { fn indent(&'a mut self) -> &'a mut Self { &mut Self { target: self.target, indent: self.indent + 1, } } fn write(&mut self, s: &str) { for _ in 0..self.indent { self.target.push(' '); } self.target.push_str(s); self.target.push('\\n'); }\n} fn main() {} 报错如下: error[E0623]: lifetime mismatch --> src/main.rs:2:16 |\n1 | fn bar(writer: &mut Writer) { | ----------- | | | these two types are declared with different lifetimes...\n2 | baz(writer.indent()); | ^^^^^^ ...but data from `writer` flows into `writer` here WTF,这什么报错,之前都没有见过,而且很难理解,什么叫writer滑入了另一个writer? 别急,我们先来仔细看下代码,注意这一段: impl<'a> Writer<'a> { fn indent(&'a mut self) -> &'a mut Self { &mut Self { target: self.target, indent: self.indent + 1, } } 这里的生命周期定义说明indent方法使用的。。。等等!你的代码错了,你怎么能在一个函数中返回一个新创建实例的引用?!!最重要的是,编译器不提示这个错误,竟然提示一个莫名其妙看不懂的东东。 行,那我们先解决这个问题,将该方法修改为: fn indent(&'a mut self) -> Writer<'a> { Writer { target: self.target, indent: self.indent + 1, }\n} 怀着惴惴这心,再一次运行程序,果不其然,编译器又朝我们扔了一坨错误: error[E0308]: mismatched types --> src/main.rs:2:9 |\n2 | baz(writer.indent()); | ^^^^^^^^^^^^^^^ | | | expected `&mut Writer<'_>`, found struct `Writer` | help: consider mutably borrowing here: `&mut writer.indent()` 哦,这次错误很明显,因为baz需要&mut Writer,但是咱们writer.indent返回了一个Writer,因此修改下即可: fn bar(writer: &mut Writer) { baz(&mut writer.indent()); writer.write(\"world\");\n} 这次总该成功了吧?再次心慌慌的运行编译器,哐: error[E0623]: lifetime mismatch --> src/main.rs:2:21 |\n1 | fn bar(writer: &mut Writer) { | ----------- | | | these two types are declared with different lifetimes...\n2 | baz(&mut writer.indent()); | ^^^^^^ ...but data from `writer` flows into `writer` here 可恶,还是这个看不懂的错误,仔细检查了下代码,这次真的没有其他错误了,只能硬着头皮上。 大概的意思可以分析,生命周期范围不匹配,说明一个大一个小,然后一个writer中流入到另一个writer说明,两个writer的生命周期定义错了,既然这里提到了indent方法调用,那么我们再去仔细看一眼: impl<'a> Writer<'a> { fn indent(&'a mut self) -> Writer<'a> { Writer { target: self.target, indent: self.indent + 1, } } ...\n} 好像有点问题,indent返回的Writer的生命周期和外面调用者的Writer的生命周期一模一样,这很不合理,一眼就能看出前者远小于后者。 这里稍微展开以下,为何indent方法返回值的生命周期不能与参数中的self相同。 首先,我们假设它们可以相同,也就是上面的代码可以编译通过 ,由于此时在返回值中借用了self的可变引用,意味着 如果你在返回值被使用后,还继续使用self 会导致重复借用的错误,因为返回值的生命周期将持续到 self 结束 。 既然不能相同,那我们尝试着修改下indent: fn indent<'b>(&'b mut self) -> Writer<'b> { Writer { target: self.target, indent: self.indent + 1, }\n} Bang! 编译成功,不过稍等,回想下生命周期消除的规则,我们还可以实现的更优雅: fn bar(writer: &mut Writer) { baz(&mut writer.indent()); writer.write(\"world\");\n} fn baz(writer: &mut Writer) { writer.write(\"hello\");\n} pub struct Writer<'a> { target: &'a mut String, indent: usize,\n} impl<'a> Writer<'a> { fn indent(&mut self) -> Writer { Writer { target: self.target, indent: self.indent + 1, } } fn write(&mut self, s: &str) { for _ in 0..self.indent { self.target.push(' '); } self.target.push_str(s); self.target.push('\\n'); }\n} fn main() {} 至此,问题彻底解决,太好了,我感觉我又变强了。可是默默看了眼自己的头发,只能以哎~一声叹息结束本章内容。","breadcrumbs":"征服编译错误 » 对抗编译检查 » 生命周期 » 生命周期过大-02 » 生命周期过大-02","id":"1020","title":"生命周期过大-02"},"1021":{"body":"当涉及生命周期时,Rust 编译器有时会变得不太聪明,如果再配合循环,蠢笨都不足以形容它,不信?那继续跟着我一起看看。","breadcrumbs":"征服编译错误 » 对抗编译检查 » 生命周期 » 循环中的生命周期 » 蠢笨编译器之循环生命周期","id":"1021","title":"蠢笨编译器之循环生命周期"},"1022":{"body":"Talk is cheap, 一起来看个例子: use rand::{thread_rng, Rng}; #[derive(Debug, PartialEq)]\nenum Tile { Empty,\n} fn random_empty_tile(arr: &mut [Tile]) -> &mut Tile { loop { let i = thread_rng().gen_range(0..arr.len()); let tile = &mut arr[i]; if Tile::Empty == *tile{ return tile; } }\n} 我们来看看上面的代码中,loop循环有几个引用: arr.len(), 一个不可变引用,生命周期随着函数调用的结束而结束 tile是可变引用,生命周期在下次循环开始前会结束 根据以上的分析,可以得出个初步结论:在同一次循环间各个引用生命周期互不影响,在两次循环间,引用也互不影响。 那就简单了,开心运行,开心。。。报错: error[E0502]: cannot borrow `*arr` as immutable because it is also borrowed as mutable --> src/main.rs:10:43 |\n8 | fn random_empty_tile(arr: &mut [Tile]) -> &mut Tile { | - let's call the lifetime of this reference `'1`\n9 | loop {\n10 | let i = thread_rng().gen_range(0..arr.len()); | ^^^ immutable borrow occurs here\n11 | let tile = &mut arr[i]; | ----------- mutable borrow occurs here\n12 | if Tile::Empty == *tile{\n13 | return tile; | ---- returning this value requires that `arr[_]` is borrowed for `'1` error[E0499]: cannot borrow `arr[_]` as mutable more than once at a time --> src/main.rs:11:20 |\n8 | fn random_empty_tile(arr: &mut [Tile]) -> &mut Tile { | - let's call the lifetime of this reference `'1`\n...\n11 | let tile = &mut arr[i]; | ^^^^^^^^^^^ `arr[_]` was mutably borrowed here in the previous iteration of the loop\n12 | if Tile::Empty == *tile{\n13 | return tile; | ---- returning this value requires that `arr[_]` is borrowed for `'1` 不仅是错误,还是史诗级别的错误!无情刷屏了!只能想办法梳理下: arr.len()报错,原因是它借用了不可变引用,但是在紧跟着的&mut arr[i]中又借用了可变引用 &mut arr[i]报错,因为在上一次循环中,已经借用过同样的可变引用&mut arr[i] tile的生命周期跟arr不一致 奇了怪了,跟我们之前的分析完全背道而驰,按理来说arr.len()的借用应该在调用后立刻结束,而不是持续到后面的代码行;同时可变借用&mut arr[i]也应该随着每次循环的结束而结束,为什么会前后两次循环会因为同一处的引用而报错?","breadcrumbs":"征服编译错误 » 对抗编译检查 » 生命周期 » 循环中的生命周期 » 循环中的生命周期错误","id":"1022","title":"循环中的生命周期错误"},"1023":{"body":"虽然报错复杂,不过可以看出,所有的错误都跟tile这个中间变量有关,我们试着移除它看看: use rand::{thread_rng, Rng}; #[derive(Debug, PartialEq)]\nenum Tile { Empty,\n} fn random_empty_tile(arr: &mut [Tile]) -> &mut Tile { loop { let i = thread_rng().gen_range(0..arr.len()); if Tile::Empty == arr[i] { return &mut arr[i]; } }\n} 见证奇迹的时刻,竟然编译通过了!到底发什么了什么?仅仅移除了中间变量,就编译通过了?是否可以大胆的猜测,因为中间变量,导致编译器变蠢了,因此无法正确的识别引用的生命周期。","breadcrumbs":"征服编译错误 » 对抗编译检查 » 生命周期 » 循环中的生命周期 » 尝试去掉中间变量","id":"1023","title":"尝试去掉中间变量"},"1024":{"body":"如果不使用循环呢?会不会也有这样的错误?咱们试着把循环展开: use rand::{thread_rng, Rng}; #[derive(Debug, PartialEq)]\nenum Tile { Empty,\n} fn random_empty_tile_2<'arr>(arr: &'arr mut [Tile]) -> &'arr mut Tile { let len = arr.len(); // First loop iteration { let i = thread_rng().gen_range(0..len); let tile = &mut arr[i]; // Lifetime: 'arr if Tile::Empty == *tile { return tile; } } // Second loop iteration { let i = thread_rng().gen_range(0..len); let tile = &mut arr[i]; // Lifetime: 'arr if Tile::Empty == *tile { return tile; } } unreachable!()\n} 结果,编译器还是不给通过,报的错误几乎一样","breadcrumbs":"征服编译错误 » 对抗编译检查 » 生命周期 » 循环中的生命周期 » 循环展开","id":"1024","title":"循环展开"},"1025":{"body":"令人沮丧的是,我找遍了网上,也没有具体的原因,大家都说这是编译器太笨导致的问题,但是关于深层的原因,也没人能说出个所有然。 因此,我无法在本文中给出为什么编译器会这么笨的真实原因,如果以后有结果,会在这里进行更新。 ------2022 年 1 月 13 日更新------- 兄弟们,我带着挖掘出的一些内容回来了,再来看段错误代码先: struct A { a: i32\n} impl A { fn one(&mut self) -> &i32{ self.a = 10; &self.a } fn two(&mut self) -> &i32 { loop { let k = self.one(); if *k > 10i32 { return k; } // 可能存在的剩余代码 // ... } }\n} 我们来逐步深入分析下: 首先为two方法增加一下生命周期标识: fn two<'a>(&'a mut self) -> &'a i32 { .. }, 这里根据生命周期的 消除规则 添加的 根据生命周期标识可知:two中返回的k的生命周期必须是'a 根据第 2 条,又可知:let k = self.one();中对self的借用生命周期也是'a 因为k的借用发生在loop循环内,因此它需要小于等于循环的生命周期,但是根据之前的推断,它又要大于等于函数的生命周期'a,而函数的生命周期又大于等于循环生命周期, 由上可以推出:let k = self.one();中k的生命周期要大于等于循环的生命周期,又要小于等于循环的生命周期, 唯一满足条件的就是:k的生命周期等于循环生命周期。 但是我们的two方法在循环中对k进行了提前返回,编译器自然会认为存在其它代码,这会导致k的生命周期小于循环的生命周期。 怎么办呢?很简单: fn two(&mut self) -> &i32 { loop { let k = self.one(); return k; }\n} 不要在if分支中返回k,而是直接返回,这样就让它们的生命周期相等了,最终可以顺利编译通过。 如果一个引用值从函数的某个路径提前返回了,那么该借用必须要在函数的所有返回路径都合法","breadcrumbs":"征服编译错误 » 对抗编译检查 » 生命周期 » 循环中的生命周期 » 深层原因","id":"1025","title":"深层原因"},"1026":{"body":"虽然不能给出原因,但是我们可以看看解决办法,在上面, 移除中间变量 和 消除代码分支 都是可行的方法,还有一种方法就是将部分引用移到循环外面. 引用外移 fn random_empty_tile(arr: &mut [Tile]) -> &mut Tile { let len = arr.len(); let mut the_chosen_i = 0; loop { let i = rand::thread_rng().gen_range(0..len); let tile = &mut arr[i]; if Tile::Empty == *tile { the_chosen_i = i; break; } } &mut arr[the_chosen_i]\n} 在上面代码中,我们只在循环中保留一个可变引用,剩下的arr.len和返回值引用,都移到循环外面,顺利通过编译.","breadcrumbs":"征服编译错误 » 对抗编译检查 » 生命周期 » 循环中的生命周期 » 解决方法","id":"1026","title":"解决方法"},"1027":{"body":"再来看一个例子,代码会更复杂,但是原因几乎相同: use std::collections::HashMap; enum Symbol { A,\n} pub struct SymbolTable { scopes: Vec, current: usize,\n} struct Scope { parent: Option, symbols: HashMap,\n} impl SymbolTable { pub fn get_mut(&mut self, name: &String) -> &mut Symbol { let mut current = Some(self.current); while let Some(id) = current { let scope = self.scopes.get_mut(id).unwrap(); if let Some(symbol) = scope.symbols.get_mut(name) { return symbol; } current = scope.parent; } panic!(\"Value not found: {}\", name); }\n} 运行后报错如下: error[E0499]: cannot borrow `self.scopes` as mutable more than once at a time --> src/main.rs:22:25 |\n18 | pub fn get_mut(&mut self, name: &String) -> &mut Symbol { | - let's call the lifetime of this reference `'1`\n...\n22 | let scope = self.scopes.get_mut(id).unwrap(); | ^^^^^^^^^^^ `self.scopes` was mutably borrowed here in the previous iteration of the loop\n23 | if let Some(symbol) = scope.symbols.get_mut(name) {\n24 | return symbol; | ------ returning this value requires that `self.scopes` is borrowed for `'1` 对于上述代码,只需要将返回值修改下,即可通过编译: fn get_mut(&mut self, name: &String) -> &mut Symbol { let mut current = Some(self.current); while let Some(id) = current { let scope = self.scopes.get_mut(id).unwrap(); if scope.symbols.contains_key(name) { return self.scopes.get_mut(id).unwrap().symbols.get_mut(name).unwrap(); } current = scope.parent; } panic!(\"Value not found: {}\", name);\n} 其中的关键就在于返回的时候,新建一个引用,而不是使用中间状态的引用。","breadcrumbs":"征服编译错误 » 对抗编译检查 » 生命周期 » 循环中的生命周期 » 一个更复杂的例子","id":"1027","title":"一个更复杂的例子"},"1028":{"body":"针对现有编译器存在的各种问题,Rust 团队正在研发一个全新的编译器,名曰 polonius ,但是目前它仍然处在开发阶段,如果想在自己项目中使用,需要在rustc/RUSTFLAGS中增加标志-Zpolonius,但是可能会导致编译速度变慢,或者引入一些新的编译错误。","breadcrumbs":"征服编译错误 » 对抗编译检查 » 生命周期 » 循环中的生命周期 » 新编译器 Polonius","id":"1028","title":"新编译器 Polonius"},"1029":{"body":"编译器不是万能的,它也会迷茫,也会犯错。 因此我们在循环中使用引用类型时要格外小心,特别是涉及可变引用,这种情况下,最好的办法就是避免中间状态,或者在返回时避免使用中间状态。","breadcrumbs":"征服编译错误 » 对抗编译检查 » 生命周期 » 循环中的生命周期 » 总结","id":"1029","title":"总结"},"103":{"body":"顾名思义,字符串是由字符组成的连续集合,但是在上一节中我们提到过, Rust 中的字符是 Unicode 类型,因此每个字符占据 4 个字节内存空间,但是在字符串中不一样,字符串是 UTF-8 编码,也就是字符串中的字符所占的字节数是变化的(1 - 4) ,这样有助于大幅降低字符串所占用的内存空间。 Rust 在语言级别,只有一种字符串类型: str,它通常是以引用类型出现 &str,也就是上文提到的字符串切片。虽然语言级别只有上述的 str 类型,但是在标准库里,还有多种不同用途的字符串类型,其中使用最广的即是 String 类型。 str 类型是硬编码进可执行文件,也无法被修改,但是 String 则是一个可增长、可改变且具有所有权的 UTF-8 编码字符串, 当 Rust 用户提到字符串时,往往指的就是 String 类型和 &str 字符串切片类型,这两个类型都是 UTF-8 编码 。 除了 String 类型的字符串,Rust 的标准库还提供了其他类型的字符串,例如 OsString, OsStr, CsString 和 CsStr 等,注意到这些名字都以 String 或者 Str 结尾了吗?它们分别对应的是具有所有权和被借用的变量。","breadcrumbs":"Rust 基础入门 » 复合类型 » 字符串与切片 » 什么是字符串?","id":"103","title":"什么是字符串?"},"1030":{"body":"特征对象是一个好东西,闭包也是一个好东西,但是如果两者你都想要时,可能就会火星撞地球,boom! 至于这两者为何会勾搭到一起?考虑一个常用场景:使用闭包作为回调函数.","breadcrumbs":"征服编译错误 » 对抗编译检查 » 生命周期 » 闭包碰到特征对象-01 » 当闭包碰到特征对象 1","id":"1030","title":"当闭包碰到特征对象 1"},"1031":{"body":"如何使用闭包作为特征对象,并解决以下错误:the parameter type `impl Fn(&str) -> Res` may not live long enough","breadcrumbs":"征服编译错误 » 对抗编译检查 » 生命周期 » 闭包碰到特征对象-01 » 学习目标","id":"1031","title":"学习目标"},"1032":{"body":"在下面代码中,我们通过闭包实现了一个简单的回调函数(错误代码已经标注): pub struct Res<'a> { value: &'a str,\n} impl<'a> Res<'a> { pub fn new(value: &str) -> Res { Res { value } }\n} pub struct Container<'a> { name: &'a str, callback: Option Res>>,\n} impl<'a> Container<'a> { pub fn new(name: &str) -> Container { Container { name, callback: None, } } pub fn set(&mut self, cb: impl Fn(&str) -> Res) { self.callback = Some(Box::new(cb)); }\n} fn main() { let mut inl = Container::new(\"Inline\"); inl.set(|val| { println!(\"Inline: {}\", val); Res::new(\"inline\") }); if let Some(cb) = inl.callback { cb(\"hello, world\"); }\n} error[E0310]: the parameter type `impl Fn(&str) -> Res` may not live long enough --> src/main.rs:25:30 |\n24 | pub fn set(&mut self, cb: impl Fn(&str) -> Res) { | -------------------- help: consider adding an explicit lifetime bound...: `impl Fn(&str) -> Res + 'static`\n25 | self.callback = Some(Box::new(cb)); | ^^^^^^^^^^^^ ...so that the type `impl Fn(&str) -> Res` will meet its required lifetime bounds 从第一感觉来说,报错属实不应该,因为我们连引用都没有用,生命周期都不涉及,怎么就报错了?在继续深入之前,先来观察下该闭包是如何被使用的: callback: Option Res>>, 众所周知,闭包跟哈姆雷特一样,每一个都有 自己的类型 ,因此我们无法通过类型标注的方式来声明一个闭包,那么只有一个办法,就是使用特征对象,因此上面代码中,通过Box的方式把闭包特征封装成一个特征对象。","breadcrumbs":"征服编译错误 » 对抗编译检查 » 生命周期 » 闭包碰到特征对象-01 » 报错的代码","id":"1032","title":"报错的代码"},"1033":{"body":"事出诡异必有妖,那接下来我们一起去会会这只妖。 特征对象的生命周期 首先编译器报错提示我们闭包活得不够久,那可以大胆推测,正因为使用了闭包作为特征对象,所以才活得不够久。因此首先需要调查下特征对象的生命周期。 首先给出结论: 特征对象隐式的具有'static生命周期 。 其实在 Rust 中,'static生命周期很常见,例如一个没有引用字段的结构体它其实也是'static。当'static用于一个类型时,该类型不能包含任何非'static引用字段,例如以下结构体: struct Foo<'a> { x : &'a [u8]\n}; 除非x字段借用了'static的引用,否则'a肯定比'static要小,那么该结构体实例的生命周期肯定不是'static: 'a: 'static的限制不会被满足( HRTB )。 对于特征对象来说,它没有包含非'static的引用,因此它隐式的具有'static生命周期, Box就跟Box是等价的。 'static 闭包的限制 其实以上代码的错误很好解决,甚至编译器也提示了我们: help: consider adding an explicit lifetime bound...: `impl Fn(&str) -> Res + 'static` 但是解决问题不是本文的目标,我们还是要继续深挖一下,如果闭包使用了'static会造成什么问题。 1. 无本地变量被捕获 inl.set(|val| { println!(\"Inline: {}\", val); Res::new(\"inline\")\n}); 以上代码只使用了闭包中传入的参数,并没有本地变量被捕获,因此'static闭包一切 OK。 2. 有本地变量被捕获 let local = \"hello\".to_string(); // 编译错误: 闭包不是'static!\ninl.set(|val| { println!(\"Inline: {}\", val); println!(\"{}\", local); Res::new(\"inline\")\n}); 这里我们在闭包中捕获了本地环境变量local,因为local不是'static,那么闭包也不再是'static。 3. 将本地变量 move 进闭包 let local = \"hello\".to_string(); inl.set(move |val| { println!(\"Inline: {}\", val); println!(\"{}\", local); Res::new(\"inline\")\n}); // 编译错误: local已经被移动到闭包中,这里无法再被借用\n// println!(\"{}\", local); 如上所示,你也可以选择将本地变量的所有权move进闭包中,此时闭包再次具有'static生命周期 4. 非要捕获本地变量的引用? 对于第 2 种情况,如果非要这么干,那'static肯定是没办法了,我们只能给予闭包一个新的生命周期: pub struct Container<'a, 'b> { name: &'a str, callback: Option Res + 'b>>,\n} impl<'a, 'b> Container<'a, 'b> { pub fn new(name: &str) -> Container { Container { name, callback: None, } } pub fn set(&mut self, cb: impl Fn(&str) -> Res + 'b) { self.callback = Some(Box::new(cb)); }\n} 肉眼可见,代码复杂度哐哐哐提升,不得不说'static真香! 友情提示:由此修改引发的一系列错误,需要你自行修复: ) (再次友情小提示,可以考虑把main中的local变量声明位置挪到inl声明位置之前)","breadcrumbs":"征服编译错误 » 对抗编译检查 » 生命周期 » 闭包碰到特征对象-01 » 深入挖掘报错原因","id":"1033","title":"深入挖掘报错原因"},"1034":{"body":"其实,大家应该都知道该如何修改了,不过出于严谨,我们还是继续给出完整的正确代码: pub fn set(&mut self, cb: impl Fn(&str) -> Res + 'static) { 可能大家觉得我重新定义了完整两个字,其实是我不想水篇幅:)","breadcrumbs":"征服编译错误 » 对抗编译检查 » 生命周期 » 闭包碰到特征对象-01 » 姗姗来迟的正确代码","id":"1034","title":"姗姗来迟的正确代码"},"1035":{"body":"闭包和特征对象的相爱相杀主要原因就在于特征对象默认具备'static的生命周期,同时我们还对什么样的类型具备'static进行了简单的分析。 同时,如果一个闭包拥有'static生命周期,那闭包无法通过引用的方式来捕获本地环境中的变量。如果你想要非要捕获,只能使用非'static。","breadcrumbs":"征服编译错误 » 对抗编译检查 » 生命周期 » 闭包碰到特征对象-01 » 总结","id":"1035","title":"总结"},"1036":{"body":"本章讲述如何解决类似cannot borrow *self as mutable because it is also borrowed as immutable这种重复借用的错误。","breadcrumbs":"征服编译错误 » 对抗编译检查 » 重复借用 » 重复借用","id":"1036","title":"重复借用"},"1037":{"body":"本文将彻底解决一个困扰广大 Rust 用户已久的常见错误:因为在函数内外同时借用一个引用,导致了重复借用错误cannot borrow *self as mutable because it is also borrowed as immutable. 本文大部分内容节选自 Rust 常见陷阱 专题,由于借用是新手绕不过去的坎,因此将其提取出来形成一个新的系列","breadcrumbs":"征服编译错误 » 对抗编译检查 » 重复借用 » 同时在函数内外使用引用 » 同时在函数内外使用引用导致的重复借用错误","id":"1037","title":"同时在函数内外使用引用导致的重复借用错误"},"1038":{"body":"struct Test { a : u32, b : u32\n} impl Test { fn increase(&mut self) { let mut a = &mut self.a; let mut b = &mut self.b; *b += 1; *a += 1; }\n} 这段代码是可以正常编译的,也许有读者会有疑问,self在这里被两个变量以可变的方式借用了,明明违反了 Rust 的所有权规则,为何它不会报错? 答案要从很久很久之前开始(啊哒~~~由于我太啰嗦,被正义群众来了一下,那咱现在开始长话短说,直接进入主题)。 正确代码为何不报错? 虽然从表面来看,a和b都可变引用了self,但是 Rust 的编译器在很多时候都足够聪明,它发现我们其实仅仅引用了同一个结构体中的不同字段,因此完全可以将其的借用权分离开来。 因此,虽然我们不能同时对整个结构体进行可变引用,但是我们可以分别对结构体中的不同字段进行可变引用,当然,一个字段至多也只能存在一个可变引用,这个最基本的所有权规则还是不能违反的。变量a引用结构体字段a,变量b引用结构体字段b,从底层来说,这种方式也不会造成两个可变引用指向了同一块内存。 至此,正确代码我们已经挖掘完毕,再来看看重构后的错误代码。","breadcrumbs":"征服编译错误 » 对抗编译检查 » 重复借用 » 同时在函数内外使用引用 » 正确的代码","id":"1038","title":"正确的代码"},"1039":{"body":"struct Test { a : u32, b : u32\n} impl Test { fn increase_a (&mut self) { self.a += 1; } fn increase(&mut self) { let b = &mut self.b; self.increase_a(); *b += 1; }\n} 果然不正义的代码就是不好看,但是邪恶的它更强了吗? error[E0499]: cannot borrow `*self` as mutable more than once at a time --> src/main.rs:14:9 |\n13 | let b = &mut self.b; | ----------- first mutable borrow occurs here\n14 | self.increase_a(); | ^^^^ second mutable borrow occurs here\n15 | *b += 1; | ------- first borrow later used here 嗯,最开始提到的错误,它终于出现了。","breadcrumbs":"征服编译错误 » 对抗编译检查 » 重复借用 » 同时在函数内外使用引用 » 重构后的错误代码","id":"1039","title":"重构后的错误代码"},"104":{"body":"在之前的代码中,已经见到好几种从 &str 类型生成 String 类型的操作: String::from(\"hello,world\") \"hello,world\".to_string() 那么如何将 String 类型转为 &str 类型呢?答案很简单,取引用即可: fn main() { let s = String::from(\"hello,world!\"); say_hello(&s); say_hello(&s[..]); say_hello(s.as_str());\n} fn say_hello(s: &str) { println!(\"{}\",s);\n} 实际上这种灵活用法是因为 deref 隐式强制转换,具体我们会在 Deref 特征 进行详细讲解。","breadcrumbs":"Rust 基础入门 » 复合类型 » 字符串与切片 » String 与 &str 的转换","id":"104","title":"String 与 &str 的转换"},"1040":{"body":"为什么?明明之前还是正确的代码,就因为放入函数中就报错了?我们先从一个简单的理解谈起,当然这个理解也是浮于表面的,等会会深入分析真实的原因。 之前讲到 Rust 编译器挺聪明,可以识别到引用到不同的结构体字段,因此不会报错。但是现在这种情况下,编译器又不够聪明了,一旦放入函数中,编译器将无法理解我们对self的使用:它仅仅用到了一个字段,而不是整个结构体。 因此它会简单的认为,这个结构体作为一个整体被可变借用了,产生两个可变引用,一个引用整个结构体,一个引用了结构体字段b,这两个引用存在重叠的部分,最终导致编译错误。","breadcrumbs":"征服编译错误 » 对抗编译检查 » 重复借用 » 同时在函数内外使用引用 » 大聪明编译器","id":"1040","title":"大聪明编译器"},"1041":{"body":"在工作生活中,我们无法理解甚至错误的理解一件事,有时是因为层次不够导致的。同样,对于本文来说,也是因为我们对编译器的所知不够,才冤枉了它,还给它起了一个屈辱的“大聪明”外号。 深入分析 如果只改变相关函数的实现而不改变它的签名,那么不会影响编译的结果 何为相关函数?当函数a调用了函数b,那么b就是a的相关函数。 上面这句是一条非常重要的编译准则,意思是,对于编译器来说,只要函数签名没有变,那么任何函数实现的修改都不会影响已有的编译结果(前提是函数实现没有错误- , -)。 以前面的代码为例: fn increase_a (&mut self) { self.a += 1;\n} fn increase(&mut self) { let b = &mut self.b; self.increase_a(); *b += 1;\n} 虽然increase_a在函数实现中没有访问self.b字段,但是它的签名允许它访问b,因此违背了借用规则。事实上,该函数有没有访问b不重要, 因为编译器在这里只关心签名,签名存在可能性,那么就立刻报出错误 。 为何会有这种编译器行为,主要有两个原因: 一般来说,我们希望编译器有能力独立的编译每个函数,而无需深入到相关函数的内部实现,因为这样做会带来快得多的编译速度。 如果没有这种保证,那么在实际项目开发中,我们会特别容易遇到各种错误。 假设我们要求编译器不仅仅关注相关函数的签名,还要深入其内部关注实现,那么由于 Rust 严苛的编译规则,当你修改了某个函数内部实现的代码后,可能会引起使用该函数的其它函数的各种错误!对于大型项目来说,这几乎是不可接受的! 然后,我们的借用类型这么简单,编译器有没有可能针对这种场景,在现有的借用规则之外增加特殊规则?答案是否定的,由于 Rust 语言的设计哲学:特殊规则的加入需要慎之又慎,而我们的这种情况其实还蛮好解决的,因此编译器不会为此新增规则。","breadcrumbs":"征服编译错误 » 对抗编译检查 » 重复借用 » 同时在函数内外使用引用 » 被冤枉的编译器","id":"1041","title":"被冤枉的编译器"},"1042":{"body":"在深入分析中,我们提到一条重要的规则,要影响编译行为,就需要更改相关函数的签名,因此可以修改increase_a的签名: fn increase_a (a :&mut u32) { *a += 1;\n} fn increase(&mut self) { let b = &mut self.b; Test::increase_a(&mut self.a); *b += 1;\n} 此时,increase_a这个相关函数,不再使用&mut self作为签名,而是获取了结构体中的字段a,此时编译器又可以清晰的知道:函数increase_a和变量b分别引用了结构体中的不同字段,因此可以编译通过。 当然,除了修改相关函数的签名,你还可以修改调用者的实现: fn increase(&mut self) { self.increase_a(); self.b += 1;\n} 在这里,我们不再单独声明变量b,而是直接调用self.b+=1进行递增,根据借用生命周期 NLL 的规则,第一个可变借用self.increase_a()的生命周期随着方法调用的结束而结束,那么就不会影响self.b += 1中的借用。","breadcrumbs":"征服编译错误 » 对抗编译检查 » 重复借用 » 同时在函数内外使用引用 » 解决办法","id":"1042","title":"解决办法"},"1043":{"body":"我们再来看一个例子: use std::collections::HashMap; struct Cpu { pc: u16, cycles: u32, opcodes: HashMap,\n} struct Opcode { size: u16, cycles: u32,\n} impl Cpu { fn new() -> Cpu { Cpu { pc: 0, cycles: 0, opcodes: HashMap::from([ (0x00, Opcode::new(1, 7)), (0x01, Opcode::new(2, 6)) ]), } } fn tick(&mut self) { let address = self.pc as u8; let opcode = &self.opcodes[&address]; step(&mut self, opcode); } } fn step(cpu : &mut Cpu, opcode: &Opcode) { } impl Opcode { fn new(size: u16, cycles: u32) -> Opcode { Opcode { size, cycles } }\n} fn main() { let mut cpu = Cpu::new(); cpu.tick();\n}","breadcrumbs":"征服编译错误 » 对抗编译检查 » 重复借用 » 同时在函数内外使用引用 » CPU 模拟例子","id":"1043","title":"CPU 模拟例子"},"1044":{"body":"知其然知其所以然,要彻底解决借用导致的编译错误,我们就必须深入了解其原理,心中有剑则手中无\"贱\"。 上面的例子就留给读者朋友自己去解决,相信你以后在遇到这种常见问题时,会更加游刃有余。","breadcrumbs":"征服编译错误 » 对抗编译检查 » 重复借用 » 同时在函数内外使用引用 » 总结","id":"1044","title":"总结"},"1045":{"body":"本文将彻底解决一个困扰广大 Rust 用户已久的常见错误: 当智能指针和结构体一起使用时导致的借用错误: cannot borrowmut_s as mutable because it is also borrowed as immutable. 相信看过 <<对抗 Rust 编译检查系列>> 的读者都知道结构体中的不同字段可以独立借用吧?","breadcrumbs":"征服编译错误 » 对抗编译检查 » 重复借用 » 智能指针引起的重复借用错误 » 智能指针引起的重复借用错误","id":"1045","title":"智能指针引起的重复借用错误"},"1046":{"body":"不知道也没关系,我们这里再简单回顾一下: struct Test { a : u32, b : u32\n} impl Test { fn increase(&mut self) { let mut a = &mut self.a; let mut b = &mut self.b; *b += 1; *a += 1; }\n} 这段代码看上去像是重复借用了&mut self,违反了 Rust 的借用规则,实际上在聪明的 Rust 编译器面前,这都不是事。它能发现我们其实借用了目标结构体的不同字段,因此完全可以将其借用权分离开来。 因此,虽然我们不能同时对整个结构体进行多次可变借用,但是我们可以分别对结构体中的不同字段进行可变借用,当然,一个字段至多也只能存在一个可变借用,这个最基本的所有权规则还是不能违反的。变量a引用结构体字段a,变量b引用结构体字段b,从底层来说,这种方式也不会造成两个可变引用指向了同一块内存。","breadcrumbs":"征服编译错误 » 对抗编译检查 » 重复借用 » 智能指针引起的重复借用错误 » 结构体中的字段借用","id":"1046","title":"结构体中的字段借用"},"1047":{"body":"如果你还不知道 RefCell,可以看看 这篇文章 ,当然不看也行,简而言之,RefCell 能够实现: 将借用规则从编译期推迟到运行期,但是并不会饶过借用规则,当不符合时,程序直接panic 实现内部可变性:简单来说,对一个不可变的值进行可变借用,然后修改内部的值","breadcrumbs":"征服编译错误 » 对抗编译检查 » 重复借用 » 智能指针引起的重复借用错误 » RefCell","id":"1047","title":"RefCell"},"1048":{"body":"既然了解了结构体的借用规则和RefCell, 我们来看一段结合了两者的代码: use std::cell::RefCell;\nuse std::io::Write; struct Data { string: String,\n} struct S { data: Data, writer: Vec,\n} fn write(s: RefCell) { let mut mut_s = s.borrow_mut(); let str = &mut_s.data.string; mut_s.writer.write(str.as_bytes());\n} 以上代码从s中可变借用出结构体S,随后又对结构体中的两个字段进行了分别借用,按照之前的规则这段代码应该顺利通过编译: error[E0502]: cannot borrow `mut_s` as mutable because it is also borrowed as immutable --> src/main.rs:16:5 |\n15 | let str = &mut_s.data.string; | ----- immutable borrow occurs here\n16 | mut_s.writer.write(str.as_bytes()); | ^^^^^ --- immutable borrow later used here | | | mutable borrow occurs here 只能说,还好它报错了,否则本篇文章已经可以结束。。。错误很简单,首先对结构体S的data字段进行了不可变借用,其次又对writer字段进行了可变借用,这个符合之前的规则:对结构体不同字段分开借用,为何报错了?","breadcrumbs":"征服编译错误 » 对抗编译检查 » 重复借用 » 智能指针引起的重复借用错误 » 被 RefCell 包裹的结构体","id":"1048","title":"被 RefCell 包裹的结构体"},"1049":{"body":"第一感觉,问题是出在borrow_mut方法返回的类型上,先来看看: pub fn borrow_mut(&self) -> RefMut<'_, T> 可以看出,该方法并没有直接返回我们的结构体,而是一个RefMut类型,而要使用该类型,需要经过编译器为我们做一次隐式的Deref转换,编译器展开后的代码大概如下: use std::cell::RefMut;\nuse std::ops::{Deref, DerefMut}; fn write(s: RefCell) { let mut mut_s: RefMut = s.borrow_mut(); let str = &Deref::deref(&mut_s).data.string; DerefMut::deref_mut(&mut mut_s).writer.write(str.as_bytes());\n} 可以看出,对结构体字段的调用,实际上经过一层函数,一层函数!?我相信你应该想起了什么,是的,在 上一篇文章 中讲过类似的问题, 大意就是 编译器对于函数往往只会分析签名,并不关心内部到底如何使用结构体 。 而上面的&Deref::deref(&mut_s)和DerefMut::deref_mut(&mut mut_s)函数,签名全部使用的是结构体,并不是结构体中的某一个字段,因此对于编译器来说,该结构体明显是被重复借用了!","breadcrumbs":"征服编译错误 » 对抗编译检查 » 重复借用 » 智能指针引起的重复借用错误 » 深入分析","id":"1049","title":"深入分析"},"105":{"body":"在其它语言中,使用索引的方式访问字符串的某个字符或者子串是很正常的行为,但是在 Rust 中就会报错: let s1 = String::from(\"hello\"); let h = s1[0]; 该代码会产生如下错误: 3 | let h = s1[0]; | ^^^^^ `String` cannot be indexed by `{integer}` | = help: the trait `Index<{integer}>` is not implemented for `String` 深入字符串内部 字符串的底层的数据存储格式实际上是[ u8 ],一个字节数组。对于 let hello = String::from(\"Hola\"); 这行代码来说,Hola 的长度是 4 个字节,因为 \"Hola\" 中的每个字母在 UTF-8 编码中仅占用 1 个字节,但是对于下面的代码呢? let hello = String::from(\"中国人\"); 如果问你该字符串多长,你可能会说 3,但是实际上是 9 个字节的长度,因为大部分常用汉字在 UTF-8 中的长度是 3 个字节,因此这种情况下对 hello 进行索引,访问 &hello[0] 没有任何意义,因为你取不到 中 这个字符,而是取到了这个字符三个字节中的第一个字节,这是一个非常奇怪而且难以理解的返回值。 字符串的不同表现形式 现在看一下用梵文写的字符串 “नमस्ते”, 它底层的字节数组如下形式: [224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,\n224, 165, 135] 长度是 18 个字节,这也是计算机最终存储该字符串的形式。如果从字符的形式去看,则是: ['न', 'म', 'स', '्', 'त', 'े'] 但是这种形式下,第四和六两个字母根本就不存在,没有任何意义,接着再从字母串的形式去看: [\"न\", \"म\", \"स्\", \"ते\"] 所以,可以看出来 Rust 提供了不同的字符串展现方式,这样程序可以挑选自己想要的方式去使用,而无需去管字符串从人类语言角度看长什么样。 还有一个原因导致了 Rust 不允许去索引字符串:因为索引操作,我们总是期望它的性能表现是 O(1),然而对于 String 类型来说,无法保证这一点,因为 Rust 可能需要从 0 开始去遍历字符串来定位合法的字符。","breadcrumbs":"Rust 基础入门 » 复合类型 » 字符串与切片 » 字符串索引","id":"105","title":"字符串索引"},"1050":{"body":"因此要解决这个问题,我们得把之前的展开形式中的Deref::deref消除掉,这样没有了函数签名,编译器也将不再懒政。 既然两次Deref::deref调用都是对智能指针的自动Deref,那么可以提前手动的把它Deref了,只做一次! fn write(s: RefCell) { let mut mut_s = s.borrow_mut(); let mut tmp = &mut *mut_s; // Here let str = &tmp.data.string; tmp.writer.write(str.as_bytes());\n} 以上代码通过*对mut_s进行了解引用,获得结构体,然后又对结构体进行了可变借用&mut,最终赋予tmp变量,那么该变量就持有了我们的结构体的可变引用,而不再是持有一个智能指针。 此后对tmp的使用就回归到文章开头的那段代码:分别借用结构体的不同字段,成功通过编译! 展开代码 我们再来模拟编译器对正确的代码进行一次展开: use std::cell::RefMut;\nuse std::ops::DerefMut; fn write(s: RefCell) { let mut mut_s: RefMut = s.borrow_mut(); let tmp: &mut S = DerefMut::deref_mut(&mut mut_s); let str = &tmp.data.string; tmp.writer.write(str.as_bytes());\n} 可以看出,此时对结构体的使用不再有DerefMut::deref的身影,我们成功消除了函数边界对编译器的影响!","breadcrumbs":"征服编译错误 » 对抗编译检查 » 重复借用 » 智能指针引起的重复借用错误 » 解决方法","id":"1050","title":"解决方法"},"1051":{"body":"事实上,除了 RefCell 外,还有不少会导致这种问题的智能指针,当然原理都是互通的,我们这里就不再进行一一深入讲解,只简单列举下: Box MutexGuard(来源于 Mutex) PeekMut(来源于 BinaryHeap) RwLockWriteGuard(来源于 RwLock) String Vec Pin","breadcrumbs":"征服编译错误 » 对抗编译检查 » 重复借用 » 智能指针引起的重复借用错误 » 不仅仅是 RefCell","id":"1051","title":"不仅仅是 RefCell"},"1052":{"body":"下面再来一个练习巩固一下,强烈建议大家按照文章的思路进行分析和解决: use std::rc::Rc;\nuse std::cell::RefCell; pub struct Foo { pub foo1: Vec, pub foo2: Vec,\n}\nfn main() { let foo_cell = Rc::new(RefCell::new(Foo { foo1: vec![true, false], foo2: vec![1, 2] })); let borrow = foo_cell.borrow_mut(); let foo1 = &borrow.foo1; // 下面代码会报错,因为`foo1`和`foo2`发生了重复借用 borrow.foo2.iter_mut().enumerate().for_each(|(idx, foo2)| { if foo1[idx] { *foo2 *= -1; } });\n}","breadcrumbs":"征服编译错误 » 对抗编译检查 » 重复借用 » 智能指针引起的重复借用错误 » 一个练习","id":"1052","title":"一个练习"},"1053":{"body":"当结构体的引用穿越函数边界时,我们要格外小心,因为编译器只会对函数签名进行检查,并不关心内部到底用了结构体的哪个字段,当签名都使用了结构体时,会立即报错。 而智能指针由于隐式解引用Deref的存在,导致了两次Deref时都让结构体穿越了函数边界Deref::deref,结果造成了重复借用的错误。 解决办法就是提前对智能指针进行手动解引用,然后对内部的值进行借用后,再行使用。","breadcrumbs":"征服编译错误 » 对抗编译检查 » 重复借用 » 智能指针引起的重复借用错误 » 总结","id":"1053","title":"总结"},"1054":{"body":"","breadcrumbs":"征服编译错误 » 对抗编译检查 » 类型未限制(todo) » 类型未限制","id":"1054","title":"类型未限制"},"1055":{"body":"","breadcrumbs":"征服编译错误 » 对抗编译检查 » 幽灵数据(todo) » 幽灵数据","id":"1055","title":"幽灵数据"},"1056":{"body":"本章收录一些 Rust 常见的陷阱,一不小心就会坑你的那种(当然,这不是 Rust 语言的问题,而是一些边边角角的知识点)。","breadcrumbs":"征服编译错误 » Rust 常见陷阱 » Rust 陷阱系列","id":"1056","title":"Rust 陷阱系列"},"1057":{"body":"一般来说,for循环能做到的,while也可以,反之亦然,但是有一种情况,还真不行,先来看代码: let mut v = vec![1,2,3]; for i in 0..v.len() { v.push(i); println!(\"{:?}\",v);\n} 我们的目的是创建一个无限增长的数组,往里面插入0..(看不懂该表达式的同学请查阅 流程控制 )的数值序列。 看起来上面代码可以完成,因为随着数组不停增长,v.len()也会不停变大,但是事实上真的如此吗? [1, 2, 3, 0]\n[1, 2, 3, 0, 1]\n[1, 2, 3, 0, 1, 2] 输出很清晰的表明,只新插入了三个元素:0..=2,刚好是v的初始长度。 这是因为: 在 for 循环中,v.len只会在循环伊始之时进行求值,之后就一直使用该值 。 行,问题算是清楚了,那该如何解决呢,我们可以使用while循环,该循环与for相反,每次都会重新求值: let mut v = vec![1,2,3]; let mut i = 0;\nwhile i < v.len() { v.push(i); i+=1; println!(\"{:?}\",v);\n} 友情提示,在你运行上述代码时,千万要及时停止,否则会Boom - 炸翻控制台。","breadcrumbs":"征服编译错误 » Rust 常见陷阱 » for 循环中使用外部数组 » for 循环中使用外部数组","id":"1057","title":"for 循环中使用外部数组"},"1058":{"body":"在 Rust 中,我们不太容易遇到栈溢出,因为默认栈还挺大的,而且大的数据往往存在堆上(动态增长),但是一旦遇到该如何处理?先来看段代码: #![feature(test)]\nextern crate test; #[cfg(test)]\nmod tests { use test::Bencher; #[bench] fn it_works(b: &mut Bencher) { b.iter(|| { let stack = [[[0.0; 2]; 512]; 512]; }); }\n} 以上代码是一个测试模块,它在堆上生成了一个数组stack,初步看起来数组挺大的,先尝试运行下cargo test: 你很可能会遇到#![feature(test)]错误,因为该特性目前只存在Rust Nightly版本上,具体解决方法见 Rust 语言圣经 running 1 test thread 'tests::it_works' has overflowed its stack\nfatal runtime error: stack overflow Bang,很不幸,遇到了百年一遇的栈溢出错误,再来试试cargo bench,竟然通过了测试,这是什么原因?为何cargo test和cargo bench拥有完全不同的行为?这就要从 Rust 的栈原理讲起。 首先看看stack数组,它的大小是8 × 2 × 512 × 512 = 4 MiB,嗯,很大,崩溃也正常(读者说,正常,只是作者你不太正常。。). 其次,cargo test和cargo bench,前者运行在一个新创建的线程上,而后者运行在 main 线程上 . 最后,main线程由于是老大,所以资源比较多,拥有令其它兄弟艳羡不已的8MB栈大小,而其它新线程只有区区2MB栈大小(取决于操作系统,linux是2MB,其它的可能更小),再对比我们的stack大小,不崩溃就奇怪了。 因此,你现在明白,为何cargo test不能运行,而cargo bench却可以欢快运行。 如果实在想要增大栈的默认大小,以通过该测试,你可以这样运行:RUST_MIN_STACK=8388608 cargo test,结果如下: running 1 test\ntest tests::it_works ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Bingo, 成功了,最后再补充点测试的背景知识: cargo test为何使用新线程?因为它需要并行的运行测试用例,与之相反,cargo bench只需要顺序的执行,因此 main 线程足矣","breadcrumbs":"征服编译错误 » Rust 常见陷阱 » 线程类型导致的栈溢出 » 线程类型导致的栈溢出","id":"1058","title":"线程类型导致的栈溢出"},"1059":{"body":"在 Rust 中,溢出后的数值被截断是很正常的: let x: u16 = 65535;\nlet v = x as u8;\nprintln!(\"{}\", v) 最终程序会输出255, 因此大家可能会下意识地就觉得算数操作在 Rust 中只会导致结果的不正确,并不会导致异常。但是实际上,如果是因为算术操作符导致的溢出,就会让整个程序 panic: fn main() { let x: u8 = 10; let v = x + u8::MAX; println!(\"{}\", v)\n} 输出结果如下: thread 'main' panicked at 'attempt to add with overflow', src/main.rs:5:13 那么当我们确实有这种需求时,该如何做呢?可以使用 Rust 提供的checked_xxx系列方法: fn main() { let x: u8 = 10; let v = x.checked_add(u8::MAX).unwrap_or(0); println!(\"{}\", v)\n} 也许你会觉得本章内容其实算不上什么陷阱,但是在实际项目快速迭代中,越是不起眼的地方越是容易出错: fn main() { let v = production_rate_per_hour(5); println!(\"{}\", v);\n} pub fn production_rate_per_hour(speed: u8) -> f64 { let cph: u8 = 221; match speed { 1..=4 => (speed * cph) as f64, 5..=8 => (speed * cph) as f64 * 0.9, 9..=10 => (speed * cph) as f64 * 0.77, _ => 0 as f64, }\n} pub fn working_items_per_minute(speed: u8) -> u32 { (production_rate_per_hour(speed) / 60 as f64) as u32\n} 上述代码中,speed * cph就会直接 panic: thread 'main' panicked at 'attempt to multiply with overflow', src/main.rs:10:18 是不是还藏的挺隐蔽的?因此大家在 Rust 中做数学运算时,要多留一个心眼,免得上了生产才发现问题所在。或者,你也可以做好单元测试:)","breadcrumbs":"征服编译错误 » Rust 常见陷阱 » 算术溢出导致的 panic » 算术溢出导致的 panic","id":"1059","title":"算术溢出导致的 panic"},"106":{"body":"前文提到过,字符串切片是非常危险的操作,因为切片的索引是通过字节来进行,但是字符串又是 UTF-8 编码,因此你无法保证索引的字节刚好落在字符的边界上,例如: let hello = \"中国人\"; let s = &hello[0..2]; 运行上面的程序,会直接造成崩溃: thread 'main' panicked at 'byte index 2 is not a char boundary; it is inside '中' (bytes 0..3) of `中国人`', src/main.rs:4:14\nnote: run with `RUST_BACKTRACE=1` environment variable to display a backtrace 这里提示的很清楚,我们索引的字节落在了 中 字符的内部,这种返回没有任何意义。 因此在通过索引区间来访问字符串时, 需要格外的小心 ,一不注意,就会导致你程序的崩溃!","breadcrumbs":"Rust 基础入门 » 复合类型 » 字符串与切片 » 字符串切片","id":"106","title":"字符串切片"},"1060":{"body":"Rust 一道独特的靓丽风景就是生命周期,也是反复折磨新手的最大黑手,就连老手,可能一不注意就会遇到一些生命周期上的陷阱,例如闭包上使用引用。","breadcrumbs":"征服编译错误 » Rust 常见陷阱 » 闭包中奇怪的生命周期 » 闭包上奇怪的生命周期","id":"1060","title":"闭包上奇怪的生命周期"},"1061":{"body":"先来看一段简单的代码: fn fn_elision(x: &i32) -> &i32 { x }\nlet closure_slision = |x: &i32| -> &i32 { x }; 乍一看,这段代码比古天乐还平平无奇,能有什么问题呢?来,走两圈试试: error: lifetime may not live long enough --> src/main.rs:39:39 |\n39 | let closure = |x: &i32| -> &i32 { x }; // fails | - - ^ returning this value requires that `'1` must outlive `'2` | | | | | let's call the lifetime of this reference `'2` | let's call the lifetime of this reference `'1` 咦?竟然报错了,明明两个一模一样功能的函数,一个正常编译,一个却报错,错误原因是编译器无法推测返回的引用和传入的引用谁活得更久! 真的是非常奇怪的错误,学过 Rust 生命周期 的读者应该都记得这样一条生命周期消除规则: 如果函数参数中只有一个引用类型,那该引用的生命周期会被自动分配给所有的返回引用 。我们当前的情况完美符合,fn_elision函数的顺利编译通过,就充分说明了问题。 那为何闭包就出问题了?","breadcrumbs":"征服编译错误 » Rust 常见陷阱 » 闭包中奇怪的生命周期 » 一段简单的代码","id":"1061","title":"一段简单的代码"},"1062":{"body":"为了验证闭包无法应用生命周期消除规则,再来看一个复杂一些的例子: use std::marker::PhantomData; trait Parser<'a>: Sized + Copy { fn parse(&self, tail: &'a str) -> &'a str { tail } fn wrap(self) -> Wrapper<'a, Self> { Wrapper { parser: self, marker: PhantomData, } }\n} #[derive(Copy, Clone)]\nstruct T<'x> { int: &'x i32,\n} impl<'a, 'x> Parser<'a> for T<'x> {} struct Wrapper<'a, P>\nwhere P: Parser<'a>,\n{ parser: P, marker: PhantomData<&'a ()>,\n} fn main() { // Error. let closure_wrap = |parser: T| parser.wrap(); // No error. fn parser_wrap(parser: T<'_>) -> Wrapper<'_, T<'_>> { parser.wrap() }\n} 该例子之所以这么复杂,纯粹是为了证明闭包上生命周期会失效,读者大大轻拍:) 编译后,不出所料的报错了: error: lifetime may not live long enough --> src/main.rs:32:36 |\n32 | let closure_wrap = |parser: T| parser.wrap(); | ------ - ^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'2` | | | | | return type of closure is Wrapper<'_, T<'2>> | has type `T<'1>`","breadcrumbs":"征服编译错误 » Rust 常见陷阱 » 闭包中奇怪的生命周期 » 一段复杂的代码","id":"1062","title":"一段复杂的代码"},"1063":{"body":"一模一样的报错,说明在这种情况下,生命周期的消除规则也没有生效,看来事情确实不简单,我眉头一皱,决定深入调查,最后还真翻到了一些讨论,经过整理后,大概分享给大家。 首先给出一个结论: 这个问题,可能很难被解决,建议大家遇到后,还是老老实实用正常的函数,不要秀闭包了 。 对于函数的生命周期而言,它的消除规则之所以能生效是因为它的生命周期完全体现在签名的引用类型上,在函数体中无需任何体现: fn fn_elision(x: &i32) -> &i32 {..} 因此编译器可以做各种编译优化,也很容易根据参数和返回值进行生命周期的分析,最终得出消除规则。 可是闭包,并没有函数那么简单,它的生命周期分散在参数和闭包函数体中(主要是它没有确切的返回值签名): let closure_slision = |x: &i32| -> &i32 { x }; 编译器就必须深入到闭包函数体中,去分析和推测生命周期,复杂度因此极具提升:试想一下,编译器该如何从复杂的上下文中分析出参数引用的生命周期和闭包体中生命周期的关系? 由于上述原因(当然,实际情况复杂的多), Rust 语言开发者其实目前是有意为之,针对函数和闭包实现了两种不同的生命周期消除规则。","breadcrumbs":"征服编译错误 » Rust 常见陷阱 » 闭包中奇怪的生命周期 » 深入调查","id":"1063","title":"深入调查"},"1064":{"body":"虽然我言之凿凿,闭包的生命周期无法解决,但是未来谁又知道呢。最大的可能性就是之前开头那种简单的场景,可以被自动识别和消除。 总之,如果有这种需求,还是像古天乐一样做一个平平无奇的男人,老老实实使用函数吧。","breadcrumbs":"征服编译错误 » Rust 常见陷阱 » 闭包中奇怪的生命周期 » 总结","id":"1064","title":"总结"},"1065":{"body":"众所周知 Rust 是一门安全性非常强的系统级语言,其中,显式的设置变量可变性,是安全性的重要组成部分。按理来说,变量可变不可变在设置时就已经决定了,但是你遇到过可变变量在某些情况失效,变成不可变吗? 先来看段正确的代码: #[derive(Debug)]\nstruct A { f1: u32, f2: u32, f3: u32\n} #[derive(Debug)]\nstruct B<'a> { f1: u32, a: &'a mut A,\n} fn main() { let mut a: A = A{ f1: 0, f2: 1, f3: 2 }; // b不可变 let b: B = B{ f1: 3, a: &mut a }; // 但是b中的字段a可以变 b.a.f1 += 1; println!(\"b is {:?} \", &b);\n} 在这里,虽然变量b被设置为不可变,但是b的其中一个字段a被设置为可变的结构体,因此我们可以通过b.a.f1 += 1来修改a的值。 也许有人还不知道这种部分可变性的存在,不过没关系,因为马上就不可变了:) 结构体可变时,里面的字段都是可变的,例如&mut a 结构体不可变时,里面的某个字段可以单独设置为可变,例如b.a 在理解了上面两条简单规则后,来看看下面这段代码: #[derive(Debug)]\nstruct A { f1: u32, f2: u32, f3: u32\n} #[derive(Debug)]\nstruct B<'a> { f1: u32, a: &'a mut A,\n} impl B<'_> { // this will not work pub fn changeme(&self) { self.a.f1 += 1; }\n} fn main() { let mut a: A = A{ f1: 0, f2: 1, f3: 2 }; // b is immutable let b: B = B{ f1: 3, a: &mut a }; b.changeme(); println!(\"b is {:?} \", &b);\n} 这段代码,仅仅做了一个小改变,不再直接修改b.a,而是通过调用b上的方法去修改其中的a,按理说不会有任何区别。因此我预言:通过方法调用跟直接调用不应该有任何区别,运行验证下: error[E0594]: cannot assign to `self.a.f1`, which is behind a `&` reference --> src/main.rs:18:9 |\n17 | pub fn changeme(&self) { | ----- help: consider changing this to be a mutable reference: `&mut self`\n18 | self.a.f1 += 1; | ^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be written 啪,又被打脸了。我说我是大意了,没有闪,大家信不?反正马先生应该是信的:D","breadcrumbs":"征服编译错误 » Rust 常见陷阱 » 可变变量不可变? » 失效的可变性","id":"1065","title":"失效的可变性"},"1066":{"body":"观察第一个例子,我们调用的b.a实际上是用b的值直接调用的,在这种情况下,由于所有权规则,编译器可以认定,只有一个可变引用指向了a,因此这种使用是非常安全的。 但是,在第二个例子中,b被藏在了&后面,根据所有权规则,同时可能存在多个b的借用,那么就意味着可能会存在多个可变引用指向a,因此编译器就拒绝了这段代码。 事实上如果你将第一段代码的调用改成: let b: &B = &B{ f1: 3, a: &mut a };\nb.a.f1 += 1; 一样会报错!","breadcrumbs":"征服编译错误 » Rust 常见陷阱 » 可变变量不可变? » 简单分析","id":"1066","title":"简单分析"},"1067":{"body":"结束之前再来一个练习,稍微有点绕,大家品味品味: #[derive(Debug)]\nstruct A { f1: u32, f2: u32, f3: u32\n} #[derive(Debug)]\nstruct B<'a> { f1: u32, a: &'a mut A,\n} fn main() { let mut a: A = A{ f1: 0, f2: 1, f3: 2 }; let b: B = B{ f1: 3, a: &mut a }; b.a.f1 += 1; a.f1 = 10; println!(\"b is {:?} \", &b);\n} 小提示:这里b.a.f1 += 1和a.f1 = 10只能有一个存在,否则就会报错。","breadcrumbs":"征服编译错误 » Rust 常见陷阱 » 可变变量不可变? » 一个练习","id":"1067","title":"一个练习"},"1068":{"body":"根据之前的观察和上面的小提示,可以得出一个结论: 可变性的真正含义是你对目标对象的独占修改权 。在实际项目中,偶尔会遇到比上述代码更复杂的可变性情况,记住这个结论,有助于我们拨云见日,直达本质。 学习,就是不断接近和认识事物本质的过程,对于 Rust 语言的学习亦是如此。","breadcrumbs":"征服编译错误 » Rust 常见陷阱 » 可变变量不可变? » 总结","id":"1068","title":"总结"},"1069":{"body":"相信大家都听说过 重构一时爽,一直重构一直爽 的说法,私以为这种说法是很有道理的,不然技术团队绩效从何而来?但是,在 Rust 中,重构可能就不是那么爽快的事了,不信?咱们来看看。","breadcrumbs":"征服编译错误 » Rust 常见陷阱 » 可变借用失败引发的深入思考 » 代码重构导致的可变借用错误","id":"1069","title":"代码重构导致的可变借用错误"},"107":{"body":"由于 String 是可变字符串,下面介绍 Rust 字符串的修改,添加,删除等常用方法: 追加 (Push) 在字符串尾部可以使用 push() 方法追加字符 char,也可以使用 push_str() 方法追加字符串字面量。这两个方法都是 在原有的字符串上追加,并不会返回新的字符串 。由于字符串追加操作要修改原来的字符串,则该字符串必须是可变的,即 字符串变量必须由 mut 关键字修饰 。 示例代码如下: fn main() { let mut s = String::from(\"Hello \"); s.push_str(\"rust\"); println!(\"追加字符串 push_str() -> {}\", s); s.push('!'); println!(\"追加字符 push() -> {}\", s);\n} 代码运行结果: 追加字符串 push_str() -> Hello rust\n追加字符 push() -> Hello rust! 插入 (Insert) 可以使用 insert() 方法插入单个字符 char,也可以使用 insert_str() 方法插入字符串字面量,与 push() 方法不同,这俩方法需要传入两个参数,第一个参数是字符(串)插入位置的索引,第二个参数是要插入的字符(串),索引从 0 开始计数,如果越界则会发生错误。由于字符串插入操作要 修改原来的字符串 ,则该字符串必须是可变的,即 字符串变量必须由 mut 关键字修饰 。 示例代码如下: fn main() { let mut s = String::from(\"Hello rust!\"); s.insert(5, ','); println!(\"插入字符 insert() -> {}\", s); s.insert_str(6, \" I like\"); println!(\"插入字符串 insert_str() -> {}\", s);\n} 代码运行结果: 插入字符 insert() -> Hello, rust!\n插入字符串 insert_str() -> Hello, I like rust! 替换 (Replace) 如果想要把字符串中的某个字符串替换成其它的字符串,那可以使用 replace() 方法。与替换有关的方法有三个。 1、replace 该方法可适用于 String 和 &str 类型。replace() 方法接收两个参数,第一个参数是要被替换的字符串,第二个参数是新的字符串。该方法会替换所有匹配到的字符串。 该方法是返回一个新的字符串,而不是操作原来的字符串 。 示例代码如下: fn main() { let string_replace = String::from(\"I like rust. Learning rust is my favorite!\"); let new_string_replace = string_replace.replace(\"rust\", \"RUST\"); dbg!(new_string_replace);\n} 代码运行结果: new_string_replace = \"I like RUST. Learning RUST is my favorite!\" 2、replacen 该方法可适用于 String 和 &str 类型。replacen() 方法接收三个参数,前两个参数与 replace() 方法一样,第三个参数则表示替换的个数。 该方法是返回一个新的字符串,而不是操作原来的字符串 。 示例代码如下: fn main() { let string_replace = \"I like rust. Learning rust is my favorite!\"; let new_string_replacen = string_replace.replacen(\"rust\", \"RUST\", 1); dbg!(new_string_replacen);\n} 代码运行结果: new_string_replacen = \"I like RUST. Learning rust is my favorite!\" 3、replace_range 该方法仅适用于 String 类型。replace_range 接收两个参数,第一个参数是要替换字符串的范围(Range),第二个参数是新的字符串。 该方法是直接操作原来的字符串,不会返回新的字符串。该方法需要使用 mut 关键字修饰 。 示例代码如下: fn main() { let mut string_replace_range = String::from(\"I like rust!\"); string_replace_range.replace_range(7..8, \"R\"); dbg!(string_replace_range);\n} 代码运行结果: string_replace_range = \"I like Rust!\" 删除 (Delete) 与字符串删除相关的方法有 4 个,他们分别是 pop(),remove(),truncate(),clear()。这四个方法仅适用于 String 类型。 1、 pop —— 删除并返回字符串的最后一个字符 该方法是直接操作原来的字符串 。但是存在返回值,其返回值是一个 Option 类型,如果字符串为空,则返回 None。 示例代码如下: fn main() { let mut string_pop = String::from(\"rust pop 中文!\"); let p1 = string_pop.pop(); let p2 = string_pop.pop(); dbg!(p1); dbg!(p2); dbg!(string_pop);\n} 代码运行结果: p1 = Some( '!',\n)\np2 = Some( '文',\n)\nstring_pop = \"rust pop 中\" 2、 remove —— 删除并返回字符串中指定位置的字符 该方法是直接操作原来的字符串 。但是存在返回值,其返回值是删除位置的字符串,只接收一个参数,表示该字符起始索引位置。remove() 方法是按照字节来处理字符串的,如果参数所给的位置不是合法的字符边界,则会发生错误。 示例代码如下: fn main() { let mut string_remove = String::from(\"测试remove方法\"); println!( \"string_remove 占 {} 个字节\", std::mem::size_of_val(string_remove.as_str()) ); // 删除第一个汉字 string_remove.remove(0); // 下面代码会发生错误 // string_remove.remove(1); // 直接删除第二个汉字 // string_remove.remove(3); dbg!(string_remove);\n} 代码运行结果: string_remove 占 18 个字节\nstring_remove = \"试remove方法\" 3、truncate —— 删除字符串中从指定位置开始到结尾的全部字符 该方法是直接操作原来的字符串 。无返回值。该方法 truncate() 方法是按照字节来处理字符串的,如果参数所给的位置不是合法的字符边界,则会发生错误。 示例代码如下: fn main() { let mut string_truncate = String::from(\"测试truncate\"); string_truncate.truncate(3); dbg!(string_truncate);\n} 代码运行结果: string_truncate = \"测\" 4、clear —— 清空字符串 该方法是直接操作原来的字符串 。调用后,删除字符串中的所有字符,相当于 truncate() 方法参数为 0 的时候。 示例代码如下: fn main() { let mut string_clear = String::from(\"string clear\"); string_clear.clear(); dbg!(string_clear);\n} 代码运行结果: string_clear = \"\" 连接 (Concatenate) 1、使用 + 或者 += 连接字符串 使用 + 或者 += 连接字符串,要求右边的参数必须为字符串的切片引用(Slice)类型。其实当调用 + 的操作符时,相当于调用了 std::string 标准库中的 add() 方法,这里 add() 方法的第二个参数是一个引用的类型。因此我们在使用 +, 必须传递切片引用类型。不能直接传递 String 类型。 + 是返回一个新的字符串,所以变量声明可以不需要 mut 关键字修饰 。 示例代码如下: fn main() { let string_append = String::from(\"hello \"); let string_rust = String::from(\"rust\"); // &string_rust会自动解引用为&str let result = string_append + &string_rust; let mut result = result + \"!\"; // `result + \"!\"` 中的 `result` 是不可变的 result += \"!!!\"; println!(\"连接字符串 + -> {}\", result);\n} 代码运行结果: 连接字符串 + -> hello rust!!!! add() 方法的定义: fn add(self, s: &str) -> String 因为该方法涉及到更复杂的特征功能,因此我们这里简单说明下: fn main() { let s1 = String::from(\"hello,\"); let s2 = String::from(\"world!\"); // 在下句中,s1的所有权被转移走了,因此后面不能再使用s1 let s3 = s1 + &s2; assert_eq!(s3,\"hello,world!\"); // 下面的语句如果去掉注释,就会报错 // println!(\"{}\",s1);\n} self 是 String 类型的字符串 s1,该函数说明,只能将 &str 类型的字符串切片添加到 String 类型的 s1 上,然后返回一个新的 String 类型,所以 let s3 = s1 + &s2; 就很好解释了,将 String 类型的 s1 与 &str 类型的 s2 进行相加,最终得到 String 类型的 s3。 由此可推,以下代码也是合法的: let s1 = String::from(\"tic\");\nlet s2 = String::from(\"tac\");\nlet s3 = String::from(\"toe\"); // String = String + &str + &str + &str + &str\nlet s = s1 + \"-\" + &s2 + \"-\" + &s3; String + &str返回一个 String,然后再继续跟一个 &str 进行 + 操作,返回一个 String 类型,不断循环,最终生成一个 s,也是 String 类型。 s1 这个变量通过调用 add() 方法后,所有权被转移到 add() 方法里面, add() 方法调用后就被释放了,同时 s1 也被释放了。再使用 s1 就会发生错误。这里涉及到 所有权转移(Move) 的相关知识。 2、使用 format! 连接字符串 format! 这种方式适用于 String 和 &str 。format! 的用法与 print! 的用法类似,详见 格式化输出 。 示例代码如下: fn main() { let s1 = \"hello\"; let s2 = String::from(\"rust\"); let s = format!(\"{} {}!\", s1, s2); println!(\"{}\", s);\n} 代码运行结果: hello rust!","breadcrumbs":"Rust 基础入门 » 复合类型 » 字符串与切片 » 操作字符串","id":"107","title":"操作字符串"},"1070":{"body":"很多时候,错误也是一种美,但是当这种错误每天都能见到时(呕): error[e0499]: cannot borrow ` * self` as mutable more than once at a time; 虽然这一类错误长得一样,但是我这里的错误可能并不是大家常遇到的那些妖艳错误,废话不多说,一起来看看。","breadcrumbs":"征服编译错误 » Rust 常见陷阱 » 可变借用失败引发的深入思考 » 欣赏下报错","id":"1070","title":"欣赏下报错"},"1071":{"body":"struct Test { a : u32, b : u32\n} impl Test { fn increase(&mut self) { let mut a = &mut self.a; let mut b = &mut self.b; *b += 1; *a += 1; }\n} 这段代码是可以正常编译的,也许有读者会有疑问,self在这里被两个变量以可变的方式借用了,明明违反了 Rust 的所有权规则,为何它不会报错? 答案要从很久很久之前开始(啊哒~~~由于我太啰嗦,被正义群众来了一下,那咱现在开始长话短说,直接进入主题)。 正确代码为何不报错? 虽然从表面来看,a和b都可变引用了self,但是 Rust 的编译器在很多时候都足够聪明,它发现我们其实仅仅引用了同一个结构体中的不同字段,因此完全可以将其的借用权分离开来。 因此,虽然我们不能同时对整个结构体进行可变引用,但是我们可以分别对结构体中的不同字段进行可变引用,当然,一个字段至多也只能存在一个可变引用,这个最基本的所有权规则还是不能违反的。变量a引用结构体字段a,变量b引用结构体字段b,从底层来说,这种方式也不会造成两个可变引用指向了同一块内存。 至此,正确代码我们已经挖掘完毕,再来看看重构后的错误代码。","breadcrumbs":"征服编译错误 » Rust 常见陷阱 » 可变借用失败引发的深入思考 » 重构前的正确代码","id":"1071","title":"重构前的正确代码"},"1072":{"body":"由于领导说我们这个函数没办法复用,那就敷衍一下呗: struct Test { a : u32, b : u32\n} impl Test { fn increase_a (&mut self) { self.a += 1; } fn increase(&mut self) { let b = &mut self.b; self.increase_a(); *b += 1; }\n} 既然领导说了,咱照做,反正他也没说怎么个复用法,咱就来个简单的,把a的递增部分复用下。 代码说实话。。。更丑了,但是更强了吗? error[E0499]: cannot borrow `*self` as mutable more than once at a time --> src/main.rs:14:9 |\n13 | let b = &mut self.b; | ----------- first mutable borrow occurs here\n14 | self.increase_a(); | ^^^^ second mutable borrow occurs here\n15 | *b += 1; | ------- first borrow later used here 嗯,最开始提到的错误,它终于出现了。","breadcrumbs":"征服编译错误 » Rust 常见陷阱 » 可变借用失败引发的深入思考 » 重构后的错误代码","id":"1072","title":"重构后的错误代码"},"1073":{"body":"为什么?明明之前还是正确的代码,就因为放入函数中就报错了?我们先从一个简单的理解谈起,当然这个理解也是浮于表面的,等会会深入分析真实的原因。 之前讲到 Rust 编译器挺聪明,可以识别到引用到不同的结构体字段,因此不会报错。但是现在这种情况下,编译器又不够聪明了,一旦放入函数中,编译器将无法理解我们对self的使用:它仅仅用到了一个字段,而不是整个结构体。 因此它会简单的认为,这个结构体作为一个整体被可变借用了,产生两个可变引用,一个引用整个结构体,一个引用了结构体字段b,这两个引用存在重叠的部分,最终导致编译错误。","breadcrumbs":"征服编译错误 » Rust 常见陷阱 » 可变借用失败引发的深入思考 » 大聪明编译器","id":"1073","title":"大聪明编译器"},"1074":{"body":"在工作生活中,我们无法理解甚至错误的理解一件事,有时是因为层次不够导致的。同样,对于本文来说,也是因为我们对编译器的所知不够,才冤枉了它,还给它起了一个屈辱的“大聪明”外号。 深入分析 如果只改变相关函数的实现而不改变它的签名,那么不会影响编译的结果 何为相关函数?当函数a调用了函数b,那么b就是a的相关函数。 上面这句是一条非常重要的编译准则,意思是,对于编译器来说,只要函数签名没有变,那么任何函数实现的修改都不会影响已有的编译结果(前提是函数实现没有错误- , -)。 以前面的代码为例: fn increase_a (&mut self) { self.a += 1;\n} fn increase(&mut self) { let b = &mut self.b; self.increase_a(); *b += 1;\n} 虽然increase_a在函数实现中没有访问self.b字段,但是它的签名允许它访问b,因此违背了借用规则。事实上,该函数有没有访问b不重要, 因为编译器在这里只关心签名,签名存在可能性,那么就立刻报出错误 。 为何会有这种编译器行为,主要有两个原因: 一般来说,我们希望编译器有能力独立的编译每个函数,而无需深入到相关函数的内部实现,因为这样做会带来快得多的编译速度。 如果没有这种保证,那么在实际项目开发中,我们会特别容易遇到各种错误。 假设我们要求编译器不仅仅关注相关函数的签名,还要深入其内部关注实现,那么由于 Rust 严苛的编译规则,当你修改了某个函数内部实现的代码后,可能会引起使用该函数的其它函数的各种错误!对于大型项目来说,这几乎是不可接受的! 然后,我们的借用类型这么简单,编译器有没有可能针对这种场景,在现有的借用规则之外增加特殊规则?答案是否定的,由于 Rust 语言的设计哲学:特殊规则的加入需要慎之又慎,而我们的这种情况其实还蛮好解决的,因此编译器不会为此新增规则。","breadcrumbs":"征服编译错误 » Rust 常见陷阱 » 可变借用失败引发的深入思考 » 被冤枉的编译器","id":"1074","title":"被冤枉的编译器"},"1075":{"body":"在深入分析中,我们提到一条重要的规则,要影响编译行为,就需要更改相关函数的签名,因此可以修改increase_a的签名: fn increase_a (a :&mut u32) { *a += 1;\n} fn increase(&mut self) { let b = &mut self.b; Test::increase_a(&mut self.a); *b += 1;\n} 此时,increase_a这个相关函数,不再使用&mut self作为签名,而是获取了结构体中的字段a,此时编译器又可以清晰的知道:函数increase_a和变量b分别引用了结构体中的不同字段,因此可以编译通过。 当然,除了修改相关函数的签名,你还可以修改调用者的实现: fn increase(&mut self) { self.increase_a(); self.b += 1;\n} 在这里,我们不再单独声明变量b,而是直接调用self.b+=1进行递增,根据借用生命周期 NLL 的规则,第一个可变借用self.increase_a()的生命周期随着方法调用的结束而结束,那么就不会影响self.b += 1中的借用。","breadcrumbs":"征服编译错误 » Rust 常见陷阱 » 可变借用失败引发的深入思考 » 解决办法","id":"1075","title":"解决办法"},"1076":{"body":"再来看一个使用了闭包的例子: use tokio::runtime::Runtime; struct Server { number_of_connections : u64\n} impl Server { pub fn new() -> Self { Server { number_of_connections : 0} } pub fn increase_connections_count(&mut self) { self.number_of_connections += 1; }\n} struct ServerRuntime { runtime: Runtime, server: Server\n} impl ServerRuntime { pub fn new(runtime: Runtime, server: Server) -> Self { ServerRuntime { runtime, server } } pub fn increase_connections_count(&mut self) { self.runtime.block_on(async { self.server.increase_connections_count() }) }\n} 代码中使用了tokio,在increase_connections_count函数中启动了一个异步任务,并且等待它的完成。这个函数中分别引用了self中的不同字段:runtime和server,但是可能因为闭包的原因,编译器没有像本文最开始的例子中那样聪明,并不能识别这两个引用仅仅引用了同一个结构体的不同部分,因此报错了: error[E0501]: cannot borrow `self.runtime` as mutable because previous closure requires unique access --> the_little_things\\src\\main.rs:28:9 |\n28 | self.runtime.block_on(async { | __________^____________--------_______- | | | | | | _________| first borrow later used by call | ||\n29 | || self.server.increase_connections_count() | || ---- first borrow occurs due to use of `self` in generator\n30 | || }) | ||_________-^ second borrow occurs here | |__________| | generator construction occurs here 解决办法 解决办法很粗暴,既然编译器不能理解闭包中的引用是不同的,那么我们就主动告诉它: pub fn increase_connections_count(&mut self) { let runtime = &mut self.runtime; let server = &mut self.server; runtime.block_on(async { server.increase_connections_count() })\n} 上面通过变量声明的方式,在闭包外声明了两个变量分别引用结构体self的不同字段,这样一来,编译器就不会那么笨,编译顺利通过。 你也可以这么写: pub fn increase_connections_count(&mut self) { let ServerRuntime { runtime, server } = self; runtime.block_on(async { server.increase_connections_count() })\n} 当然,如果难以解决,还有一个笨办法,那就是将server和runtime分离开来,不要放在一个结构体中。","breadcrumbs":"征服编译错误 » Rust 常见陷阱 » 可变借用失败引发的深入思考 » 闭包中的例子","id":"1076","title":"闭包中的例子"},"1077":{"body":"心中有剑,手中无剑,是武学至高境界。 本文列出的那条编译规则,在未来就将是大家心中的那把剑,当这些心剑招式足够多时,量变产生质变,终将天下无敌。","breadcrumbs":"征服编译错误 » Rust 常见陷阱 » 可变借用失败引发的深入思考 » 总结","id":"1077","title":"总结"},"1078":{"body":"迭代器,在 Rust 中是一个非常耀眼的存在,它光鲜亮丽,它让 Rust 大道至简,它备受用户的喜爱。可是,它也是懒惰的,不信?一起来看看。","breadcrumbs":"征服编译错误 » Rust 常见陷阱 » 不太勤快的迭代器 » 不太勤快的迭代器","id":"1078","title":"不太勤快的迭代器"},"1079":{"body":"在迭代器学习中,我们提到过迭代器在功能上可以替代循环,性能上略微优于循环(避免边界检查),安全性上优于循环,因此在 Rust 中,迭代器往往都是更优的选择,前提是迭代器得发挥作用。 在下面代码中,分别是使用for循环和迭代器去生成一个HashMap。 使用循环: use std::collections::HashMap;\n#[derive(Debug)]\nstruct Account { id: u32,\n} fn main() { let accounts = [Account { id: 1 }, Account { id: 2 }, Account { id: 3 }]; let mut resolvers = HashMap::new(); for a in accounts { resolvers.entry(a.id).or_insert(Vec::new()).push(a); } println!(\"{:?}\",resolvers);\n} 使用迭代器: let mut resolvers = HashMap::new();\naccounts.into_iter().map(|a| { resolvers .entry(a.id) .or_insert(Vec::new()) .push(a);\n});\nprintln!(\"{:?}\",resolvers); 预料之外的结果 两端代码乍一看(很多时候我们快速浏览代码的时候,不会去细看)都很正常, 运行下试试: for循环很正常,输出{2: [Account { id: 2 }], 1: [Account { id: 1 }], 3: [Account { id: 3 }]} 迭代器很。。。不正常,输出了一个{}, 黑人问号? ? ? 在继续深挖之前,我们先来简单回顾下迭代器。","breadcrumbs":"征服编译错误 » Rust 常见陷阱 » 不太勤快的迭代器 » for 循环 vs 迭代器","id":"1079","title":"for 循环 vs 迭代器"},"108":{"body":"我们可以通过转义的方式 \\ 输出 ASCII 和 Unicode 字符。 fn main() { // 通过 \\ + 字符的十六进制表示,转义输出一个字符 let byte_escape = \"I'm writing \\x52\\x75\\x73\\x74!\"; println!(\"What are you doing\\x3F (\\\\x3F means ?) {}\", byte_escape); // \\u 可以输出一个 unicode 字符 let unicode_codepoint = \"\\u{211D}\"; let character_name = \"\\\"DOUBLE-STRUCK CAPITAL R\\\"\"; println!( \"Unicode character {} (U+211D) is called {}\", unicode_codepoint, character_name ); // 换行了也会保持之前的字符串格式 let long_string = \"String literals can span multiple lines. The linebreak and indentation here ->\\ <- can be escaped too!\"; println!(\"{}\", long_string);\n} 当然,在某些情况下,可能你会希望保持字符串的原样,不要转义: fn main() { println!(\"{}\", \"hello \\\\x52\\\\x75\\\\x73\\\\x74\"); let raw_str = r\"Escapes don't work here: \\x3F \\u{211D}\"; println!(\"{}\", raw_str); // 如果字符串包含双引号,可以在开头和结尾加 # let quotes = r#\"And then I said: \"There is no escape!\"\"#; println!(\"{}\", quotes); // 如果还是有歧义,可以继续增加,没有限制 let longer_delimiter = r###\"A string with \"# in it. And even \"##!\"###; println!(\"{}\", longer_delimiter);\n}","breadcrumbs":"Rust 基础入门 » 复合类型 » 字符串与切片 » 字符串转义","id":"108","title":"字符串转义"},"1080":{"body":"在迭代器章节中,我们曾经提到过,迭代器的 适配器 分为两种:消费者适配器和迭代器适配器,前者用来将一个迭代器变为指定的集合类型,往往通过collect实现;后者用于生成一个新的迭代器,例如上例中的map。 还提到过非常重要的一点: 迭代器适配器都是懒惰的,只有配合消费者适配器使用时,才会进行求值 .","breadcrumbs":"征服编译错误 » Rust 常见陷阱 » 不太勤快的迭代器 » 回顾下迭代器","id":"1080","title":"回顾下迭代器"},"1081":{"body":"在我们之前的迭代器示例中,只有一个迭代器适配器map: accounts.into_iter().map(|a| { resolvers .entry(a.id) .or_insert(Vec::new()) .push(a);\n}); 首先, accounts被拿走所有权后转换成一个迭代器,其次该迭代器通过map方法生成一个新的迭代器,最后,在此过程中没有以类如collect的消费者适配器收尾。 因此在上述过程中,map完全是懒惰的,它没有做任何事情,它在等一个消费者适配器告诉它:赶紧起床,任务可以开始了,它才会开始行动。 自然,我们的插值计划也失败了。 事实上,IDE 和编译器都会对这种代码给出警告:iterators are lazy and do nothing unless consumed","breadcrumbs":"征服编译错误 » Rust 常见陷阱 » 不太勤快的迭代器 » 懒惰是根因","id":"1081","title":"懒惰是根因"},"1082":{"body":"原因非常清晰,如果读者还有疑惑,建议深度了解下上面给出的迭代器链接,我们这里就不再赘述。 下面列出三种合理的解决办法: 不再使用迭代器适配器map,改成for_each: let mut resolvers = HashMap::new();\naccounts.into_iter().for_each(|a| { resolvers .entry(a.id) .or_insert(Vec::new()) .push(a);\n}); 但是,相关的文档也友善的提示了我们,除非作为链式调用的收尾,否则更建议使用for循环来处理这种情况。哎,忙忙碌碌,又回到了原点,不禁让人感叹:天道有轮回。 使用消费者适配器collect来收尾,将map产生的迭代器收集成一个集合类型: let resolvers: HashMap<_, _> = accounts\n.into_iter()\n.map(|a| (a.id, a))\n.collect(); 嗯,还挺简洁,挺rusty. 使用fold,语义表达更强: let resolvers = account.into_iter().fold(HashMap::new(), |mut resolvers, a|{ resolvers.entry(a.id).or_insert(Vec::new()).push(a); resolvers\n});","breadcrumbs":"征服编译错误 » Rust 常见陷阱 » 不太勤快的迭代器 » 解决办法","id":"1082","title":"解决办法"},"1083":{"body":"在使用迭代器时,要清晰的认识到需要用到的方法是迭代型还是消费型适配器,如果一个调用链中没有以消费型适配器结尾,就需要打起精神了,也许,不远处就是一个陷阱在等你跳:)","breadcrumbs":"征服编译错误 » Rust 常见陷阱 » 不太勤快的迭代器 » 总结","id":"1083","title":"总结"},"1084":{"body":"@todo https://www.reddit.com/r/rust/comments/rrgxr0/a_critique_of_rusts_range_types/","breadcrumbs":"征服编译错误 » Rust 常见陷阱 » 奇怪的序列 x..y » 奇怪的序列x..y","id":"1084","title":"奇怪的序列x..y"},"1085":{"body":"Rust 的迭代器无处不在,直至你在它上面栽了跟头,经过深入调查才发现:哦,原来是迭代器的锅。不信的话,看看这个报错你能想到是迭代器的问题吗: borrow of moved value: words.","breadcrumbs":"征服编译错误 » Rust 常见陷阱 » 无处不在的迭代器 » 无处不在的迭代器","id":"1085","title":"无处不在的迭代器"},"1086":{"body":"以下的代码非常简单,用来统计文本中字词的数量,并打印出来: fn main() { let s = \"hello world\"; let mut words = s.split(\" \"); let n = words.count(); println!(\"{:?}\",words);\n} 四行代码,行云流水,一气呵成,且看成效: error[E0382]: borrow of moved value: `words` --> src/main.rs:5:21 |\n3 | let mut words = s.split(\" \"); | --------- move occurs because `words` has type `std::str::Split<'_, &str>`, which does not implement the `Copy` trait\n4 | let n = words.count(); | ------- `words` moved due to this method call\n5 | println!(\"{:?}\",words); | ^^^^^ value borrowed here after move 世事难料,我以为只有的生命周期、闭包才容易背叛革命,没想到一个你浓眉大眼的count方法也背叛革命。从报错来看,是因为count方法拿走了words的所有权,来看看签名: fn count(self) -> usize 从签名来看,编译器的报错是正确的,但是为什么?为什么一个简单的标准库count方法就敢拿走所有权?","breadcrumbs":"征服编译错误 » Rust 常见陷阱 » 无处不在的迭代器 » 报错的代码","id":"1086","title":"报错的代码"},"1087":{"body":"在 迭代器 章节中,我们曾经学习过两个概念:迭代器适配器和消费者适配器,前者用于对迭代器中的元素进行操作,最终生成一个新的迭代器,例如map、filter等方法;而后者用于消费掉迭代器,最终产生一个结果,例如collect方法, 一个典型的示例如下: let v1: Vec = vec![1, 2, 3]; let v2: Vec<_> = v1.iter().map(|x| x + 1).collect(); assert_eq!(v2, vec![2, 3, 4]); 在其中,我们还提到一个细节,消费者适配器会拿走迭代器的所有权,那么这个是否与我们最开始碰到的问题有关系?","breadcrumbs":"征服编译错误 » Rust 常见陷阱 » 无处不在的迭代器 » 迭代器回顾","id":"1087","title":"迭代器回顾"},"1088":{"body":"要解释这个问题,必须要找到words是消费者适配器的证据,因此我们需要深入源码进行查看。 其实。。也不需要多深,只要进入words的源码,就能看出它属于Iterator特征,那说明split方法产生了一个迭代器?再来看看: pub fn split<'a, P>(&'a self, pat: P) -> Split<'a, P>\nwhere P: Pattern<'a>,\n//An iterator over substrings of this string slice, separated by characters matched by a pattern. 还真是,从代码注释来看,Split就是一个迭代器类型,用来迭代被分隔符隔开的子字符串集合。 真相大白了,split产生一个迭代器,而count方法是一个消费者适配器,用于消耗掉前者产生的迭代器,最终生成字词统计的结果。 本身问题不复杂,但是在 解决方法上,可能还有点在各位客官的意料之外 ,且看下文。","breadcrumbs":"征服编译错误 » Rust 常见陷阱 » 无处不在的迭代器 » 深入调查","id":"1088","title":"深入调查"},"1089":{"body":"你可能会想用collect来解决这个问题,先收集成一个集合,然后进行统计。当然此方法完全可行,但是很不rusty(很符合 rust 规范、潮流的意思),以下给出最rusty的解决方案: let words = s.split(\",\");\nlet n = words.clone().count(); 在继续之前,我得先找一个地方藏好,因为俺有一个感觉,烂西红柿正在铺天盖地的呼啸而来,伴随而来的是读者的正义呵斥: 你管clone叫最好、最rusty的解决方法?? 大家且听我慢慢道来,事实上,在 Rust 中clone不总是性能低下的代名词,因为clone的行为完全取决于它的具体实现。 迭代器的clone代价 对于迭代器而言,它其实并不需要持有数据才能进行迭代,事实上它包含一个引用,该引用指向了保存在堆上的数据,而迭代器自身的结构是保存在栈上。 因此对迭代器的clone仅仅是复制了一份栈上的简单结构,性能非常高效,例如: pub struct Split<'a, T: 'a, P>\nwhere P: FnMut(&T) -> bool,\n{ // Used for `SplitWhitespace` and `SplitAsciiWhitespace` `as_str` methods pub(crate) v: &'a [T], pred: P, // Used for `SplitAsciiWhitespace` `as_str` method pub(crate) finished: bool,\n} impl Clone for Split<'_, T, P>\nwhere P: Clone + FnMut(&T) -> bool,\n{ fn clone(&self) -> Self { Split { v: self.v, pred: self.pred.clone(), finished: self.finished } }\n} 以上代码实现了对Split迭代器的克隆,可以看出,底层的的数组self.v并没有被克隆而是简单的复制了一个引用,依然指向了底层的数组&[T],因此这个克隆非常高效。","breadcrumbs":"征服编译错误 » Rust 常见陷阱 » 无处不在的迭代器 » 最 rusty 的解决方法","id":"1089","title":"最 rusty 的解决方法"},"109":{"body":"前文提到了几种使用 UTF-8 字符串的方式,下面来一一说明。 字符 如果你想要以 Unicode 字符的方式遍历字符串,最好的办法是使用 chars 方法,例如: for c in \"中国人\".chars() { println!(\"{}\", c);\n} 输出如下 中\n国\n人 字节 这种方式是返回字符串的底层字节数组表现形式: for b in \"中国人\".bytes() { println!(\"{}\", b);\n} 输出如下: 228\n184\n173\n229\n155\n189\n228\n186\n186 获取子串 想要准确的从 UTF-8 字符串中获取子串是较为复杂的事情,例如想要从 holla中国人नमस्ते 这种变长的字符串中取出某一个子串,使用标准库你是做不到的。 你需要在 crates.io 上搜索 utf8 来寻找想要的功能。 可以考虑尝试下这个库: utf8_slice 。","breadcrumbs":"Rust 基础入门 » 复合类型 » 字符串与切片 » 操作 UTF-8 字符串","id":"109","title":"操作 UTF-8 字符串"},"1090":{"body":"看起来是无效借用导致的错误,实际上是迭代器被消费了导致的问题,这说明 Rust 编译器虽然会告诉你错误原因,但是这个原因不总是根本原因。我们需要一双慧眼和勤劳的手,来挖掘出这个宝藏,最后为己所用。 同时,克隆在 Rust 中也并不总是 bad guy 的代名词,有的时候我们可以大胆去使用,当然前提是了解你的代码场景和具体的clone实现,这样你也能像文中那样作出非常rusty的选择。","breadcrumbs":"征服编译错误 » Rust 常见陷阱 » 无处不在的迭代器 » 总结","id":"1090","title":"总结"},"1091":{"body":"本篇陷阱较短,主要解决新手在多线程间传递消息时可能会遇到的一个问题:主线程会一直阻塞,无法结束。 Rust 标准库中提供了一个消息通道,非常好用,也相当简单明了,但是但是在使用起来还是可能存在坑: use std::sync::mpsc;\nfn main() { use std::thread; let (send, recv) = mpsc::channel(); let num_threads = 3; for i in 0..num_threads { let thread_send = send.clone(); thread::spawn(move || { thread_send.send(i).unwrap(); println!(\"thread {:?} finished\", i); }); } for x in recv { println!(\"Got: {}\", x); } println!(\"finished iterating\");\n} 以上代码看起来非常正常,运行下试试: thread 0 finished\nthread 1 finished\nGot: 0\nGot: 1\nthread 2 finished\nGot: 2 奇怪,主线程竟然卡死了,最后一行 println!(\"finished iterating\");一直没有被输出。 其实,上面的描述有问题,主线程并不是卡死,而是for循环并没有结束,至于for循环不结束的原因是消息通道没有被关闭。 回忆一下 Rust 消息通道关闭的两个条件:所有发送者全部被drop或接收者被drop,由于for循环还在使用接收者,因为后者条件无法被满足,那么只能发送者全部被drop,才能让例子中的消息通道关闭。 来分析下代码,每一个子线程都从send获取了一个拷贝,然后该拷贝在子线程结束时自动被drop,看上去没问题啊。等等,好像send本身并没有被drop,因为send要等到main函数结束才会被drop,那么代码就陷入了一个尴尬的境地:main函数要结束需要for循环结束,for循环结束需要send被drop,而send要被drop需要main函数结束。。。 破局点只有一个,那就是主动drop掉send,这个简单,使用std::mem::drop函数即可,得益于prelude,我们只需要使用drop: use std::sync::mpsc;\nfn main() { use std::thread; let (send, recv) = mpsc::channel(); let num_threads = 3; for i in 0..num_threads { let thread_send = send.clone(); thread::spawn(move || { thread_send.send(i).unwrap(); println!(\"thread {:?} finished\", i); }); } drop(send); for x in recv { println!(\"Got: {}\", x); } println!(\"finished iterating\");\n} 此时再运行,主线程将顺利结束。","breadcrumbs":"征服编译错误 » Rust 常见陷阱 » 线程间传递消息导致主线程无法结束 » 线程间传递消息导致主线程无法结束","id":"1091","title":"线程间传递消息导致主线程无法结束"},"1092":{"body":"本文总结了一个新手在使用消息通道时常见的错误,那就是忘记处理创建通道时得到的发送者,最后由于该发送者的存活导致通道无法被关闭,最终主线程阻塞,造成程序错误。","breadcrumbs":"征服编译错误 » Rust 常见陷阱 » 线程间传递消息导致主线程无法结束 » 总结","id":"1092","title":"总结"},"1093":{"body":"大家应该都知道, 虽然 Rust 的字符串 &str、String 在底层是通过 Vec 实现的:字符串数据以字节数组的形式存在堆上,但在使用时,它们都是 UTF-8 编码的,例如: fn main() { let s: &str = \"中国人\"; for c in s.chars() { println!(\"{}\", c) // 依次输出:中 、 国 、 人 } let c = &s[0..3]; // 1. \"中\" 在 UTF-8 中占用 3 个字节 2. Rust 不支持字符串索引,因此只能通过切片的方式获取 \"中\" assert_eq!(c, \"中\");\n} 从上述代码,可以很清晰看出,Rust 的字符串确实是 UTF-8 编码的,这就带来一个隐患:可能在某个转角,你就会遇到来自糟糕性能的示爱。","breadcrumbs":"征服编译错误 » Rust 常见陷阱 » 警惕 UTF-8 引发的性能隐患 » 警惕 UTF-8 引发的性能隐患","id":"1093","title":"警惕 UTF-8 引发的性能隐患"},"1094":{"body":"例如我们尝试写一个词法解析器,里面用到了以下代码 self.source.chars().nth(self.index).unwrap(); 去获取下一个需要处理的字符,大家可能会以为 .nth 的访问应该非常快吧?事实上它确实很快,但是并不妨碍这段代码在循环处理 70000 长度的字符串时,需要消耗 5s 才能完成! 这么看来,唯一的问题就在于 .chars() 上了。 其实原因很简单,简单到我们不需要用代码来说明,只需要文字描述即可传达足够的力量:每一次循环时,.chars().nth(index) 都需要对字符串进行一次 UTF-8 解析,这个解析实际上是相当昂贵的,特别是当配合循环时,算法的复杂度就是平方级的。 既然找到原因,那解决方法也很简单:只要将 self.source.chars() 的迭代器存储起来就行,这样每次 .nth 调用都会复用已经解析好的迭代器,而不是重新去解析一次 UTF-8 字符串。 当然,我们还可以使用三方库来解决这个问题,例如 str_indices 。","breadcrumbs":"征服编译错误 » Rust 常见陷阱 » 警惕 UTF-8 引发的性能隐患 » 问题描述 & 解决","id":"1094","title":"问题描述 & 解决"},"1095":{"body":"最终的优化结果如下: 保存迭代器后: 耗时 5s -> 4ms 进一步使用 u8 字节数组来替换 char,最后使用 String::from_utf8 来构建 UTF-8 字符串: 耗时 4ms -> 400us 肉眼可见的巨大提升,12500 倍! 总之,我们在热点路径中使用字符串做 UTF-8 的相关操作时,就算不提前优化,也要做到心里有数,这样才能在问题发生时,进退自如。","breadcrumbs":"征服编译错误 » Rust 常见陷阱 » 警惕 UTF-8 引发的性能隐患 » 总结","id":"1095","title":"总结"},"1096":{"body":"","breadcrumbs":"Rust 性能优化 todo » Rust性能剖析 todo","id":"1096","title":"Rust性能剖析 todo"},"1097":{"body":"部分内容借鉴了Rust in action和Rust高级编程 https://www.youtube.com/watch?v=rDoqT-a6UFg","breadcrumbs":"Rust 性能优化 todo » 深入内存 todo » 深入内存","id":"1097","title":"深入内存"},"1098":{"body":"","breadcrumbs":"Rust 性能优化 todo » 深入内存 todo » 指针和引用 todo » 指针和引用(todo)","id":"1098","title":"指针和引用(todo)"},"1099":{"body":"https://lucumr.pocoo.org/2022/1/30/unsafe-rust/","breadcrumbs":"Rust 性能优化 todo » 深入内存 todo » 未初始化内存 todo » 未初始化内存","id":"1099","title":"未初始化内存"},"11":{"body":"AWS 从 2017 年开始就用 Rust 实现了无服务器计算平台: AWS Lambda 和 AWS Fargate,并且用 Rust 重写了 Bottlerocket OS 和 AWS Nitro 系统,这两个是弹性计算云 (EC2) 的重要服务 Cloudflare 是 Rust 的重度用户,DNS、无服务计算、网络包监控等基础设施都与 Rust 密不可分 Dropbox 的底层存储服务完全由 Rust 重写,达到了数万 PB 的规模 Google 除了在安卓系统的部分模块中使用 Rust 外,还在它最新的操作系统 Fuchsia 中重度使用 Rust Facebook 使用 Rust 来增强自己的网页端、移动端和 API 服务的性能,同时还写了 Hack 编程语言的虚拟机 Microsoft 使用 Rust 为 Azure 平台提供一些组件,其中包括 IoT 的核心服务 GitHub 和 npmjs.com,使用 Rust 提供高达每天 13 亿次的 npm 包下载 Rust 目前已经成为全世界区块链平台的首选开发语言 TiDB,国内最有名的开源分布式数据库 尤其值得一提的是,AWS 实际上在押宝 Rust,内部对 Rust 的使用已经上升到头等公民 first-class 的地位。","breadcrumbs":"进入 Rust 编程世界 » 使用现状","id":"11","title":"使用现状"},"110":{"body":"那么问题来了,为啥 String 可变,而字符串字面值 str 却不可以? 就字符串字面值来说,我们在编译时就知道其内容,最终字面值文本被直接硬编码进可执行文件中,这使得字符串字面值快速且高效,这主要得益于字符串字面值的不可变性。不幸的是,我们不能为了获得这种性能,而把每一个在编译时大小未知的文本都放进内存中(你也做不到!),因为有的字符串是在程序运行得过程中动态生成的。 对于 String 类型,为了支持一个可变、可增长的文本片段,需要在堆上分配一块在编译时未知大小的内存来存放内容,这些都是在程序运行时完成的: 首先向操作系统请求内存来存放 String 对象 在使用完成后,将内存释放,归还给操作系统 其中第一部分由 String::from 完成,它创建了一个全新的 String。 重点来了,到了第二部分,就是百家齐放的环节,在有 垃圾回收 GC 的语言中,GC 来负责标记并清除这些不再使用的内存对象,这个过程都是自动完成,无需开发者关心,非常简单好用;但是在无 GC 的语言中,需要开发者手动去释放这些内存对象,就像创建对象需要通过编写代码来完成一样,未能正确释放对象造成的后果简直不可估量。 对于 Rust 而言,安全和性能是写到骨子里的核心特性,如果使用 GC,那么会牺牲性能;如果使用手动管理内存,那么会牺牲安全,这该怎么办?为此,Rust 的开发者想出了一个无比惊艳的办法:变量在离开作用域后,就自动释放其占用的内存: { let s = String::from(\"hello\"); // 从此处起,s 是有效的 // 使用 s\n} // 此作用域已结束, // s 不再有效,内存被释放 与其它系统编程语言的 free 函数相同,Rust 也提供了一个释放内存的函数: drop,但是不同的是,其它语言要手动调用 free 来释放每一个变量占用的内存,而 Rust 则在变量离开作用域时,自动调用 drop 函数: 上面代码中,Rust 在结尾的 } 处自动调用 drop。 其实,在 C++ 中,也有这种概念: Resource Acquisition Is Initialization (RAII) 。如果你使用过 RAII 模式的话应该对 Rust 的 drop 函数并不陌生。 这个模式对编写 Rust 代码的方式有着深远的影响,在后面章节我们会进行更深入的介绍。","breadcrumbs":"Rust 基础入门 » 复合类型 » 字符串与切片 » 字符串深度剖析","id":"110","title":"字符串深度剖析"},"1100":{"body":"https://www.reddit.com/r/rust/comments/s4pknf/investigating_memory_allocations_in_rust/","breadcrumbs":"Rust 性能优化 todo » 深入内存 todo » 内存分配 todo » 内存分配(todo)","id":"1100","title":"内存分配(todo)"},"1101":{"body":"https://www.reddit.com/r/rust/comments/rwta4h/why_arent_rust_structs_laid_out_in_memory_like_c/","breadcrumbs":"Rust 性能优化 todo » 深入内存 todo » 内存布局 todo » 内存布局(todo)","id":"1101","title":"内存布局(todo)"},"1102":{"body":"","breadcrumbs":"Rust 性能优化 todo » 深入内存 todo » 虚拟内存 todo » 虚拟内存","id":"1102","title":"虚拟内存"},"1103":{"body":"https://nnethercote.github.io/perf-book/profiling.html","breadcrumbs":"Rust 性能优化 todo » 性能调优 doing » performance","id":"1103","title":"performance"},"1104":{"body":"https://www.reddit.com/r/rust/comments/rupcux/how_do_i_profile_a_rust_web_application_in/ https://zhuanlan.zhihu.com/p/191655266","breadcrumbs":"Rust 性能优化 todo » 性能调优 doing » How do I profile a Rust web application in production?","id":"1104","title":"How do I profile a Rust web application in production?"},"1105":{"body":"https://www.reddit.com/r/rust/comments/s793x7/force_4byte_memory_alignment/","breadcrumbs":"Rust 性能优化 todo » 性能调优 doing » 内存对齐","id":"1105","title":"内存对齐"},"1106":{"body":"https://www.reddit.com/r/rust/comments/sr02aj/what_makes_ripgrep_so_fast/","breadcrumbs":"Rust 性能优化 todo » 性能调优 doing » riggrep 为啥这么快","id":"1106","title":"riggrep 为啥这么快"},"1107":{"body":"https://flakm.github.io/posts/heap_allocation/","breadcrumbs":"Rust 性能优化 todo » 性能调优 doing » 测试堆性能","id":"1107","title":"测试堆性能"},"1108":{"body":"https://www.reddit.com/r/rust/comments/t06hk7/string_concatenations_benchmarks_updated/","breadcrumbs":"Rust 性能优化 todo » 性能调优 doing » 字符串操作性能","id":"1108","title":"Rust 性能优化 todo"},"1109":{"body":"深拷贝可以说是Rust性能优化的禁忌之词,但是在最不该发生深拷贝的地方却发生了, 本文带领大家来深入分析下原因。 在所有权章节中,我们详细介绍过 所有权转移(move) , 里面提到过一个重点:当类型实现Copy特征时,不会转移所有权,而是直接对值进行拷贝: fn main() { let x = 1; let y = x; // 不会报错 println!(\"我(x)的值仅仅是被复制了,我还拥有值的所有权,不信你看:{:?}\",x); let s = \"aaa\".to_string(); let s1 = s; // 会报错 println!(\"我(s)的值被转移给了s1,我已经失去所有权了: {}\",s);\n} 这里的x是数值类型,因此实现了Copy特征,当赋值给y时,仅仅是复制了值,并没有把所有权转移给y,但是s恰好相反,它没有实现Copy特征,当赋值后,所有权被转移给s1,最终导致了最后一行代码的报错. 根据之前的所有权学习章节,所有权转移时的仅仅是复制一个引用,并不会复制底层的数据,例如上面代码中,s的所有权转移给s1时,仅仅是复制了一个引用,该引用继续指向之前的字符串底层数据,因此 所有权转移的性能是非常高的 。 但是如果一切都这么完美,也不会出现这篇文章了,实际上是怎么样?先来看一段代码.","breadcrumbs":"Rust 性能优化 todo » 性能调优 doing » 深入理解 move » Rust所有权转移时发生了奇怪的深拷贝","id":"1109","title":"Rust所有权转移时发生了奇怪的深拷贝"},"111":{"body":"Rust By Practice,支持代码在线编辑和运行,并提供详细的习题解答。 字符串 习题解答 切片 习题解答 String 习题解答","breadcrumbs":"Rust 基础入门 » 复合类型 » 字符串与切片 » 课后练习","id":"111","title":"课后练习"},"1110":{"body":"struct LargeArray { a: [i128; 10000],\n} impl LargeArray { #[inline(always)] fn transfer(mut self) -> Self { println!(\"{:?}\", &mut self.a[1] as *mut i128); // 改变数组中的值 self.a[1] += 23; self.a[4] += 24; // 返回所有权 self }\n} fn main() { let mut f = LargeArray { a: [10i128; 10000] }; println!(\"{:?}\", &mut f.a[1] as *mut i128); let mut f2 = f.transfer(); println!(\"{:?}\", &mut f2.a[1] as *mut i128);\n} 上面的例子很简单,创建了一个结构体f(内部有一个大数组),接着将它的所有权转移给transfer方法,最后再通过Self返回,转移给f2,在此过程中,观察结构体中的数组第二个元素的内存地址如何变化。 这里还有几个注意点: LargeArray没有实现Copy特征,因此在所有权转移时, 本应该 只是复制一下引用,底层的数组并不会被复制 transfer方法的参数self表示接收所有权,而不是借用,返回类型Self也表示返回所有权,而不是返回借用, 具体内容在 方法 章节有介绍 从上可知,我们并不应该去复制底层的数组,那么底层数组的地址也不应该变化,换而言之三次内存地址输出应该是同一个地址。但是真的如此吗?世事难料: 0x16f9d6870\n0x16fa4bbc0\n0x16fa24ac0 果然,结果让人大跌眼镜,竟然三次地址都不一样,意味着每次转移所有权都发生了底层数组的深拷贝!什么情况?!!如果这样,我们以后还能信任Rust吗?完全不符合官方的宣传。 在福建有一个武夷山5A景区,不仅美食特别好吃,而且风景非常优美,其中最著名的就是历时1个多小时的九曲十八弯漂流,而我们的结论是否也能像漂游一样来个大转折?大家拭目以待。","breadcrumbs":"Rust 性能优化 todo » 性能调优 doing » 深入理解 move » move时发生了数据的深拷贝","id":"1110","title":"move时发生了数据的深拷贝"},"1111":{"body":"首先,通过谷歌搜索,我发现了一些蛛丝马迹,有文章提到如果通过println输出内存地址,可能会导致编译器优化失效,也就是从本该有的所有权转移变成了深拷贝,不妨来试试。 但是问题又来了,如果不用println或者类似的方法,我们怎么观察内存地址?好像陷入了绝路。。。只能从Rust之外去想办法了,此时大学学过的汇编发挥了作用: .LCPI0_0: .quad 10 .quad 0\nexample::xxx: mov eax, 160000 call __rust_probestack sub rsp, rax mov rax, rsp lea rcx, [rsp + 160000] vbroadcasti128 ymm0, xmmword ptr [rip + .LCPI0_0]\n.LBB0_1: vmovdqu ymmword ptr [rax], ymm0 vmovdqu ymmword ptr [rax + 32], ymm0 vmovdqu ymmword ptr [rax + 64], ymm0 vmovdqu ymmword ptr [rax + 96], ymm0 vmovdqu ymmword ptr [rax + 128], ymm0 vmovdqu ymmword ptr [rax + 160], ymm0 vmovdqu ymmword ptr [rax + 192], ymm0 vmovdqu ymmword ptr [rax + 224], ymm0 vmovdqu ymmword ptr [rax + 256], ymm0 vmovdqu ymmword ptr [rax + 288], ymm0 vmovdqu ymmword ptr [rax + 320], ymm0 vmovdqu ymmword ptr [rax + 352], ymm0 vmovdqu ymmword ptr [rax + 384], ymm0 vmovdqu ymmword ptr [rax + 416], ymm0 vmovdqu ymmword ptr [rax + 448], ymm0 vmovdqu ymmword ptr [rax + 480], ymm0 vmovdqu ymmword ptr [rax + 512], ymm0 vmovdqu ymmword ptr [rax + 544], ymm0 vmovdqu ymmword ptr [rax + 576], ymm0 vmovdqu ymmword ptr [rax + 608], ymm0 add rax, 640 cmp rax, rcx jne .LBB0_1 mov rax, qword ptr [rsp + 16] mov rdx, qword ptr [rsp + 24] add rax, 69 adc rdx, 0 add rsp, 160000 vzeroupper ret 去掉所有println后的汇编生成如上所示(大家可以在godbolt上自己尝试),以我蹩脚的汇编水平来看,貌似没有任何数组拷贝的发生,也就是说: 如同量子的不可观测性,我们的move也这么傲娇?我们用println观测,它就傲娇去复制,不观测时,就老老实实转移所有权?WTF! 事情感觉进入了僵局,下一步该如何办?","breadcrumbs":"Rust 性能优化 todo » 性能调优 doing » 深入理解 move » 罪魁祸首println?","id":"1111","title":"罪魁祸首println?"},"1112":{"body":"我突然灵光一现,想到一个问题,之前的所有权转移其实可以分为两类: 栈上数据的复制和堆上数据的转移 ,这也是非常符合直觉的,例如i32这种类型实现了Copy特征,可以存储在栈上,因此它就是复制行为,而String类型是引用存储在栈上,底层数据存储在堆上,因此转移所有权时只需要复制一下引用即可。 那问题来了,我们的LargeArray存在哪里?这也许就是一个破局点! struct LargeArray { a: [i128; 10000],\n} 结构体是一个复合类型,它内部字段的数据存在哪里,就大致决定了它存在哪里。而该结构体里面的a字段是一个数组,而不是动态数组Vec,从 数组 章节可知:数组是存储在栈上的数据结构! 再想想,栈上的数据在move的时候,是要从一个栈复制到另外一个栈的,那是不是内存地址就变了?!因此,就能完美解释,为什么使用println时,数组的地址会变化了,是因为栈上的数组发生了复制。 但是问题还有,为什么不使用println,数组地址就不变?要解释清楚这个问题,先从编译器优化讲起。","breadcrumbs":"Rust 性能优化 todo » 性能调优 doing » 深入理解 move » 栈和堆的不同move行为","id":"1112","title":"栈和堆的不同move行为"},"1113":{"body":"从根本上来说,move就意味着拷贝复制,只不过看是浅拷贝还是深拷贝,对于堆上的数据来说,浅拷贝只复制引用,而栈上的数据则是整个复制。 但是在实际场景中,由于编译器的复杂实现,它能优化的场景远比我们想象中更多,例如对于move过程中的复制,编译器有可能帮你优化掉,在没有println的代码中,该move过程就被Rust编译器优化了。 但是这种编译器优化非常复杂,而且随着Rust的版本更新在不停变化,因此几乎没有人能说清楚这里面的门门道道,但是有一点可以知道: move确实存在被优化的可能性,最终避免了复制的发生 . 那么println没有被优化的原因也呼之欲出了: 它阻止了编译器对move的优化。","breadcrumbs":"Rust 性能优化 todo » 性能调优 doing » 深入理解 move » 编译器对move的优化","id":"1113","title":"编译器对move的优化"},"1114":{"body":"编译器优化的一个基本准则就是:中间过程没有其它代码在使用,则可以尝试消除这些中间过程。 回头来看看println: println!(\"{:?}\", &mut f.a[1] as *mut i128); 它需要打印数组在各个点的内存地址,假如编译器优化了复制,那这些中间状态的内存地址是不是就丢失了?对于这种可能会导致状态丢失的情况,编译器是不可能进行优化的,因此move时的栈上数组复制就顺理成章的发生了, 还是2次。","breadcrumbs":"Rust 性能优化 todo » 性能调优 doing » 深入理解 move » println阻止了优化","id":"1114","title":"println阻止了优化"},"1115":{"body":"那么,在实践中遇到这种情况怎么办? &mut self 其实办法也很多,首当其冲的就是使用&mut self进行可变借用,而不是转移进来所有权,再转移出去。 Box分配到堆上 如果你确实需要依赖所有权的转移来实现某个功能(例如链式方法调用:x.a().b()...),那么就需要使用Box把该数组分配在堆上,而不是栈上: struct LargeArray { a: Box<[i128; 10000]>,\n} impl LargeArray { #[inline(always)] fn transfer(mut self) -> Self { println!(\"{:?}\", &mut self.a[1] as *mut i128); //do some stuff to alter it self.a[1] += 23; self.a[4] += 24; //return the same object self }\n} fn main() { let mut f = LargeArray { a: Box::new([10i128; 10000] )}; println!(\"{:?}\", &mut f.a[1] as *mut i128); let mut f2 = f.transfer(); println!(\"{:?}\", &mut f2.a[1] as *mut i128);\n} 输出如下: 0x138008010\n0x138008010\n0x138008010 完美符合了我们对堆上数据的预期,hooray! 神龟莫测的编译器优化 当然,你也可以选择相信编译器的优化,虽然很难识它真面目,同时它的行为也神鬼莫测,但是总归是在之前的例子中证明了,它确实可以,不是嘛? = , =","breadcrumbs":"Rust 性能优化 todo » 性能调优 doing » 深入理解 move » 最佳实践","id":"1115","title":"最佳实践"},"1116":{"body":"","breadcrumbs":"Rust 性能优化 todo » 性能调优 doing » 糟糕的提前优化 todo » 糟糕的提前优化","id":"1116","title":"糟糕的提前优化"},"1117":{"body":"由于Rust的编译器和LLVM很强大,因此就算你使用了多层函数调用去完成一件事(嵌套函数调用往往出于设计上的考虑),依然不会有性能上的影响,因为最终生成的机器码会消除这些多余的函数调用。 总之用Rust时,你不必操心多余的函数调用,只要写合理的代码,然后Rust会帮助你运行的更快!","breadcrumbs":"Rust 性能优化 todo » 性能调优 doing » 糟糕的提前优化 todo » 函数调用","id":"1117","title":"函数调用"},"1118":{"body":"","breadcrumbs":"Rust 性能优化 todo » 性能调优 doing » Clone 和 Copy todo » Clone和Copy","id":"1118","title":"Clone和Copy"},"1119":{"body":"https://www.reddit.com/r/rust/comments/sx8b7m/how_is_rust_able_to_elide_bounds_checks/","breadcrumbs":"Rust 性能优化 todo » 性能调优 doing » 减少 Runtime check(todo) » 减少runtime check","id":"1119","title":"减少runtime check"},"112":{"body":"https://blog.csdn.net/a1595901624/article/details/119294443","breadcrumbs":"Rust 基础入门 » 复合类型 » 字符串与切片 » 引用资料","id":"112","title":"引用资料"},"1120":{"body":"以下代码,我们实现了两种循环方式: // 第一种\nlet collection = [1, 2, 3, 4, 5];\nfor i in 0..collection.len() { let item = collection[i]; // ...\n} // 第二种\nfor item in collection { } 第一种方式是循环索引,然后通过索引下标去访问集合,第二种方式是直接循环迭代集合中的元素,优劣如下: 性能 :第一种使用方式中collection[index]的索引访问,会因为边界检查(bounds checking)导致运行时的性能损耗 - Rust会检查并确认index是落在集合内也就是合法的,但是第二种直接迭代的方式就不会触发这种检查,因为编译器会在编译时就完成分析并证明这种访问是合法的`","breadcrumbs":"Rust 性能优化 todo » 性能调优 doing » 减少 Runtime check(todo) » 减少集合访问的边界检查","id":"1120","title":"减少集合访问的边界检查"},"1121":{"body":"https://www.reddit.com/r/rust/comments/rntx7s/why_use_boxleak/","breadcrumbs":"Rust 性能优化 todo » 性能调优 doing » 减少 Runtime check(todo) » Box::leak","id":"1121","title":"Box::leak"},"1122":{"body":"https://www.reddit.com/r/rust/comments/rnbubh/whats_the_big_deal_with_bounds_checking/ https://www.reddit.com/r/rust/comments/s6u65e/optimization_of_bubble_sort_fails_without_hinting/","breadcrumbs":"Rust 性能优化 todo » 性能调优 doing » 减少 Runtime check(todo) » bounds check","id":"1122","title":"bounds check"},"1123":{"body":"https://www.reddit.com/r/rust/comments/rui1zz/write_assertions_that_clarify_code_to_both_the/","breadcrumbs":"Rust 性能优化 todo » 性能调优 doing » 减少 Runtime check(todo) » 使用assert 优化检查性能","id":"1123","title":"使用assert 优化检查性能"},"1124":{"body":"https://github.com/TC5027/blog/blob/master/false_sharing.md","breadcrumbs":"Rust 性能优化 todo » 性能调优 doing » CPU 缓存性能优化 todo » CPU缓存性能优化","id":"1124","title":"CPU缓存性能优化"},"1125":{"body":"Consider we work with the following struct representing a counter, struct Counter(u64); and we want to increment it with random u8 values with the help of a for loop : use rand::Rng;\nfn main() { let mut counter = Counter(0); let mut rng = rand::thread_rng(); for _ in 0..1_000_000 { counter.0 += rng.gen::() as u64; }\n} This takes 1.90ms to run on my laptop using cargo run --release. Remember this timing as it will be our reference value :) Now suppose we were given this struct, holding not 1 but 2 counters : struct Counters { c1 : u64, c2 : u64\n} Using the same approach, performing the increments for the 2 counters in a single-threaded fashion, we would expect to be twice slower (in fact it takes 3.71ms to execute). Can we do better ? Well, as our 2 counters are independent, we could spawn 2 threads, assign them one counter and increment concurrently ! Given I have 4 CPUs on my laptop, I would expect to be just as fast as the first scenario. Let's see ! First thing, we could create a local variable in each thread which would be incremented and then we would set the counter value to this incremented one (spoiler : good idea). But we could also save these 2 variables and share the Counter between the 2 threads with an Arc (spoiler : definitely not worth). Let's do this second option ! ^^ Doing the following code, fn main() { let counters = Arc::new(Counters{c1:0, c2:0}); let counters_clone = counters.clone(); let handler1 = thread::spawn(move || { let mut rng = rand::thread_rng(); for _ in 0..1_000_000 { counters.c1 += rng.gen::() as u64; } }); let handler2 = thread::spawn(move || { let mut rng = rand::thread_rng(); for _ in 0..1_000_000 { counters_clone.c2 += rng.gen::() as u64; } }); handler1.join(); handler2.join();\n} we end up with an error : cannot assign to data in an Arc cannot assign help: trait DerefMut is required to modify through a dereference, but it is not implemented for std::sync::Arcrustc(E0594) Unlucky. Maybe we could use atomic types . These types provide operations that synchronize updates between threads. In fact, as an equivalent of += we could use the fetch_add method which has the following signature : pub fn fetch_add(&self, val: u64, order: Ordering) -> u64. What should be highlighted is the &self. We could expect a &mut self given the modification we want to perform using it but thanks to the property that an atomic operation is performed without interruptions we don't need exclusive access to the variable to safely update it. We can solve the error replacing the counter's type by AtomicU64 as like that we only require Arc to implement the Deref trait (given the signature of fetch_add) and it is the case ! We so have to change a bit our struct to : struct Counters { c1 : AtomicU64, c2 : AtomicU64,\n} and our code to : fn main() { let counters = Arc::new(Counters{ c1 : AtomicU64::new(0), c2 : AtomicU64::new(0) }); let counters_clone = counters.clone(); let handler1 = thread::spawn(move || { let mut rng = rand::thread_rng(); for _ in 0..1_000_000 { counters.c1.fetch_add(rng.gen::() as u64,Relaxed); } }); let handler2 = thread::spawn(move || { let mut rng = rand::thread_rng(); for _ in 0..1_000_000 { counters_clone.c2.fetch_add(rng.gen::() as u64,Relaxed); } }); handler1.join();handler2.join();\n} We could naturally expect the operation on Atomics to be a bit slower than the ones on u64 but let's see ! 30.22ms .. ok... that's terrible ^^ Do Atomics operations explain all this ? I ran a benchmark to compare += and fetch_add( ,Relaxed) to figure it out : let mut sum = 0;\nlet start = Instant::now();\nfor _ in 0..10_000_000 { sum += rng.gen::() as u64;\n}\nprintln!(\"time spent u64 sum : {:?}\", start.elapsed());\nlet atomic_sum = AtomicU64::new(0);\nlet start = Instant::now();\nfor _ in 0..10_000_000 { atomic_sum.fetch_add(rng.gen::() as u64, Relaxed);\n}\nprintln!(\"time spent AtomicU64 sum : {:?}\", start.elapsed()); The u64 sums takes 20.07ms while the AtomicU64 one takes 70.28ms. So we should only be 3 times slower than 2ms but we are 15 times slower how can it be ??? Hint : CPU cache... but why should we care ? CPU cache is a data storage, located close to CPU, offering a fast access to data. In a computer, when the CPU needs to read or write a value, it checks if it is present inside the cache or not. If it is the case then the CPU directly uses the cached data. Otherwise, the cache allocates a new entry and copies data from main memory, an entry being of fixed size and called cache line . CPU cache is relatively small compared to RAM but much faster, and that's why a program should be designed to use as much as possible data lying in cache, based on a locality principle, to avoid expensive access to RAM. If we represent our current situation it looks like this : figure The red square corresponds to the first counter and the green one to the second. They can potentially lie in the same cache line ! If data is modified through CPU 0 in its L1 cache we expect our computer to reflect the changes both in memory and in the other L1 cache. To ensure this coherency, there exists coherence protocols which can force the whole cache line impacted by the change to be propagated through the whole system, in order to update the copies of the value changed. With that in mind, what is happening in our code comes from that : we suffer from coherency protocol due to our 2 counters lying on the same cache line. Updating first counter through CPU 0 involves an update in the system of the data stored in the cache line where the second counter (unchanged) potentially lies. During this update, CPU 1 cannot access the second counter whereas it is clearly independent from the change made by CPU 0, and that's why we are slow. How can we solve then ? well by making sure that the counters lie on different cache lines and that's where we can use the repr attribute. In Rust, we can specify the alignment we want for our type with the repr(align) attribute. We use it like this : #[repr(align(64))]\nstruct CachePadded(AtomicU64); A data of alignment X is stored in memory at address multiple of X. Knowing this, giving to our counters an alignment equal to the size of a cache line, we ensure that the 2 counters won't be stored in the same cache line ! We can get the size of cache lines with command getconf LEVEL1_DCACHE_LINESIZE. On my laptop the output value is 64. With those changes we have now a timing of 7.16ms which seems decent given we work with Atomics. Mission succeeded ! Finally given my remark at the beginning, I wanted to share a potentially better solution, using local variables in the threads, and channels to communicate these local variables back to the main thread : use std::sync::mpsc::channel;\nfn main() { let (s1,t1) = channel(); let (s2,t2) = channel(); let h1 = thread::spawn(move || { let mut local_counter = 0; let mut rng = rand::thread_rng(); for _ in 0..1_000_000 { local_counter += rng.gen::() as u64; } s1.send(local_counter) }); let h2 = thread::spawn(move || { let mut local_counter = 0; let mut rng = rand::thread_rng(); for _ in 0..1_000_000 { local_counter += rng.gen::() as u64; } s2.send(local_counter) }); h1.join(); h2.join(); let counter = Counters{c1: t1.recv().unwrap(),c2: t2.recv().unwrap()};\n} It takes 2.03 ms to execute :)","breadcrumbs":"Rust 性能优化 todo » 性能调优 doing » CPU 缓存性能优化 todo » On a use of the \"repr\" attribute in Rust","id":"1125","title":"On a use of the \"repr\" attribute in Rust"},"1126":{"body":"https://www.reddit.com/r/rust/comments/ruavjm/is_there_a_difference_in_performance_between/","breadcrumbs":"Rust 性能优化 todo » 性能调优 doing » CPU 缓存性能优化 todo » 动态和静态分发","id":"1126","title":"动态和静态分发"},"1127":{"body":"https://www.reddit.com/r/rust/comments/rn7ozz/find_perfect_number_comparison_go_java_rust/ package main import ( \"fmt\" \"math\" \"time\"\n) func main() { n := 320000 nums := make(map[int][]int) start := time.Now() calPerfs(n, nums) fmt.Printf(\"runtime: %s\\n\", time.Since(start))\n} func calPerfs(n int, nums map[int][]int) { for i := 1; i <= n; i++ { d := divs(i) if sum(d) == i { nums[i] = all(d) } }\n} func divs(num int) map[int]struct{} { r := make(map[int]struct{}) r[1] = struct{}{} mid := int(math.Sqrt(float64(num))) for i := 2; i <= mid; i++ { if num%i == 0 { r[i] = struct{}{} r[num/i] = struct{}{} } } return r\n} func sum(ds map[int]struct{}) int { var n int for k := range ds { n += k } return n\n} func all(ds map[int]struct{}) []int { var a []int for k := range ds { a = append(a, k) } return a\n}","breadcrumbs":"Rust 性能优化 todo » 性能调优 doing » 计算性能优化 todo » 计算性能优化","id":"1127","title":"计算性能优化"},"1128":{"body":"use std::time::Instant; const N: usize = 320_000\t; fn is_perfect(n: usize) -> bool { //println!(\"{:?}\", n); let mut sum = 1; let end = (n as f64).sqrt() as usize; for i in 2..end + 1{ if n % i == 0 { if i * i == n { sum += i; } else { sum += i + n / i; } } } sum == n\n} fn find_perfs(n: usize) -> Vec { let mut perfs:Vec = vec![]; for i in 2..n + 1 { if is_perfect(i) { perfs.push(i) } } perfs\n} fn main() { let start = Instant::now(); let perfects = find_perfs(N); println!(\"{:?}\", start.elapsed()); println!(\"{:?}, in {:?}\", perfects, N);\n}","breadcrumbs":"Rust 性能优化 todo » 性能调优 doing » 计算性能优化 todo » 120ms","id":"1128","title":"120ms"},"1129":{"body":"use { std::{time::Instant},\n}; const N: usize = 320000; // Optimized, takes about 320ms on an Core i7 6700 @ 3.4GHz\nfn cal_perfs2(n: usize) -> Vec { (1..=n) .into_iter() .filter(|i| cal2(*i) == *i) .collect::>()\n} fn cal2(n: usize) -> usize { (2..=(n as f64).sqrt() as usize) .into_iter() .filter_map(|i| if n % i == 0 { Some([i, n / i]) } else { None }) .map(|a| a[0] + a[1]) .sum::() + 1\n} fn main() { let start = Instant::now(); let perf2 = cal_perfs2(N); println!(\"{:?}\",perf2); println!(\"Optimized: {:?}\", start.elapsed());\n}","breadcrumbs":"Rust 性能优化 todo » 性能调优 doing » 计算性能优化 todo » 90ms","id":"1129","title":"90ms"},"113":{"body":"元组是由多种类型组合到一起形成的,因此它是复合类型,元组的长度是固定的,元组中元素的顺序也是固定的。 可以通过以下语法创建一个元组: fn main() { let tup: (i32, f64, u8) = (500, 6.4, 1);\n} 变量 tup 被绑定了一个元组值 (500, 6.4, 1),该元组的类型是 (i32, f64, u8),看到没?元组是用括号将多个类型组合到一起,简单吧? 可以使用模式匹配或者 . 操作符来获取元组中的值。","breadcrumbs":"Rust 基础入门 » 复合类型 » 元组 » 元组","id":"113","title":"元组"},"1130":{"body":"https://www.reddit.com/r/rust/comments/rkddg3/stackheap_question_regarding_performance/","breadcrumbs":"Rust 性能优化 todo » 性能调优 doing » 堆和栈 todo » 堆和栈","id":"1130","title":"堆和栈"},"1131":{"body":"https://www.reddit.com/r/rust/comments/s28g4x/allocating_many_boxes_at_once/ https://www.reddit.com/r/rust/comments/szza43/memory_freed_but_not_immediately/","breadcrumbs":"Rust 性能优化 todo » 性能调优 doing » 内存 allocator todo » 内存allocator todo","id":"1131","title":"内存allocator todo"},"1132":{"body":"https://era.co/blog/unbuffered-io-slows-rust-programs","breadcrumbs":"Rust 性能优化 todo » 性能调优 doing » 常用性能测试工具 todo » 常用性能测试工具","id":"1132","title":"常用性能测试工具"},"1133":{"body":"https://www.reddit.com/r/rust/comments/rxj81f/rust_profiling/","breadcrumbs":"Rust 性能优化 todo » 性能调优 doing » 常用性能测试工具 todo » profiling","id":"1133","title":"profiling"},"1134":{"body":"https://blog.zhuangty.com/rust-enum-layout/","breadcrumbs":"Rust 性能优化 todo » 性能调优 doing » Enum 内存优化 todo » Enum内存优化 todo","id":"1134","title":"Enum内存优化 todo"},"1135":{"body":"","breadcrumbs":"Rust 性能优化 todo » 编译优化 todo » 对抗编译检查","id":"1135","title":"对抗编译检查"},"1136":{"body":"https://ttalk.im/2021/12/llvm-infrastructure-and-rust.html","breadcrumbs":"Rust 性能优化 todo » 编译优化 todo » LLVM todo » LLVM todo","id":"1136","title":"LLVM todo"},"1137":{"body":"","breadcrumbs":"Rust 性能优化 todo » 编译优化 todo » 常见属性标记 todo » 常见属性标记","id":"1137","title":"常见属性标记"},"1138":{"body":"#[repr(align(64))]\nstruct CachePadded(AtomicU64); A data of alignment X is stored in memory at address multiple of X https://doc.rust-lang.org/reference/attributes.html","breadcrumbs":"Rust 性能优化 todo » 编译优化 todo » 常见属性标记 todo » 强制内存对齐","id":"1138","title":"强制内存对齐"},"1139":{"body":"","breadcrumbs":"Rust 性能优化 todo » 编译优化 todo » 提升编译速度 todo » 优化编译速度","id":"1139","title":"优化编译速度"},"114":{"body":"fn main() { let tup = (500, 6.4, 1); let (x, y, z) = tup; println!(\"The value of y is: {}\", y);\n} 上述代码首先创建一个元组,然后将其绑定到 tup 上,接着使用 let (x, y, z) = tup; 来完成一次模式匹配,因为元组是 (n1, n2, n3) 形式的,因此我们用一模一样的 (x, y, z) 形式来进行匹配,元组中对应的值会绑定到变量 x, y, z上。这就是解构:用同样的形式把一个复杂对象中的值匹配出来。","breadcrumbs":"Rust 基础入门 » 复合类型 » 元组 » 用模式匹配解构元组","id":"114","title":"用模式匹配解构元组"},"1140":{"body":"在Rust中,一段很不起眼的代码中可能也隐藏着玄机,编译器在细无声的为我们做着各种优化,本章将记录这些优化,帮助大家更好的理解程序的性能。","breadcrumbs":"Rust 性能优化 todo » 编译优化 todo » 编译器优化 todo » 编译器优化","id":"1140","title":"编译器优化"},"1141":{"body":"https://www.reddit.com/r/learnrust/comments/rz34ht/where_does_the_data_go_if_you_replace_some_with/","breadcrumbs":"Rust 性能优化 todo » 编译优化 todo » 编译器优化 todo » Option 枚举 todo » Option枚举","id":"1141","title":"Option枚举"},"1142":{"body":"下面的列表包含 Rust 中正在使用或者以后会用到的关键字。因此,这些关键字不能被用作标识符(除了 原生标识符 ),包括函数、变量、参数、结构体字段、模块、包、常量、宏、静态值、属性、类型、特征或生命周期。","breadcrumbs":"Appendix » 关键字 » 附录 A:关键字","id":"1142","title":"附录 A:关键字"},"1143":{"body":"如下关键字目前有对应其描述的功能。 as - 强制类型转换,或use 和 extern crate包和模块引入语句中的重命名 break - 立刻退出循环 const - 定义常量或原生常量指针(constant raw pointer) continue - 继续进入下一次循环迭代 crate - 链接外部包 dyn - 动态分发特征对象 else - 作为 if 和 if let 控制流结构的 fallback enum - 定义一个枚举类型 extern - 链接一个外部包,或者一个宏变量(该变量定义在另外一个包中) false - 布尔值 false fn - 定义一个函数或 函数指针类型 ( function pointer type ) for - 遍历一个迭代器或实现一个 trait 或者指定一个更高级的生命周期 if - 基于条件表达式的结果来执行相应的分支 impl - 为结构体或者特征实现具体功能 in - for 循环语法的一部分 let - 绑定一个变量 loop - 无条件循环 match - 模式匹配 mod - 定义一个模块 move - 使闭包获取其所捕获项的所有权 mut - 在引用、裸指针或模式绑定中使用,表明变量是可变的 pub - 表示结构体字段、impl 块或模块的公共可见性 ref - 通过引用绑定 return - 从函数中返回 Self - 实现特征类型的类型别名 self - 表示方法本身或当前模块 static - 表示全局变量或在整个程序执行期间保持其生命周期 struct - 定义一个结构体 super - 表示当前模块的父模块 trait - 定义一个特征 true - 布尔值 true type - 定义一个类型别名或关联类型 unsafe - 表示不安全的代码、函数、特征或实现 use - 在当前代码范围内(模块或者花括号对)引入外部的包、模块等 where - 表示一个约束类型的从句 while - 基于一个表达式的结果判断是否继续循环","breadcrumbs":"Appendix » 关键字 » 目前正在使用的关键字","id":"1143","title":"目前正在使用的关键字"},"1144":{"body":"如下关键字没有任何功能,不过由 Rust 保留以备将来的应用。 abstract async await become box do final macro override priv try typeof unsized virtual yield","breadcrumbs":"Appendix » 关键字 » 保留做将来使用的关键字","id":"1144","title":"保留做将来使用的关键字"},"1145":{"body":"原生标识符(Raw identifiers)允许你使用通常不能使用的关键字,其带有 r# 前缀。 例如,match 是关键字。如果尝试编译如下使用 match 作为名字的函数: fn match(needle: &str, haystack: &str) -> bool { haystack.contains(needle)\n} 会得到这个错误: error: expected identifier, found keyword `match` --> src/main.rs:4:4 |\n4 | fn match(needle: &str, haystack: &str) -> bool { | ^^^^^ expected identifier, found keyword 该错误表示你不能将关键字 match 用作函数标识符。你可以使用原生标识符将 match 作为函数名称使用: 文件名: src/main.rs fn r#match(needle: &str, haystack: &str) -> bool { haystack.contains(needle)\n} fn main() { assert!(r#match(\"foo\", \"foobar\"));\n} 此代码编译没有任何错误。注意 r# 前缀需同时用于函数名定义和 main 函数中的调用。 原生标识符允许使用你选择的任何单词作为标识符,即使该单词恰好是保留关键字。 此外,原生标识符允许你使用其它 Rust 版本编写的库。比如,try 在 Rust 2015 edition 中不是关键字,却在 Rust 2018 edition 是关键字。所以如果用 2015 edition 编写的库中带有 try 函数,在 2018 edition 中调用时就需要使用原始标识符语法,在这里是 r#try。","breadcrumbs":"Appendix » 关键字 » 原生标识符","id":"1145","title":"原生标识符"},"1146":{"body":"该附录包含了 Rust 目前出现过的各种符号,这些符号之前都分散在各个章节中。","breadcrumbs":"Appendix » 运算符与符号 » 附录 B:运算符与符号","id":"1146","title":"附录 B:运算符与符号"},"1147":{"body":"表 B-1 包含了 Rust 中的运算符、上下文中的示例、简短解释以及该运算符是否可重载。如果一个运算符是可重载的,则该运算符上用于重载的特征也会列出。 下表中,expr 是表达式,ident 是标识符,type 是类型,var 是变量,trait 是特征,pat 是匹配分支(pattern)。 表 B-1:运算符 运算符 示例 解释 是否可重载 ! ident!(...), ident!{...}, ident![...] 宏展开 ! !expr 按位非或逻辑非 Not != var != expr 不等比较 PartialEq % expr % expr 算术求余 Rem %= var %= expr 算术求余与赋值 RemAssign & &expr, &mut expr 借用 & &type, &mut type, &'a type, &'a mut type 借用指针类型 & expr & expr 按位与 BitAnd &= var &= expr 按位与及赋值 BitAndAssign && expr && expr 逻辑与 * expr * expr 算术乘法 Mul *= var *= expr 算术乘法与赋值 MulAssign * *expr 解引用 * *const type, *mut type 裸指针 + trait + trait, 'a + trait 复合类型限制 + expr + expr 算术加法 Add += var += expr 算术加法与赋值 AddAssign , expr, expr 参数以及元素分隔符 - - expr 算术取负 Neg - expr - expr 算术减法 Sub -= var -= expr 算术减法与赋值 SubAssign -> fn(...) -> type, |...| -> type 函数与闭包,返回类型 . expr.ident 成员访问 .. .., expr.., ..expr, expr..expr 右半开区间 PartialOrd ..= ..=expr, expr..=expr 闭合区间 PartialOrd .. ..expr 结构体更新语法 .. variant(x, ..), struct_type { x, .. } “代表剩余部分”的模式绑定 ... expr...expr (不推荐使用,用..=替代) 闭合区间 / expr / expr 算术除法 Div /= var /= expr 算术除法与赋值 DivAssign : pat: type, ident: type 约束 : ident: expr 结构体字段初始化 : 'a: loop {...} 循环标志 ; expr; 语句和语句结束符 ; [...; len] 固定大小数组语法的部分 << expr << expr 左移 Shl <<= var <<= expr 左移与赋值 ShlAssign < expr < expr 小于比较 PartialOrd <= expr <= expr 小于等于比较 PartialOrd = var = expr, ident = type 赋值/等值 == expr == expr 等于比较 PartialEq => pat => expr 匹配分支语法的部分 > expr > expr 大于比较 PartialOrd >= expr >= expr 大于等于比较 PartialOrd >> expr >> expr 右移 Shr >>= var >>= expr 右移与赋值 ShrAssign @ ident @ pat 模式绑定 ^ expr ^ expr 按位异或 BitXor ^= var ^= expr 按位异或与赋值 BitXorAssign | pat | pat 模式匹配中的多个可选条件 | expr | expr 按位或 BitOr |= var |= expr 按位或与赋值 BitOrAssign || expr || expr 逻辑或 ? expr? 错误传播","breadcrumbs":"Appendix » 运算符与符号 » 运算符","id":"1147","title":"运算符"},"1148":{"body":"表 B-2:独立语法 符号 解释 'ident 生命周期名称或循环标签 ...u8, ...i32, ...f64, ...usize, 等 指定类型的数值常量 \"...\" 字符串常量 r\"...\", r#\"...\"#, r##\"...\"##, etc. 原生字符串, 未转义字符 b\"...\" 将 &str 转换成 &[u8; N] 类型的数组 br\"...\", br#\"...\"#, br##\"...\"##, 等 原生字节字符串,原生和字节字符串字面值的结合 '...' Char 字符 b'...' ASCII 字节 |...| expr 闭包 ! 代表总是空的类型,用于发散函数(无返回值函数) _ 模式绑定中表示忽略的意思;也用于增强整型字面值的可读性 表 B-3 展示了模块和对象调用路径的语法。 表 B-3:路径相关语法 符号 解释 ident::ident 命名空间路径 ::path 从当前的包的根路径开始的相对路径 self::path 与当前模块相对的路径(如一个显式相对路径) super::path 与父模块相对的路径 type::ident, ::ident 关联常量、关联函数、关联类型 ::... 不可以被直接命名的关联项类型(如 <&T>::...,<[T]>::..., 等) trait::method(...) 使用特征名进行方法调用,以消除方法调用的二义性 type::method(...) 使用类型名进行方法调用, 以消除方法调用的二义性 ::method(...) 将类型转换为特征,再进行方法调用,以消除方法调用的二义性 表 B-4 展示了使用泛型参数时用到的符号。 表 B-4:泛型 符号 解释 path<...> 为一个类型中的泛型指定具体参数(如 Vec) path::<...>, method::<...> 为一个泛型、函数或表达式中的方法指定具体参数,通常指双冒号(turbofish)(如 \"42\".parse::()) fn ident<...> ... 泛型函数定义 struct ident<...> ... 泛型结构体定义 enum ident<...> ... 泛型枚举定义 impl<...> ... 实现泛型 for<...> type 高阶生命周期限制 type 泛型,其一个或多个相关类型必须被指定为特定类型(如 Iterator- ) 表 B-5 展示了使用特征约束来限制泛型参数的符号。 表 B-5:特征约束 符号 解释 T: U 泛型参数 T需实现U类型 T: 'a 泛型 T 的生命周期必须长于 'a(意味着该类型不能传递包含生命周期短于 'a 的任何引用) T : 'static 泛型 T 只能使用声明周期为'static 的引用 'b: 'a 生命周期'b必须长于生命周期'a T: ?Sized 使用一个不定大小的泛型类型 'a + trait, trait + trait 多个类型组成的复合类型限制 表 B-6 展示了宏以及在一个对象上定义属性的符号。 表 B-6:宏与属性 符号 解释 #[meta] 外部属性 #![meta] 内部属性 $ident 宏替换 $ident:kind 宏捕获 $(…)… 宏重复 ident!(...), ident!{...}, ident![...] 宏调用 表 B-7 展示了写注释的符号。 表 B-7:注释 符号 注释 // 行注释 //! 内部行(hang)文档注释 /// 外部行文档注释 /*...*/ 块注释 /*!...*/ 内部块文档注释 /**...*/ 外部块文档注释 表 B-8 展示了出现在使用元组时的符号。 表 B-8:元组 符号 解释 () 空元组(亦称单元),即是字面值也是类型 (expr) 括号表达式 (expr,) 单一元素元组表达式 (type,) 单一元素元组类型 (expr, ...) 元组表达式 (type, ...) 元组类型 expr(expr, ...) 函数调用表达式;也用于初始化元组结构体 struct 以及元组枚举 enum 变体 expr.0, expr.1, etc. 元组索引 表 B-9 展示了使用大括号的上下文。 表 B-9:大括号 符号 解释 {...} 代码块表达式 Type {...} 结构体字面值 表 B-10 展示了使用方括号的上下文。 表 B-10:方括号 符号 解释 [...] 数组 [expr; len] 数组里包含len个expr [type; len] 数组里包含了len个type类型的对象 expr[expr] 集合索引。 重载(Index, IndexMut) expr[..], expr[a..], expr[..b], expr[a..b] 集合索引,也称为集合切片,索引要实现以下特征中的其中一个:Range,RangeFrom,RangeTo 或 RangeFull","breadcrumbs":"Appendix » 运算符与符号 » 非运算符符号","id":"1148","title":"非运算符符号"},"1149":{"body":"在 语句与表达式 章节中,我们对表达式有过介绍,下面对这些常用表达式进行一一说明。","breadcrumbs":"Appendix » 表达式 » 附录 C:表达式","id":"1149","title":"附录 C:表达式"},"115":{"body":"模式匹配可以让我们一次性把元组中的值全部或者部分获取出来,如果只想要访问某个特定元素,那模式匹配就略显繁琐,对此,Rust 提供了 . 的访问方式: fn main() { let x: (i32, f64, u8) = (500, 6.4, 1); let five_hundred = x.0; let six_point_four = x.1; let one = x.2;\n} 和其它语言的数组、字符串一样,元组的索引从 0 开始。","breadcrumbs":"Rust 基础入门 » 复合类型 » 元组 » 用 . 来访问元组","id":"115","title":"用 . 来访问元组"},"1150":{"body":"let n = 3;\nlet s = \"test\";","breadcrumbs":"Appendix » 表达式 » 基本表达式","id":"1150","title":"基本表达式"},"1151":{"body":"fn main() { let var1 = 10; let var2 = if var1 >= 10 { var1 } else { var1 + 10 }; println!(\"{}\", var2);\n} 通过 if 表达式将值赋予 var2。 你还可以在循环中结合 continue 、break 来使用: let mut v = 0;\nfor i in 1..10 { v = if i == 9 { continue } else { i }\n}\nprintln!(\"{}\", v);","breadcrumbs":"Appendix » 表达式 » if 表达式","id":"1151","title":"if 表达式"},"1152":{"body":"let o = Some(3);\nlet v = if let Some(x) = o { x\n} else { 0\n};","breadcrumbs":"Appendix » 表达式 » if let 表达式","id":"1152","title":"if let 表达式"},"1153":{"body":"let o = Some(3);\nlet v = match o { Some(x) => x, _ => 0\n};","breadcrumbs":"Appendix » 表达式 » match 表达式","id":"1153","title":"match 表达式"},"1154":{"body":"let mut n = 0;\nlet v = loop { if n == 10 { break n } n += 1;\n};","breadcrumbs":"Appendix » 表达式 » loop 表达式","id":"1154","title":"loop 表达式"},"1155":{"body":"let mut n = 0;\nlet v = { println!(\"before: {}\", n); n += 1; println!(\"after: {}\", n); n\n};\nprintln!(\"{}\", v);","breadcrumbs":"Appendix » 表达式 » 语句块 {}","id":"1155","title":"语句块 {}"},"1156":{"body":"在本书的各个部分中,我们讨论了可应用于结构体和枚举定义的 derive 属性。被 derive 标记的对象会自动实现对应的默认特征代码,继承相应的功能。 在本附录中,我们列举了所有标准库存在的 derive 特征,每个特征覆盖了以下内容 该特征将会派生什么样的操作符和方法 由 derive 提供什么样的特征实现 实现特征对于类型意味着什么 你需要什么条件来实现该特征 特征示例 如果你希望不同于 derive 属性所提供的行为,请查阅 标准库文档 中每个特征的细节以了解如何手动实现它们。 除了本文列出的特征之外,标准库中定义的其它特征不能通过 derive 在类型上实现。这些特征不存在有意义的默认行为,所以由你负责以合理的方式实现它们。 一个无法被派生的特征例子是为终端用户处理格式化的 Display 。你应该时常考虑使用合适的方法来为终端用户显示一个类型。终端用户应该看到类型的什么部分?他们会找出相关部分吗?对他们来说最关心的数据格式是什么样的?Rust 编译器没有这样的洞察力,因此无法为你提供合适的默认行为。 本附录所提供的可派生特征列表其实并不全面:库可以为其内部的特征实现 derive ,因此除了本文列出的标准库 derive 之外,还有很多很多其它库的 derive 。实现 derive 涉及到过程宏的应用,这在 宏章节 中有介绍。","breadcrumbs":"Appendix » 派生特征 trait » 附录 D:派生特征 trait","id":"1156","title":"附录 D:派生特征 trait"},"1157":{"body":"Debug 特征可以让指定对象输出调试格式的字符串,通过在 {} 占位符中增加 :? 表明,例如println!(\"show you some debug info: {:?}\", MyObject);. Debug 特征允许以调试为目的来打印一个类型的实例,所以程序员可以在执行过程中看到该实例的具体信息。 例如,在使用 assert_eq! 宏时, Debug 特征是必须的。如果断言失败,这个宏就把给定实例的值打印出来,这样程序员就能看到两个实例为什么不相等。","breadcrumbs":"Appendix » 派生特征 trait » 用于开发者输出的 Debug","id":"1157","title":"用于开发者输出的 Debug"},"1158":{"body":"PartialEq 特征可以比较一个类型的实例以检查是否相等,并开启了 == 和 != 运算符的功能。 派生的 PartialEq 实现了 eq 方法。当 PartialEq 在结构体上派生时,只有 所有 的字段都相等时两个实例才相等,同时只要有任何字段不相等则两个实例就不相等。当在枚举上派生时,每一个成员都和其自身相等,且和其他成员都不相等。 例如,当使用 assert_eq! 宏时,需要比较一个类型的两个实例是否相等,则 PartialEq 特征是必须的。 Eq 特征没有方法, 其作用是表明每一个被标记类型的值都等于其自身。 Eq 特征只能应用于那些实现了 PartialEq 的类型,但并非所有实现了 PartialEq 的类型都可以实现 Eq。浮点类型就是一个例子:浮点数的实现表明两个非数字( NaN ,not-a-number)值是互不相等的。 例如,对于一个 HashMap 中的 key 来说, Eq 是必须的,这样 HashMap 就可以知道两个 key 是否一样。","breadcrumbs":"Appendix » 派生特征 trait » 等值比较的 PartialEq 和 Eq","id":"1158","title":"等值比较的 PartialEq 和 Eq"},"1159":{"body":"PartialOrd 特征可以让一个类型的多个实例实现排序功能。实现了 PartialOrd 的类型可以使用 <、 >、<= 和 >= 操作符。一个类型想要实现 PartialOrd 的前提是该类型已经实现了 PartialEq 。 派生 PartialOrd 实现了 partial_cmp 方法,一般情况下其返回一个 Option,但是当给定的值无法进行排序时将返回 None。尽管大多数类型的值都可以比较,但一个无法产生顺序的例子是:浮点类型的非数字值。当在浮点数上调用 partial_cmp 时, NaN 的浮点数将返回 None。 当在结构体上派生时, PartialOrd 以在结构体定义中字段出现的顺序比较每个字段的值来比较两个实例。当在枚举上派生时,认为在枚举定义中声明较早的枚举项小于其后的枚举项。 例如,对于来自于 rand 包的 gen_range 方法来说,当在一个大值和小值指定的范围内生成一个随机值时, PartialOrd trait 是必须的。 对于派生了 Ord 特征的类型,任何两个该类型的值都能进行排序。 Ord 特征实现了 cmp 方法,它返回一个 Ordering 而不是 Option,因为总存在一个合法的顺序。一个类型要想使用 Ord 特征,它必须要先实现 PartialOrd 和 Eq 。当在结构体或枚举上派生时, cmp 方法 和 PartialOrd 的 partial_cmp 方法表现是一致的。 例如,当在 BTreeSet(一种基于有序值存储数据的数据结构)上存值时, Ord 是必须的。","breadcrumbs":"Appendix » 派生特征 trait » 次序比较的 PartialOrd 和 Ord","id":"1159","title":"次序比较的 PartialOrd 和 Ord"},"116":{"body":"元组在函数返回值场景很常用,例如下面的代码,可以使用元组返回多个值: fn main() { let s1 = String::from(\"hello\"); let (s2, len) = calculate_length(s1); println!(\"The length of '{}' is {}.\", s2, len);\n} fn calculate_length(s: String) -> (String, usize) { let length = s.len(); // len() 返回字符串的长度 (s, length)\n} calculate_length 函数接收 s1 字符串的所有权,然后计算字符串的长度,接着把字符串所有权和字符串长度再返回给 s2 和 len 变量。 在其他语言中,可以用结构体来声明一个三维空间中的点,例如 Point(10, 20, 30),虽然使用 Rust 元组也可以做到:(10, 20, 30),但是这样写有个非常重大的缺陷: 不具备任何清晰的含义 ,在下一章节中,会提到一种与元组类似的结构体,元组结构体,可以解决这个问题。","breadcrumbs":"Rust 基础入门 » 复合类型 » 元组 » 元组的使用示例","id":"116","title":"元组的使用示例"},"1160":{"body":"Clone 特征用于创建一个值的深拷贝(deep copy),复制过程可能包含代码的执行以及堆上数据的复制。查阅 通过 Clone 进行深拷贝 获取有关 Clone 的更多信息。 派生 Clone 实现了 clone 方法,当为整个的类型实现 Clone 时,在该类型的每一部分上都会调用 clone 方法。这意味着类型中所有字段或值也必须实现了 Clone,这样才能够派生 Clone 。 例如,当在一个切片(slice)上调用 to_vec 方法时, Clone 是必须的。切片只是一个引用,并不拥有其所包含的实例数据,但是从 to_vec 中返回的 Vector 需要拥有实例数据,因此, to_vec 需要在每个元素上调用 clone 来逐个复制。因此,存储在切片中的类型必须实现 Clone。 Copy 特征允许你通过只拷贝存储在栈上的数据来复制值(浅拷贝),而无需复制存储在堆上的底层数据。查阅 通过 Copy 复制栈数据 的部分来获取有关 Copy 的更多信息。 实际上 Copy 特征并不阻止你在实现时使用了深拷贝,只是,我们不应该这么做,毕竟遵循一个语言的惯例是很重要的。当用户看到 Copy 时,潜意识就应该知道这是浅拷贝,复制一个值会非常快。 当一个类型的内部字段全部实现了 Copy 时,你就可以在该类型上派上 Copy 特征。 一个类型如果要实现 Copy 它必须先实现 Clone ,因为一个类型实现 Clone 后,就等于顺便实现了 Copy 。 总之, Copy 拥有更好的性能,当浅拷贝足够的时候,就不要使用 Clone ,不然会导致你的代码运行更慢,对于 性能优化 来说,一个很大的方面就是减少热点路径深拷贝的发生。","breadcrumbs":"Appendix » 派生特征 trait » 复制值的 Clone 和 Copy","id":"1160","title":"复制值的 Clone 和 Copy"},"1161":{"body":"Hash 特征允许你使用 hash 函数把一个任意大小的实例映射到一个固定大小的值上。派生 Hash 实现了 hash 方法,对某个类型进行 hash 调用,其实就是对该类型下每个字段单独进行 hash 调用,然后把结果进行汇总,这意味着该类型下的所有的字段也必须实现了 Hash,这样才能够派生 Hash。 例如,在 HashMap 上存储数据,存放 key 的时候, Hash 是必须的。","breadcrumbs":"Appendix » 派生特征 trait » 固定大小的值映射的 Hash","id":"1161","title":"固定大小的值映射的 Hash"},"1162":{"body":"Default 特征会帮你创建一个类型的默认值。 派生 Default 意味着自动实现了 default 函数。 default 函数的派生实现调用了类型每部分的 default 函数,这意味着类型中所有的字段也必须实现了 Default,这样才能够派生 Default 。 Default::default 函数通常结合结构体更新语法一起使用,这在第五章的 结构体更新语法 部分有讨论。可以自定义一个结构体的一小部分字段而剩余字段则使用 ..Default::default() 设置为默认值。 例如,当你在 Option 实例上使用 unwrap_or_default 方法时, Default 特征是必须的。如果 Option 是 None 的话, unwrap_or_default 方法将返回 T 类型的 Default::default 的结果。","breadcrumbs":"Appendix » 派生特征 trait » 默认值的 Default","id":"1162","title":"默认值的 Default"},"1163":{"body":"","breadcrumbs":"Appendix » prelude 模块 todo » 附录 E:prelude 模块","id":"1163","title":"附录 E:prelude 模块"},"1164":{"body":"","breadcrumbs":"Appendix » Rust 版本说明 » 附录 F:Rust 版本发布","id":"1164","title":"附录 F:Rust 版本发布"},"1165":{"body":"早在第一章,我们见过 cargo new 在 Cargo.toml 中增加了一些有关 edition 的元数据。本附录将解释其意义! 与其它语言相比,Rust 的更新迭代较为频繁(得益于精心设计过的发布流程以及 Rust 语言开发者团队管理): 每 6 周发布一个迭代版本 2 - 3 年发布一个新的大版本:每一个版本会结合已经落地的功能,并提供一个清晰的带有完整更新文档和工具的功能包。新版本会作为常规的 6 周发布过程的一部分发布。 好处在于,可以满足不同的用户群体的需求: 对于活跃的 Rust 用户,他们总是能很快获取到新的语言内容,毕竟,尝鲜是技术爱好者的共同特点:) 对于一般的用户,edition 的发布会告诉这些用户:Rust 语言相比上次大版本发布,有了重大的改进,值得一看 对于 Rust 语言开发者,可以让他们的工作成果更快的被世人所知,不必锦衣夜行 在本文档编写时,Rust 已经有三个版本:Rust 2015、2018、2021。本书基于 Rust 2021 edition 编写。 Cargo.toml 中的 edition 字段表明代码应该使用哪个版本编译。如果该字段不存在,其默认为 2021 以提供后向兼容性。 每个项目都可以选择不同于默认的 Rust 2021 edition 的版本。这样,版本可能会包含不兼容的修改,比如新版本中新增的关键字可能会与老代码中的标识符冲突并导致错误。不过,除非你选择应用这些修改,否则旧代码依然能够被编译,即便你升级了编译器版本。 所有 Rust 编译器都支持任何之前存在的编译器版本,并可以链接任何支持版本的包。编译器修改只影响最初的解析代码的过程。因此,如果你使用 Rust 2021 而某个依赖使用 Rust 2018,你的项目仍旧能够编译并使用该依赖。反之,若项目使用 Rust 2018 而依赖使用 Rust 2021 亦可工作。 有一点需要明确:大部分功能在所有版本中都能使用。开发者使用任何 Rust 版本将能继续接收最新稳定版的改进。然而在一些情况,主要是增加了新关键字的时候,则可能出现了只能用于新版本的功能。只需切换版本即可利用新版本的功能。 请查看 Edition Guide 了解更多细节,这是一个完全介绍版本的书籍,包括如何通过 cargo fix 自动将代码迁移到新版本。","breadcrumbs":"Appendix » Rust 版本说明 » Rust 版本说明","id":"1165","title":"Rust 版本说明"},"1166":{"body":"本附录介绍 Rust 语言自身是如何开发的以及这如何影响作为 Rust 开发者的你。","breadcrumbs":"Appendix » Rust 版本说明 » Rust 自身开发流程","id":"1166","title":"Rust 自身开发流程"},"1167":{"body":"作为一个语言,Rust 十分 注重代码的稳定性。我们希望 Rust 成为你代码坚实的基础,假如持续地有东西在变,这个希望就实现不了。但与此同时,如果不能实验新功能的话,在发布之前我们又无法发现其中重大的缺陷,而一旦发布便再也没有修改的机会了。 对于这个问题我们的解决方案被称为 “无停滞稳定”(“stability without stagnation”),其指导性原则是:无需担心升级到最新的稳定版 Rust。每次升级应该是无痛的,并应带来新功能,更少的 Bug 和更快的编译速度。","breadcrumbs":"Appendix » Rust 版本说明 » 无停滞稳定","id":"1167","title":"无停滞稳定"},"1168":{"body":"开发 Rust 语言是基于一个 火车时刻表 来进行的:所有的开发工作在 Master 分支上完成,但是发布就像火车时刻表一样,拥有不同的时间,发布采用的软件发布列车模型,被用于思科 IOS 和等其它软件项目。Rust 有三个 发布通道 ( release channel ): Nightly Beta Stable(稳定版) 大部分 Rust 开发者主要采用稳定版通道,不过希望实验新功能的开发者可能会使用 nightly 或 beta 版。 如下是一个开发和发布过程如何运转的例子:假设 Rust 团队正在进行 Rust 1.5 的发布工作。该版本发布于 2015 年 12 月,这个版本和时间显然比较老了,不过这里只是为了提供一个真实的版本。Rust 新增了一项功能:一个 master 分支的新提交。每天晚上,会产生一个新的 nightly 版本。每天都是发布版本的日子,而这些发布由发布基础设施自动完成。所以随着时间推移,发布轨迹看起来像这样,版本一天一发: nightly: * - - * - - * 每 6 周时间,是准备发布新版本的时候了!Rust 仓库的 beta 分支会从用于 nightly 的 master 分支产生。现在,有了两个发布版本: nightly: * - - * - - * |\nbeta: * 大部分 Rust 用户不会主要使用 beta 版本,不过在 CI 系统中对 beta 版本进行测试能够帮助 Rust 发现可能的回归缺陷(regression)。同时,每天仍产生 nightly 发布: nightly: * - - * - - * - - * - - * |\nbeta: * 比如我们发现了一个回归缺陷。好消息是在这些缺陷流入稳定发布之前还有一些时间来测试 beta 版本!fix 被合并到 master,为此 nightly 版本得到了修复,接着这些 fix 将 backport 到 beta 分支,一个新的 beta 发布就产生了: nightly: * - - * - - * - - * - - * - - * |\nbeta: * - - - - - - - - * 第一个 beta 版的 6 周后,是发布稳定版的时候了!stable 分支从 beta 分支生成: nightly: * - - * - - * - - * - - * - - * - * - * |\nbeta: * - - - - - - - - * |\nstable: * 好的!Rust 1.5 发布了!然而,我们忘了些东西:因为又过了 6 周,我们还需发布 新版 Rust 的 beta 版,Rust 1.6。所以从 beta 分支生成 stable 分支后,新版的 beta 分支也再次从 nightly 生成: nightly: * - - * - - * - - * - - * - - * - * - * | |\nbeta: * - - - - - - - - * * |\nstable: * 这被称为 “train model”,因为每 6 周,一个版本 “离开车站”(“leaves the station”),不过从 beta 通道到达稳定通道还有一段旅程。 Rust 每 6 周发布一个版本,如时钟般准确。如果你知道了某个 Rust 版本的发布时间,就可以知道下个版本的时间:6 周后。每 6 周发布版本的一个好的方面是下一班车会来得更快。如果特定版本碰巧缺失某个功能也无需担心:另一个版本很快就会到来!这有助于减少因临近发版时间而偷偷释出未经完善的功能的压力。 多亏了这个过程,你总是可以切换到下一版本的 Rust 并验证是否可以轻易的升级:如果 beta 版不能如期工作,你可以向 Rust 团队报告并在发布稳定版之前得到修复!beta 版造成的破坏是非常少见的,不过 rustc 也不过是一个软件,可能会存在 Bug。","breadcrumbs":"Appendix » Rust 版本说明 » Choo, Choo! ~~ 小火车发布流程启动","id":"1168","title":"Choo, Choo! ~~ 小火车发布流程启动"},"1169":{"body":"这个发布模型中另一个值得注意的地方:不稳定功能(unstable features)。Rust 使用一个被称为 “功能标记”(“feature flags”)的技术来确定给定版本的某个功能是否启用。如果新功能正在积极地开发中,其提交到了 master,因此会出现在 nightly 版中,不过会位于一个 功能标记 之后。作为用户,如果你希望尝试这个正在开发的功能,则可以在源码中使用合适的标记来开启,不过必须使用 nightly 版。 如果使用的是 beta 或稳定版 Rust,则不能使用任何功能标记。这是在新功能被宣布为永久稳定之前获得实用价值的关键。这既满足了希望使用最尖端技术的同学,那些坚持稳定版的同学也知道其代码不会被破坏。这就是无停滞稳定。 本书只包含稳定的功能,因为还在开发中的功能仍可能改变,当其进入稳定版时肯定会与编写本书的时候有所不同。你可以在网上获取 nightly 版的文档。","breadcrumbs":"Appendix » Rust 版本说明 » 不稳定功能","id":"1169","title":"不稳定功能"},"117":{"body":"Rust By Practice ,支持代码在线编辑和运行,并提供详细的 习题解答 。","breadcrumbs":"Rust 基础入门 » 复合类型 » 元组 » 课后练习","id":"117","title":"课后练习"},"1170":{"body":"安装 Rust Nightly 版本 Rustup 使得改变不同发布通道的 Rust 更为简单,其在全局或分项目的层次工作。其默认会安装稳定版 Rust。例如为了安装 nightly: $ rustup install nightly 你会发现 rustup 也安装了所有的 工具链 ( toolchains , Rust 和其相关组件)。如下是一位作者的 Windows 计算机上的例子: > rustup toolchain list\nstable-x86_64-pc-windows-msvc (default)\nbeta-x86_64-pc-windows-msvc\nnightly-x86_64-pc-windows-msvc 在指定目录使用 Rust Nightly 如你所见,默认是稳定版。大部分 Rust 用户在大部分时间使用稳定版。你可能也会这么做,不过如果你关心最新的功能,可以为特定项目使用 nightly 版。为此,可以在项目目录使用 rustup override 来设置当前目录 rustup 使用 nightly 工具链: $ cd ~/projects/needs-nightly\n$ rustup override set nightly 现在,每次在 *~/需要 nightly 的项目/*下(在项目的根目录下,也就是 Cargo.toml 所在的目录) 调用 rustc 或 cargo,rustup 会确保使用 nightly 版 Rust。在你有很多 Rust 项目时大有裨益!","breadcrumbs":"Appendix » Rust 版本说明 » Rustup 和 Rust Nightly 的职责","id":"1170","title":"Rustup 和 Rust Nightly 的职责"},"1171":{"body":"那么你如何了解这些新功能呢?Rust 开发模式遵循一个 Request For Comments (RFC) 过程 。如果你希望改进 Rust,可以编写一个提议,也就是 RFC。 任何人都可以编写 RFC 来改进 Rust,同时这些 RFC 会被 Rust 团队评审和讨论,他们由很多不同分工的子团队组成。这里是 Rust 官网 上所有团队的总列表,其包含了项目中每个领域的团队:语言设计、编译器实现、基础设施、文档等。各个团队会阅读相应的提议和评论,编写回复,并最终达成接受或回绝功能的一致。 如果功能被接受了,在 Rust 仓库会打开一个 issue,人们就可以实现它。实现功能的人可能不是最初提议功能的人!当实现完成后,其会合并到 master 分支并位于一个特性开关(feature gate)之后,正如 不稳定功能 部分所讨论的。 在稍后的某个时间,一旦使用 nightly 版的 Rust 团队能够尝试这个功能了,团队成员会讨论这个功能在 nightly 中运行的情况,并决定是否应该进入稳定版。如果决定继续推进,特性开关会移除,然后这个功能就被认为是稳定的了!乘着“发布的列车”,最终在新的稳定版 Rust 中出现。","breadcrumbs":"Appendix » Rust 版本说明 » RFC 过程和团队","id":"1171","title":"RFC 过程和团队"},"1172":{"body":"本目录包含了 Rust 历次版本更新的重要内容解读,需要注意,每个版本实际更新的内容要比这里记录的更多,全部内容请访问每节开头的官方链接查看。","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 附录 G:Rust 更新版本列表","id":"1172","title":"附录 G:Rust 更新版本列表"},"1173":{"body":"众所周知,Rust 小版本发布非常频繁,6 周就发布一次,因此通常不会有特别值得普通用户关注的内容,但是这次 1.58 版本不同,新增了(stable 化了)一个非常好用的功能: 在格式化字符串时捕获环境中的值 。 Rust 1.58 官方 release doc: Announcing Rust 1.58.0 | Rust Blog","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.58 » Rust 新版解读 | 1.58 | 重点: 格式化字符串捕获环境中的值","id":"1173","title":"Rust 新版解读 | 1.58 | 重点: 格式化字符串捕获环境中的值"},"1174":{"body":"在以前,想要输出一个函数的返回值,你需要这么做: fn get_person() -> String { String::from(\"sunface\")\n}\nfn main() { let p = get_person(); println!(\"Hello, {}!\", p); // implicit position println!(\"Hello, {0}!\", p); // explicit index println!(\"Hello, {person}!\", person = p);\n} 问题倒也不大,但是一旦格式化字符串长了后,就会非常冗余,而在 1.58 后,我们可以这么写: fn get_person() -> String { String::from(\"sunface\")\n}\nfn main() { let person = get_person(); println!(\"Hello, {person}!\");\n} 是不是清晰、简洁了很多?甚至还可以将环境中的值用于格式化参数: let (width, precision) = get_format();\nfor (name, score) in get_scores() { println!(\"{name}: {score:width$.precision$}\");\n} 但也有局限,它只能捕获普通的变量,对于更复杂的类型(例如表达式),可以先将它赋值给一个变量或使用以前的 name = expression 形式的格式化参数。 目前除了 panic! 外,其它接收格式化参数的宏,都可以使用新的特性。对于 panic! 而言,如果还在使用 2015版本 或 2018版本 版本 ,那 panic!(\"{ident}\") 依然会被当成 正常的字符串来处理,同时编译器会给予 warn 提示。而对于 2021版本 ,则可以正常使用: fn get_person() -> String { String::from(\"sunface\")\n}\nfn main() { let person = get_person(); panic!(\"Hello, {person}!\");\n} 输出: thread 'main' panicked at 'Hello, sunface!', src/main.rs:6:5\nnote: run with `RUST_BACKTRACE=1` environment variable to display a backtrace","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.58 » 在格式化字符串时捕获环境中的值","id":"1174","title":"在格式化字符串时捕获环境中的值"},"1175":{"body":"在 1.58 中为 Option 和 Result 新增了 unwrap_unchecked 方法,与 unwrap 遇到错误或者空值直接 panic 不同,unwrap_unchecked 遇到错误时处理方式糟糕的多: fn get_num() -> Option { None\n}\nfn main() { unsafe { let n = get_num().unwrap_unchecked(); }\n} 输出如下: zsh: segmentation fault cargo run 嗯,段错误了,对比下 panic,有一种泪流满面的冲动:我要这不安全的方法何用? 其实,还真有些用: 想要较小的可执行文件时(嵌入式,WASM 等),该方法就可以大显身手。因为 panic 会导致 二进制可执行文件变大不少 它还可以提高一点性能, 因为编译器可能无法优化掉 unwrap 的指令分支, 虽然它只会增加区区几条分支预测指令","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.58 » 比 unwrap 更危险的 unwrap_unchecked","id":"1175","title":"比 unwrap 更危险的 unwrap_unchecked"},"1176":{"body":"Rust 团队于今天凌晨( 2022-02-25 )发布了最新的 1.59 版本,其中最引人瞩目的特性应该就是支持在代码中内联汇编了,一起来看看。","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.59 » Rust 新版解读 | 1.59 | 重点: 内联汇编、解构式赋值","id":"1176","title":"Rust 新版解读 | 1.59 | 重点: 内联汇编、解构式赋值"},"1177":{"body":"该特性对于需要底层控制的应用非常有用,例如想要控制底层执行、访问特定的机器指令等。 例如,如果目标平台是 x86-64 时,你可以这么写: use std::arch::asm; // 使用 shifts 和 adds 实现 x 乘以 6\nlet mut x: u64 = 4;\nunsafe {`` asm!( \"mov {tmp}, {x}\", \"shl {tmp}, 1\", \"shl {x}, 2\", \"add {x}, {tmp}\", x = inout(reg) x, tmp = out(reg) _, );\n}\nassert_eq!(x, 4 * 6); 大家发现没,这里的格式化字符串的使用方式跟我们平时的 println!、format! 并无区别, 除了 asm! 之外, global_asm! 宏也可以这么使用。 内联汇编中使用的汇编语言和指令取决于相应的机器平台,截至目前,Rust 支持以下平台的内联汇编: x86 和 x86-64 ARM AArch64 RISC-V 如果大家希望深入了解,可以看官方的 Reference 文档,同时在 Rust Exercise 中提供了更多的示例(目前正在翻译中..)。","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.59 » 内联汇编( inline assembly )","id":"1177","title":"内联汇编( inline assembly )"},"1178":{"body":"现在你可以在赋值语句的左式中使用元组、切片和结构体模式了。 let (a, b, c, d, e); (a, b) = (1, 2);\n[c, .., d, _] = [1, 2, 3, 4, 5];\nStruct { e, .. } = Struct { e: 5, f: 3 }; assert_eq!([1, 2, 1, 4, 5], [a, b, c, d, e]); 这种使用方式跟 let 保持了一致性,但是需要注意,使用 += 的赋值语句还不支持解构式赋值。","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.59 » 解构式赋值( Destructuring assignments)","id":"1178","title":"解构式赋值( Destructuring assignments)"},"1179":{"body":"为参数设置默认值 现在我们可以为 const 泛型参数设置默认值: struct ArrayStorage { arr: [T; N],\n} impl ArrayStorage { fn new(a: T, b: T) -> ArrayStorage { ArrayStorage { arr: [a, b], } }\n} 取消参数顺序的限制 在之前版本中,类型参数必须要在所有的 const 泛型参数之前,现在,这个限制被放宽了,例如你可以这样交替排列它们: fn cartesian_product< T, const N: usize, U, const M: usize, V, F\n>(a: [T; N], b: [U; M], f: F) -> [[V; N]; M]\nwhere F: FnMut(&T, &U) -> V\n{ // ...\n}","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.59 » const 泛型","id":"1179","title":"const 泛型"},"118":{"body":"上一节中提到需要一个更高级的数据结构来帮助我们更好的抽象问题,结构体 struct 恰恰就是这样的复合数据结构,它是由其它数据类型组合而来。 其它语言也有类似的数据结构,不过可能有不同的名称,例如 object、 record 等。 结构体跟之前讲过的 元组 有些相像:都是由多种类型组合而成。但是与元组不同的是,结构体可以为内部的每个字段起一个富有含义的名称。因此结构体更加灵活更加强大,你无需依赖这些字段的顺序来访问和解析它们。","breadcrumbs":"Rust 基础入门 » 复合类型 » 结构体 » 结构体","id":"118","title":"结构体"},"1180":{"body":"对于受限的环境来说,减少编译出的二进制文件体积是非常重要的。 在之前,我们可以在二进制文件创建后,手动的来完成。现在 cargo 和 rustc 支持在链接( linked )后就删除 debug 信息,在 Cargo.toml 中新增以下配置: [profile.release]\nstrip = \"debuginfo\" 以上配置会将 release 二进制文件中的 debug 信息移除。你也可以使用 \"symbols\" 或 true 选项来移除所有支持的 symbol 信息。 根据 reddit 网友的测试,如果使用了 strip = true,那编译后的体积将大幅减少(50% 左右): 先使用 lto = true : 4,397,320 bytes 再使用 strip = true : 2,657,304 bytes 最后 opt-level = \"z\" : 1,857,680 bytes 如果是 WASM,还可以使用以下配置进一步减少体积: [package.metadata.wasm-pack.profile.release]\nwasm-opt = ['-Os'] github 上一个开源仓库 也证明了这一点,总体来看,这个配置的效果是非常显著的!","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.59 » 减少二进制文件体积:删除 debug 信息","id":"1180","title":"减少二进制文件体积:删除 debug 信息"},"1181":{"body":"1.59.0 版本默认关闭了增量编译的功能(你可以通过环境变量显式地启用:RUSTC_FORCE_INCREMENTAL=1 ),这会降低已知 Bug #94124 的影响,该 Bug 会导致增量编译过程中的反序列化错误和 panic。 不过大家也不用担心,这个 Bug 会在 1.60.0 版本修复,也就是 6 周后,增量编译会重新设置为默认开启,如果没有意外的话 :)","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.59 » 默认关闭增量编译","id":"1181","title":"默认关闭增量编译"},"1182":{"body":"一些方法和特征实现现在已经可以 stable 中使用,具体见 官方发布说明","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.59 » 稳定化的 API 列表","id":"1182","title":"稳定化的 API 列表"},"1183":{"body":"原文链接: https://blog.rust-lang.org/2022/04/07/Rust-1.60.0.html 通过 rustup 安装的同学可以使用以下命令升级到 1.60 版本: $ rustup update stable","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.60 » Rust 新版解读 | 1.60 | 重点: 查看 Cargo 构建耗时详情、Cargo Feature 增加新语法","id":"1183","title":"Rust 新版解读 | 1.60 | 重点: 查看 Cargo 构建耗时详情、Cargo Feature 增加新语法"},"1184":{"body":"rustc 新增了基于 LLVM 的代码覆盖率测量,想要测试的同学可以通过以下方式重新构建你的项目: $ RUSTFLAGS=\"-C instrument-coverage\" cargo build 运行新生成的可执行文件将在当前目录下产生一个 default.profraw 文件( 路径和文件名可以通过环境变量进行 覆盖 )。 llvm-tools-preview 组件包含了 llvm-profdata,可以用于处理和合并原生的测量结果输出raw profile output)(测量区域执行数)。 llvm-cov 用于报告生成,它将 llvm-profdata 处理后的输出跟二进制可执行文件自身相结合,对于前者大家可能好理解,但是为何要跟后者可执行文件相结合呢?原因在于可执行文件中嵌入了一个从计数器到实际源代码单元的映射。 rustup component add llvm-tools-preview\n$(rustc --print sysroot)/lib/rustlib/x86_64-unknown-linux-gnu/bin/llvm-profdata merge -sparse default.profraw -o default.profdata\n$(rustc --print sysroot)/lib/rustlib/x86_64-unknown-linux-gnu/bin/llvm-cov show -Xdemangler=rustfilt target/debug/coverage-testing \\ -instr-profile=default.profdata \\ -show-line-counts-or-regions \\ -show-instantiations 基于一个简单的 hello world 可执行文件,执行以上命令就可以获得如下带有标记的结果: 1| 1|fn main() {\n2| 1| println!(\"Hello, world!\");\n3| 1|} 从结果中可以看出:每一行代码都已经被成功覆盖。 如果大家还想要了解更多,可以看下 官方的 rustc 文档 。目前来说,基准功能已经稳定了,并将以某种形式存在于未来所有的 Rust 发布版本中。 但输出格式和产生这些输出的 LLVM 工具可能依然会发生变化,基于此,大家在使用时需要确保 llvm-tools-preview 和 rustc ( 用于编译代码的 )使用了相同的版本。","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.60 » 基于源码的代码覆盖","id":"1184","title":"基于源码的代码覆盖"},"1185":{"body":"新版本中,以下命令已经可以正常使用了: $ cargo build --timings Compiling hello-world v0.1.0 (hello-world) Timing report saved to target/cargo-timings/cargo-timing-20220318T174818Z.html Finished dev [unoptimized + debuginfo] target(s) in 0.98s 此命令会生成一个 cargo build 的耗时详情报告,除了上面提到的路径外,报告还会被拷贝到 target/cargo-timings/cargo-timing.html。这里是一个 在线示例 。该报告在你需要提升构建速度时会非常有用,更多的信息请 查看文档 。","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.60 » 查看 Cargo 构建耗时","id":"1185","title":"查看 Cargo 构建耗时"},"1186":{"body":"关于 Cargo Features ,强烈推荐大家看看 Cargo 使用指南 ,可能是目前最好的中文翻译版本。 新版本为 Cargo Features 引入了两个新的语法: 命名空间 ( Namespaced )和弱依赖,它们可以让 features 跟可选依赖进行更好的交互。 Cargo 支持 可选依赖 已经很久了,例如以下代码所示: [dependencies]\njpeg-decoder = { version = \"0.1.20\", default-features = false, optional = true } [features]\n# 通过开启 jpeg-decoder 依赖的 \"rayon` feture,来启用并行化处理\nparallel = [\"jpeg-decoder/rayon\"] 这个例子有两点值得注意: 可选依赖 jpeg-decoder 隐式地定义了一个同名的 feature,当启用 jpeg-decoder feature 时将同时启用 jpeg-decoder \"jpeg-decoder/rayon\" 语法会启用 jpeg-decoder 依赖,并且还会启用 jpeg-decoder 依赖的 rayon feature 而命名空间正是为了处理第一个问题而出现的。新版本中,我们可以在 [features] 中使用 dep: 前缀来显式地引用一个可选的依赖。再无需像第一点一样:先隐式的将可选依赖暴露为一个 feature,再通过 feature 来启用它。 这样一来,我们将能更好的定义可选依赖所对应的 feture,包括将可选依赖隐藏在一个更具描述性的 feature 名称后面。 弱依赖用于处理第二点: 根据第二点,optional-dependency/feature-name 必定会启用 optional-dependency 这个可选依赖。然而在一些场景中,我们只希望在其它 features 已经启用了可选依赖 optional-dependency 时才去启用 feature-name 这个 feature。 从 1.60 开始,我们可以使用 \"package-name?/feature-name\" 这种带有 ? 形式的语法: 只有当其它项已经启用了可选依赖 package-name 的情况下才去开启给定的 feature feature-name。 译者注:简单来说,要启用 feature 必须需要别人先启用了其前置的可选依赖,再也无法像之前的第二点一样,既能开启可选依赖,又能启用 feature。 例如,我们希望为自己的库增加一些序列化功能,它需要开启某个可选依赖中的指定 feature,可以这么做: [dependencies]\nserde = { version = \"1.0.133\", optional = true }\nrgb = { version = \"0.8.25\", optional = true } [features]\nserde = [\"dep:serde\", \"rgb?/serde\"] 这里定义了以下关系: 开启 serde feature 将启用可选的 serde 依赖 只有当 rgb 依赖在其它地方已经被启用后,此处才能启用 rgb 的 serde feature","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.60 » Cargo Feature 的新语法","id":"1186","title":"Cargo Feature 的新语法"},"1187":{"body":"在 1.59 更新说明中 ,我们有提到因为某些问题,增量编译被默认关闭了,现在官方修复了其中一些,并且确认目前的状态不会再影响用户的使用,因此在 1.60 版本中,增量编译又重新默认开启了。","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.60 » 增量编译重启开启","id":"1187","title":"增量编译重启开启"},"1188":{"body":"译者注:Instant 可以获取当前的时间,因此保证其单调增长是非常重要的,例如 uuid 的生成往往依赖于时间戳的单调增长,一旦时间回退,就可能出现 uuid 重复的情况。 在目前所有的平台上,Instant 会去尝试使用系统提供的 API 来保证单调性行为( 目前主要针对 tier 1 的平台 )。然而在实际场景中,这种单调性偶尔会因为硬件、虚拟化或操作系统bug 等原因而失效。 为了解决这些失效或是平台没有提供 API 的情况,Instant::duration_since, Instant::elapsed 和 Instant::sub 现在饱和为零( 这里不太好翻译,原文是 now saturate to zero,大概意思是非负?)。而在老版本中,这种时间回退的情况会导致 panic。 Instant::checked_duration_since 也可以用于检测和处理单调性失败或 Instants 的减法顺序不正确的情况。 但是目前的解决方法会遮掩一些错误的发生,因此在未来版本中,Rust 可能会重新就某些场景引入 panic 机制。 在 1.60 版本前,单调性主要通过标准库的互斥锁 Mutex 或原子性 atomic 来保证,但是在 Instant::now() 调用频繁时,可能会导致明显的性能问题。","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.60 » Instant 单调性保证","id":"1188","title":"Instant 单调性保证"},"1189":{"body":"原文链接: https://blog.rust-lang.org/2022/05/19/Rust-1.61.0.html 翻译 by : AllanDowney 通过 rustup 安装的同学可以使用以下命令升级到 1.61 版本: $ rustup update stable","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.61 » Rust 新版解读 | 1.61 | 重点: 自定义 main 函数 ExitCode、const fn 增强、为锁定的 stdio 提供静态句柄","id":"1189","title":"Rust 新版解读 | 1.61 | 重点: 自定义 main 函数 ExitCode、const fn 增强、为锁定的 stdio 提供静态句柄"},"119":{"body":"天下无敌的剑士往往也因为他有一柄无双之剑,既然结构体这么强大,那么我们就需要给它配套一套强大的语法,让用户能更好的驾驭。 定义结构体 一个结构体由几部分组成: 通过关键字 struct 定义 一个清晰明确的结构体 名称 几个有名字的结构体 字段 例如, 以下结构体定义了某网站的用户: struct User { active: bool, username: String, email: String, sign_in_count: u64,\n} 该结构体名称是 User,拥有 4 个字段,且每个字段都有对应的字段名及类型声明,例如 username 代表了用户名,是一个可变的 String 类型。 创建结构体实例 为了使用上述结构体,我们需要创建 User 结构体的 实例 : let user1 = User { email: String::from(\"someone@example.com\"), username: String::from(\"someusername123\"), active: true, sign_in_count: 1, }; 有几点值得注意: 初始化实例时, 每个字段 都需要进行初始化 初始化时的字段顺序 不需要 和结构体定义时的顺序一致 访问结构体字段 通过 . 操作符即可访问结构体实例内部的字段值,也可以修改它们: let mut user1 = User { email: String::from(\"someone@example.com\"), username: String::from(\"someusername123\"), active: true, sign_in_count: 1, }; user1.email = String::from(\"anotheremail@example.com\"); 需要注意的是,必须要将结构体实例声明为可变的,才能修改其中的字段,Rust 不支持将某个结构体某个字段标记为可变。 简化结构体创建 下面的函数类似一个构建函数,返回了 User 结构体的实例: fn build_user(email: String, username: String) -> User { User { email: email, username: username, active: true, sign_in_count: 1, }\n} 它接收两个字符串参数: email 和 username,然后使用它们来创建一个 User 结构体,并且返回。可以注意到这两行: email: email 和 username: username,非常的扎眼,因为实在有些啰嗦,如果你从 TypeScript 过来,肯定会鄙视 Rust 一番,不过好在,它也不是无可救药: fn build_user(email: String, username: String) -> User { User { email, username, active: true, sign_in_count: 1, }\n} 如上所示,当函数参数和结构体字段同名时,可以直接使用缩略的方式进行初始化,跟 TypeScript 中一模一样。 结构体更新语法 在实际场景中,有一种情况很常见:根据已有的结构体实例,创建新的结构体实例,例如根据已有的 user1 实例来构建 user2: let user2 = User { active: user1.active, username: user1.username, email: String::from(\"another@example.com\"), sign_in_count: user1.sign_in_count, }; 老话重提,如果你从 TypeScript 过来,肯定觉得啰嗦爆了:竟然手动把 user1 的三个字段逐个赋值给 user2,好在 Rust 为我们提供了 结构体更新语法: let user2 = User { email: String::from(\"another@example.com\"), ..user1 }; 因为 user2 仅仅在 email 上与 user1 不同,因此我们只需要对 email 进行赋值,剩下的通过结构体更新语法 ..user1 即可完成。 .. 语法表明凡是我们没有显式声明的字段,全部从 user1 中自动获取。需要注意的是 ..user1 必须在结构体的尾部使用。 结构体更新语法跟赋值语句 = 非常相像,因此在上面代码中,user1 的部分字段所有权被转移到 user2 中:username 字段发生了所有权转移,作为结果,user1 无法再被使用。 聪明的读者肯定要发问了:明明有三个字段进行了自动赋值,为何只有 username 发生了所有权转移? 仔细回想一下 所有权 那一节的内容,我们提到了 Copy 特征:实现了 Copy 特征的类型无需所有权转移,可以直接在赋值时进行 数据拷贝,其中 bool 和 u64 类型就实现了 Copy 特征,因此 active 和 sign_in_count 字段在赋值给 user2 时,仅仅发生了拷贝,而不是所有权转移。 值得注意的是:username 所有权被转移给了 user2,导致了 user1 无法再被使用,但是并不代表 user1 内部的其它字段不能被继续使用,例如: # #[derive(Debug)]\n# struct User {\n# active: bool,\n# username: String,\n# email: String,\n# sign_in_count: u64,\n# }\n# fn main() {\nlet user1 = User { email: String::from(\"someone@example.com\"), username: String::from(\"someusername123\"), active: true, sign_in_count: 1,\n};\nlet user2 = User { active: user1.active, username: user1.username, email: String::from(\"another@example.com\"), sign_in_count: user1.sign_in_count,\n};\nprintln!(\"{}\", user1.active);\n// 下面这行会报错\nprintln!(\"{:?}\", user1);\n# }","breadcrumbs":"Rust 基础入门 » 复合类型 » 结构体 » 结构体语法","id":"119","title":"结构体语法"},"1190":{"body":"一开始, Rust main 函数只能返回单元类型 ()(隐式或显式),总是指示成功的退出状态,如果您要你想要其它的,必须调用 process::exit(code)。从 Rust 1.26 开始, main 允许返回一个 Result ,其中 Ok 转换为 C EXIT_SUCCESS,Err 转换为 EXIT_FAILURE(也调试打印错误)。在底层,这些返回类型统一使用不稳定的 Termination 特征。 在此版本中,最终稳定了 Termination 特征,以及一个更通用的 ExitCode 类型,它封装了特定于平台的返回类型。它具有 SUCCESS 和 FAILURE 常量,并为更多任意值实现 From。也可以为您自己的类型实现 Termination 特征,允许您在转换为 ExitCode 之前定制任何类型的报告。 例如,下面是一种类型安全的方式来编写 git bisect 运行脚本的退出代码: use std::process::{ExitCode, Termination}; #[repr(u8)]\npub enum GitBisectResult { Good = 0, Bad = 1, Skip = 125, Abort = 255,\n} impl Termination for GitBisectResult { fn report(self) -> ExitCode { // Maybe print a message here ExitCode::from(self as u8) }\n} fn main() -> GitBisectResult { std::panic::catch_unwind(|| { todo!(\"test the commit\") }).unwrap_or(GitBisectResult::Abort)\n}","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.61 » 支持自定义 main 函数 ExitCode","id":"1190","title":"支持自定义 main 函数 ExitCode"},"1191":{"body":"这个版本稳定了几个增量特性,以支持 const 函数的更多功能: fn 指针的基本处理:现在可以在 const fn 中创建、传递和强制转换函数指针。例如,在为解释器构建编译时函数表时,这可能很有用。但是,仍然不允许调用 fn 指针。 特征约束:现在可以将特征约束写在 const fn 的泛型参数上,如 T: Copy,以前只允许 Sized。 dyn Trait 类型:类似地,const fn 现在可以处理特征对象 dyn Trait。 impl Trait 类型:const fn 的参数和返回值现在可以是不透明的 impl Trait 类型。 注意,特征特性还不支持在 const fn 中调用这些特征的方法。","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.61 » const fn 增强","id":"1191","title":"const fn 增强"},"1192":{"body":"三种标准 I/O 流 —— Stdin 、Stdout 和 Stderr —— 都有一个 锁(&self),允许对同步读写进行更多控制。但是,它们返回的锁守卫具有从 &self 借来的生命周期,因此它们被限制在原始句柄的范围内。这被认为是一个不必要的限制,因为底层锁实际上是在静态存储中,所以现在守卫返回一个 'static 生命期,与句柄断开连接。 例如,一个常见的错误来自于试图获取一个句柄并将其锁定在一个语句中: // error[E0716]: temporary value dropped while borrowed\nlet out = std::io::stdout().lock();\n// ^^^^^^^^^^^^^^^^^ - temporary value is freed at the end of this statement\n// |\n// creates a temporary which is freed while still in use 现在锁守卫是 'static,而不是借用那个临时的,所以这个可以正常工作!","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.61 » 为锁定的 stdio 提供静态句柄","id":"1192","title":"为锁定的 stdio 提供静态句柄"},"1193":{"body":"原文链接: https://blog.rust-lang.org/2022/06/30/Rust-1.62.0.html 翻译 by : AllanDowney 通过 rustup 安装的同学可以使用以下命令升级到 1.62 版本: $ rustup update stable","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.62 » Rust 新版解读 | 1.62 | 重点: Cargo add,#[default] 枚举变量,Linux 上更薄更快的 Mutex,裸机 x86_64 构架","id":"1193","title":"Rust 新版解读 | 1.62 | 重点: Cargo add,#[default] 枚举变量,Linux 上更薄更快的 Mutex,裸机 x86_64 构架"},"1194":{"body":"现在可以使用 cargo add 直接从命令行添加新的依赖项。此命令支持指定功能和版本。它还可以用来修改现有的依赖关系。 例如: $ cargo add log\n$ cargo add serde --features derive\n$ cargo add nom@5 有关更多信息,请参阅 cargo 文档 。","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.62 » Cargo add","id":"1194","title":"Cargo add"},"1195":{"body":"如果指定枚举默认变量,现在可以使用 #[derive(Default)]。例如,到目前为止,您必须手动为此枚举写入 Default : #[derive(Default)]\nenum Maybe { #[default] Nothing, Something(T),\n} 到目前为止,只允许将“单元”变量(没有字段的变量)标记为#[default]。 RFC 中提供了有关此功能的更多信息。","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.62 » #[default] 枚举变量","id":"1195","title":"#[default] 枚举变量"},"1196":{"body":"以前,Linux 上的 pthreads 库支持 Mutex、Condvar 和 RwLock 。 pthreads 锁 支持比 Rust API 本身更多的功能,包括运行时配置,并且设计用于比 Rust 提供的静态保证更少的语言中。 例如,Mutex 实现是 40 个字节,不能被移动(move)。这迫使标准库在后台为使用 pthreads 的平台的每个新 Mutex 分配一个 Box。 现在 Rust 的标准库在 Linux 上提供了这些锁的原始 futex 实现,它非常轻量级,不需要额外分配。在 1.62.0 中,Mutex 在 Linux 上的内部状态只需要 5 个字节,尽管在未来的版本中可能会发生变化。 这是提高 Rust 的锁类型效率的长期努力的一部分,包括以前在 Windows 上的改进,如取消绑定其原语。您可以在 跟踪问题 中了解更多有关这方面的信息。","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.62 » Linux 上更薄更快的 Mutex","id":"1196","title":"Linux 上更薄更快的 Mutex"},"1197":{"body":"现在更容易为 x86_64 构建无操作系统的二进制文件,例如在编写内核时。x86_64-unknown-none 构架已升级到第 2 层,可以用 rustup 安装。 $ rustup target add x86_64-unknown-none\n$ rustc --target x86_64-unknown-none my_no_std_program.rs 您可以在 Embedded Rust book 中阅读更多关于使用 no_std 进行开发的信息。","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.62 » 裸机 x86_64 构架","id":"1197","title":"裸机 x86_64 构架"},"1198":{"body":"Rust 1.63 官方 release doc: Announcing Rust 1.63.0 | Rust Blog 通过 rustup 安装的同学可以使用以下命令升级到 1.63 版本: $ rustup update stable","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.63 » Rust 新版解读 | 1.63 | 重点: Socped threads","id":"1198","title":"Rust 新版解读 | 1.63 | 重点: Socped threads"},"1199":{"body":"Rust 从 1.0 版本起,就可以使用 std::thread::spawn 来创建一个线程,但是这个函数要求了其生成的线程必须拥有任何传递进去的参数的所有权,也就是说你不能把引用数据传递进去。在一些线程会在方法末尾退出的情况下(通常使用 join() 方法),这个严格的约束显得不必要,在此之前也通常使用 Arc 包裹数据的的方法来妥协。 随着 1.63 版本的推出,标准库新增了 区域线程 ,允许在区域 scope 内创建使用当前调用栈内引用数据的线程。std::thread::scope 的API保证其中创建的线程会在自身返回前推出,也就允许安全的借用数据。看下面的例子,在 scope 内创建两个线程来,分别借用了数据: let mut a = vec![1, 2, 3];\nlet mut x = 0; std::thread::scope(|s| { s.spawn(|| { println!(\"hello from the first scoped thread\"); // 可以借用变量 `a` dbg!(&a); }); s.spawn(|| { println!(\"hello from the second scoped thread\"); // 没有其它线程在使用,所以也可以可变借用 `x` x += a[0] + a[2]; }); println!(\"hello from the main thread\");\n}); // Scope 退出后,可以继续修改、访问变量。\na.push(4);\nassert_eq!(x, a.len());","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.63 » 区域线程 Scoped threads","id":"1199","title":"区域线程 Scoped threads"},"12":{"body":"与其它语言相比,Rust 的更新迭代较为频繁(得益于精心设计过的发布流程以及 Rust 语言开发者团队的严格管理): 每 6 周发布一个迭代版本 2-3 年发布一个新的大版本,例如 Rust 2018 edition,Rust 2021 edition 好处在于,可以满足不同的用户群体的需求: 对于活跃的 Rust 用户,他们总是能很快获取到新的语言内容,毕竟,尝鲜是技术爱好者的共同特点:) 对于一般的用户,edition 大版本的发布会告诉他们:Rust 语言相比上次大版本发布,有了重大的改进,值得一看 对于 Rust 语言开发者,可以让他们的工作成果更快的被世人所知,不必锦衣夜行","breadcrumbs":"进入 Rust 编程世界 » Rust 语言版本更新","id":"12","title":"Rust 语言版本更新"},"120":{"body":"先来看以下代码: #[derive(Debug)] struct File { name: String, data: Vec, } fn main() { let f1 = File { name: String::from(\"f1.txt\"), data: Vec::new(), }; let f1_name = &f1.name; let f1_length = &f1.data.len(); println!(\"{:?}\", f1); println!(\"{} is {} bytes long\", f1_name, f1_length); } 上面定义的 File 结构体在内存中的排列如下图所示: 从图中可以清晰地看出 File 结构体两个字段 name 和 data 分别拥有底层两个 [u8] 数组的所有权(String 类型的底层也是 [u8] 数组),通过 ptr 指针指向底层数组的内存地址,这里你可以把 ptr 指针理解为 Rust 中的引用类型。 该图片也侧面印证了: 把结构体中具有所有权的字段转移出去后,将无法再访问该字段,但是可以正常访问其它的字段 。","breadcrumbs":"Rust 基础入门 » 复合类型 » 结构体 » 结构体的内存排列","id":"120","title":"结构体的内存排列"},"1200":{"body":"之前 Rust 代码在使用平台相关 API ,涉及到文件描述符(file descriptor on unix)或句柄(handles on windows)的时候,都是直接使用对应的描述符(比如,c_int alias RawFd)。因此类型系统无法判断 API 是会获取文件描述符的所有权,还是仅仅借用它。 现在,Rust 提供了封装类型诸如 BorrowedFd 和 OwnedFd。这些封装类型都标记为了 #[repr(transparent)],意味着 extern \"C\" 绑定下也可以直接使用这些类型来编码所有权语义。完整的封装类型参见原文下的 stabilized apis in 1.63","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.63 » Rust 对原始文件描述符/句柄的所有权","id":"1200","title":"Rust 对原始文件描述符/句柄的所有权"},"1201":{"body":"Condvar::new, Mutex::new 和 RwLock::new 可以在 const 上下文里被调用了,不必再使用 lazy_static 库来写全局静态的 Mutex, RwLock, Condvar 了。","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.63 » Mutex, RwLock, Condvar 作为静态变量","id":"1201","title":"Mutex, RwLock, Condvar 作为静态变量"},"1202":{"body":"诸如 fn foo(value: T, f: impl Copy) 的函数签名,使用 Turbofish foo::(3,3) 来指定 T 的具体类型会出现编译错误: error[E0632]: cannot provide explicit generic arguments when `impl Trait` is used in argument position --> src/lib.rs:4:11 |\n4 | foo::(3, 3); | ^^^ explicit generic argument not allowed | = note: see issue #83701 for more information 1.63里这个限制被放松了,显式泛型类型可以用 Turbofish 来指定了。不过 impl Trait 参数,尽管已经脱糖(desugare)成了泛型,因为还是不透明的所以无法通过 Turbofish 指定。","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.63 » Turbofish 可用于含有 impl Trait 的泛型函数上","id":"1202","title":"Turbofish 可用于含有 impl Trait 的泛型函数上"},"1203":{"body":"1.63 的rustc,完全删除了之前的词法借用检查,完全启用了新的 NLL 借用检查器。这不会对编译结果有任何变化,但对编译器的借用错误检查有优化效果。 如果对NLL不了解,在本书 引用与借用 一章里有介绍。 或者看官方博客的介绍 NLL 更详细内容可以看原博客 blog","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.63 » 完成了 Non-lexical-lifetime 的生命周期检查器的迁移","id":"1203","title":"完成了 Non-lexical-lifetime 的生命周期检查器的迁移"},"1204":{"body":"Rust 1.64 官方 release doc: Announcing Rust 1.64.0 | Rust Blog 通过 rustup 安装的同学可以使用以下命令升级到 1.64 版本: $ rustup update stable","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.64 » Rust 新版解读 | 1.64 | 重点: IntoFuture , Cargo 优化","id":"1204","title":"Rust 新版解读 | 1.64 | 重点: IntoFuture , Cargo 优化"},"1205":{"body":"1.64 稳定了 IntoFuture trait,不同于用在 for ... in ... 的 IntoIterator trait,IntoFuture 增强了 .awiat 关键字。现在 .await 可以 await 除了 futures 外,还可以 await 任何实现了 IntoFuture trait 并经此转换成 Future 的对象。这可以让你的 api 对用户更加优化。 举一个用在网络存储供应端的例子: pub struct Error { ... }\npub struct StorageResponse { ... }:\npub struct StorageRequest(bool); impl StorageRequest { /// 实例化一个 `StorageRequest` pub fn new() -> Self { ... } /// 是否开启 debug 模式 pub fn set_debug(self, b: bool) -> Self { ... } /// 发送请求并接受回复 pub async fn send(self) -> Result { ... }\n} 通常地使用方法可能类似如下代码: let response = StorageRequest::new() // 1. 实例化 .set_debug(true) // 2. 设置一些选项 .send() // 3. 构造 future .await?; // 4. 执行 future ,传递 error 这个代码已经不错了,不过 1.64 后可以做的更好。使用 IntoFuture ,把第三步的 “构造 future ” 和 第四步的 “执行 future ” 合并到一个步骤里: let response = StorageRequest::new() // 1. 实例化 .set_debug(true) // 2. 设置一些选项 .await?; // 3. 构造并执行 future ,传递 error 想要实现上面的效果,我们需要给 StorageRequest 实现 IntoFuture trait。IntoFuture 需要确定好要返回的 future,可以用下面的代码来实现: // 首先需要引入一些必须的类型\nuse std::pin::Pin;\nuse std::future::{Future, IntoFuture}; pub struct Error { ... }\npub struct StorageResponse { ... }\npub struct StorageRequest(bool); impl StorageRequest { /// 实例化一个 `StorageRequest` pub fn new() -> Self { ... } /// 是否开启 debug 模式 pub fn set_debug(self, b: bool) -> Self { ... } /// 发送请求并接受回复 pub async fn send(self) -> Result { ... }\n} // 新的实现内容\n// 1. 定义好返回的 future 类型\npub type StorageRequestFuture = Pin> + Send + 'static>>\n// 2. 给 `StorageRequest` 实现 `IntoFuture`\nimpl IntoFuture for StorageRequest { type IntoFuture = StorageRequestFuture; type Output = ::Output; fn into_future(self) -> Self::IntoFuture { Box::pin(self.send()) }\n} 这确实需要多写一点实现代码,不过可以给用户提供一个更简单的 api 。 未来,Rust 异步团队 希望能够通过给类型别名提供 impl Trait Type Alias Impl Trait ,来简化定义 futures 实现 IntoFuture 的代码;再想办法移除 Box 来提升性能。","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.64 » 使用 IntoFuture 增强 .await","id":"1205","title":"使用 IntoFuture 增强 .await"},"1206":{"body":"当调用 C-ABI 或者调用 C-ABI 的时候,Rust 代码通常会使用诸如 c_uint 或者 c_ulong 的类型别名来匹配目标语言里的对应类型。 在次之前,这些类型别名仅在 std 里可用,而在嵌入式或者其它仅能使用 core 或者 alloc 的场景下无法使用。 1.64 里在 core::ffi 里提供了所有 c_* 的类型别名,还有 core::ffi::CStr 对应 C 的字符串,还有仅用 alloc 库情况下可以用 alloc::ffi::CString 来对应 C 的字符串。","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.64 » core 和 alloc 中和 C 语言兼容的 FFI 类型","id":"1206","title":"core 和 alloc 中和 C 语言兼容的 FFI 类型"},"1207":{"body":"rust-analyzer 现在被加进 Rust 工具集里了。这让在各平台上下载使用 rust-analyzer 更加方便。通过 rustup component 来安装: rustup component add rust-analyzer 目前,使用 rustup 安装的版本,需要这样启用: rustup run stable rust-analyzer 下一次 rustup 的发布本把会提供一个内置的代理,来运行对应版本的 rust-analyzer 。","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.64 » 可以通过 rustup 来使用 rust-analyzer","id":"1207","title":"可以通过 rustup 来使用 rust-analyzer"},"1208":{"body":"当在一个 Cargo workspace 里管理多个相关的库/产品时,现在可以避免在多个库里使用相同的字段值了,比如相同的版本号,仓库链接,rust-version。在更新的时候也可以更容易地保持这些信息地一致性。更多细节可以参考: workspace.package workspace.dependencies \"inheriting a dependency from a workspace\" 另外在构建多个目标地时候,现在可以直接传递多个 --target 选项给 cargo build 来一次性编译所有目标。也可以在 .cargo/config.toml 里设置一个 build.target 的 array 来改变默认构建时的对象。","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.64 » Cargo 优化,workspace 继承和多目标构建","id":"1208","title":"Cargo 优化,workspace 继承和多目标构建"},"1209":{"body":"更多稳定API列表和其它更新内容,请参考原文最后 stabilized-apis","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.64 » 稳定API && Others","id":"1209","title":"稳定API && Others"},"121":{"body":"结构体必须要有名称,但是结构体的字段可以没有名称,这种结构体长得很像元组,因此被称为元组结构体,例如: struct Color(i32, i32, i32); struct Point(i32, i32, i32); let black = Color(0, 0, 0); let origin = Point(0, 0, 0); 元组结构体在你希望有一个整体名称,但是又不关心里面字段的名称时将非常有用。例如上面的 Point 元组结构体,众所周知 3D 点是 (x, y, z) 形式的坐标点,因此我们无需再为内部的字段逐一命名为:x, y, z。","breadcrumbs":"Rust 基础入门 » 复合类型 » 结构体 » 元组结构体(Tuple Struct)","id":"121","title":"元组结构体(Tuple Struct)"},"1210":{"body":"Rust 1.65 官方 release doc: Announcing Rust 1.65.0 | Rust Blog 通过 rustup 安装的同学可以使用以下命令升级到 1.65 版本: $ rustup update stable","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.65 » Rust 新版解读 | 1.65 | 重点: 泛型关联类型,新绑定语法!","id":"1210","title":"Rust 新版解读 | 1.65 | 重点: 泛型关联类型,新绑定语法!"},"1211":{"body":"关联类型(associated types)里现在可以加上生命周期、类型、const泛型了,类似于: trait Foo { type Bar<'x>;\n} 三言两语说不清这个变化的好处,看几个例子来感受一下: /// 一个类似于 `Iterator` 的 trait ,可以借用 `Self`。\ntrait LendingIterator { type Item<'a> where Self: 'a; fn next<'a>(&'a mut self) -> Option>;\n} /// 可以给智能指针类型,比如 `Rc` 和 `Arc` 实现的 trait,来实现指针类型的泛用性\ntrait PointerFamily { type Pointer: Deref; fn new(value: T) -> Self::Pointer;\n} /// 允许借用数组对象,对不需要连续存储数据的固定长度数组类型很有用\ntrait BorrowArray { type Array<'x, const N: usize> where Self: 'x; fn borrow_array<'a, const N: usize>(&'a self) -> Self::Array<'a, N>;\n} 泛型关联类型十分通用,能够写出许多之前无法实现的模式。更多的信息可以参考下面的链接: 2021/08/03/GAT稳定版本推进 2022/10/28/GAT稳定版本发布公告 第一个对上面的例子进行了更深入的讨论,第二个讨论了一些已知的局限性。 更深入的阅读可以在关联类型的 nightly reference 和 原始 RFC (已经过去6.5年了!) 里找到。","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.65 » 泛型关联类型 Generic associated types (GATs)","id":"1211","title":"泛型关联类型 Generic associated types (GATs)"},"1212":{"body":"新的 let 语法,尝试模式匹配,找不到匹配的情况下执行发散的 else 块。 let PATTERN: TYPE = EXPRESSION else { DIVERGING_CODE;\n}; 常规的 let 语法仅能使用 irrefutable patterns,直译为不可反驳的模式,也就是一定要匹配上。一般情况下都是单个变量绑定,也用在解开结构体,元组,数组等复合类型上。原先并不适用条件匹配,比如从枚举里确定枚举值。直到现在我们有了 let - else。这是 refutable pattern,直译为可反驳的模式,能够像常规 let 一样匹配并绑定变量到周围范围内,在模式不匹配的时候执行发送的 else (可以是 break, return, panic!)。 fn get_count_item(s: &str) -> (u64, &str) { let mut it = s.split(' '); let (Some(count_str), Some(item)) = (it.next(), it.next()) else { panic!(\"Can't segment count item pair: '{s}'\"); }; let Ok(count) = u64::from_str(count_str) else { panic!(\"Can't parse integer: '{count_str}'\"); }; (count, item)\n}\nassert_eq!(get_count_item(\"3 chairs\"), (3, \"chairs\")); if - else 和 match 或者 if let 最大不一样的地方是变量绑定的范围,在此之前你需要多写一点重复的代码和一次外层的 let 绑定来完成: let (count_str, item) = match (it.next(), it.next()) { (Some(count_str), Some(item)) => (count_str, item), _ => panic!(\"Can't segment count item pair: '{s}'\"), }; let count = if let Ok(count) = u64::from_str(count_str) { count } else { panic!(\"Can't parse integer: '{count_str}'\"); };","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.65 » let - else 语法","id":"1212","title":"let - else 语法"},"1213":{"body":"块表达式现在可以标记为 break 的目标,来达到提前终止块的目的。这听起来有点像 goto 语法,不过这并不是随意的跳转,只能从块里跳转到块末尾。这在之前已经可以用 loop 块来实现了,你可能大概率见过这种总是只执行一次的 loop。 1.65 可以直接给块语句添加标记来提前退出了,还可以携带返回值: let result = 'block: { do_thing(); if condition_not_met() { break 'block 1; } do_next_thing(); if condition_not_met() { break 'block 2; } do_last_thing(); 3\n};","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.65 » break 跳出标记过的代码块","id":"1213","title":"break 跳出标记过的代码块"},"1214":{"body":"其它更新细节,和稳定的API列表,参考 原Blog","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.65 » Others","id":"1214","title":"Others"},"1215":{"body":"Rust 1.66 官方 release doc: Announcing Rust 1.66.0 | Rust Blog 通过 rustup 安装的同学可以使用以下命令升级到 1.66 版本: $ rustup update stable","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.66 » Rust 新版解读 | 1.66 | 重点: 有字段枚举的显示判别","id":"1215","title":"Rust 新版解读 | 1.66 | 重点: 有字段枚举的显示判别"},"1216":{"body":"枚举的显示判别在跨语言传递值时很关键,需要两个语言里每个枚举值的判别是一致的,比如: #[repr(u8)]\nenum Bar { A, B, C = 42, D,\n} 这个例子里,枚举 Bar 使用了 u8 作为原语表形(representation),并且 Bar::C 使用 42 来判别,其它没有显示判别的枚举值会按照源码里地顺序自动地递增赋值,这里的 Bar::A 是0,Bar::B 是1,Bar::D 是43。如果没有显示判别,那就只能在 Bar::B 和 Bar::C 之间加上 40 个无意义的枚举值了。 在1.66之前,枚举的显示判别只能用在无字段枚举上。现在对有字段枚举的显示判别也稳定了: #[repr(u8)]\nenum Foo { A(u8), B(i8), C(bool) = 42,\n} 注意:可以通过 as 转换(比如 Bar::C as u8 )来判断一个无字段枚举的判别值,但是 Rust 还没有给有字段枚举提供语言层面上的获取原始判别值的方法,只能通过 unsafe 的代码来检查有字段枚举的判别值。考虑到这个使用场景往往出现在必须使用 unsafe 代码的跨语言的 FFI 里,希望这没有造成太大的负担。如果你的确需要的话,参考 std::mem::discriminant。","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.66 » 对有字段枚举的显示判别","id":"1216","title":"对有字段枚举的显示判别"},"1217":{"body":"当对编译器产生的代码做基准测试时,常常需要阻止一些优化,比如下面的代码里, push_cap 在一个循环里执行了4次 Vec::push : fn push_cap(v: &mut Vec) { for i in 0..4 { v.push(i); }\n} pub fn bench_push() -> Duration { let mut v = Vec::with_capacity(4); let now = Instant::now(); push_cap(&mut v); now.elapsed()\n} 如果你检查一下在 x86_64 机器上编译的优化输出结果,你会注意到整个 push_cap 方法都被优化掉了... example::bench_push: sub rsp, 24 call qword ptr [rip + std::time::Instant::now@GOTPCREL] lea rdi, [rsp + 8] mov qword ptr [rsp + 8], rax mov dword ptr [rsp + 16], edx call qword ptr [rip + std::time::Instant::elapsed@GOTPCREL] add rsp, 24 ret 现在可以通过调用 black_box 来避免类似情况的发送。 虽然实际上 black_box 内部只会取走值并直接返回,但是编译器会认为这个方法可能做任何事情。 use std::hint::black_box; fn push_cap(v: &mut Vec) { for i in 0..4 { v.push(i); black_box(v.as_ptr()); }\n} 这样就可以得到展开循环的 结果 : mov dword ptr [rbx], 0 mov qword ptr [rsp + 8], rbx mov dword ptr [rbx + 4], 1 mov qword ptr [rsp + 8], rbx mov dword ptr [rbx + 8], 2 mov qword ptr [rsp + 8], rbx mov dword ptr [rbx + 12], 3 mov qword ptr [rsp + 8], rbx 你还能发现结果里有 black_box 带来的副作用,无意义的 mov qword ptr [rsp + 8], rbx 指令在每一次循环后出现,用来获取 v.as_ptr() 作为参数传递给并未真正使用的方法。 注意到上面的例子里,push 指令都不用考虑内存分配的问题,这是因为编译器运行在 Vec::with_capacity(4) 的条件下。你可以尝试改动一下 black_box 的位置或者在多处使用,来看看其对编译的优化输出的影响。","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.66 » 黑盒方法 core::hint::black_box","id":"1217","title":"黑盒方法 core::hint::black_box"},"1218":{"body":"1.62里我们引入了 cargo add 来通过命令行给你的项目增加依赖项。现在可以使用 cargo remove 来移除依赖了。","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.66 » cargo remove","id":"1218","title":"cargo remove"},"1219":{"body":"其它更新细节,和稳定的API列表,参考 原Blog","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.66 » Others","id":"1219","title":"Others"},"122":{"body":"还记得之前讲过的基本没啥用的 单元类型 吧?单元结构体就跟它很像,没有任何字段和属性,但是好在,它还挺有用。 如果你定义一个类型,但是不关心该类型的内容, 只关心它的行为时,就可以使用 单元结构体: struct AlwaysEqual; let subject = AlwaysEqual; // 我们不关心 AlwaysEqual 的字段数据,只关心它的行为,因此将它声明为单元结构体,然后再为它实现某个特征\nimpl SomeTrait for AlwaysEqual { }","breadcrumbs":"Rust 基础入门 » 复合类型 » 结构体 » 单元结构体(Unit-like Struct)","id":"122","title":"单元结构体(Unit-like Struct)"},"1220":{"body":"Rust 1.67 官方 release doc: Announcing Rust 1.67.0 | Rust Blog 通过 rustup 安装的同学可以使用以下命令升级到 1.67 版本: $ rustup update stable 2023新年好!大年初五更新的新版本,来看看有什么新变化~","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.67 » Rust 新版解读 | 1.67 | #[must_use] in async fn","id":"1220","title":"Rust 新版解读 | 1.67 | #[must_use] in async fn"},"1221":{"body":"注明了 #[must_use] 的 async 函数会把该属性应用在返回的 impl Future 结果上。Future trait 已经注明了 #[must_use] ,所以所有实现了 Future 的类型都会自动加上 #[must_use]。 所以在 1.67 版本,编译器会警告返回值没有被使用: #[must_use]\nasync fn bar() -> u32 { 0 } async fn caller() { bar().await;\n} warning: unused output of future returned by `bar` that must be used --> src/lib.rs:5:5 |\n5 | bar().await; | ^^^^^^^^^^^ | = note: `#[warn(unused_must_use)]` on by default","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.67 » #[must_use] 作用于 async fn 上","id":"1221","title":"#[must_use] 作用于 async fn 上"},"1222":{"body":"标准库里的 mpsc(多生产者单消费者) 通道自从 1.0 版本就有了,这次版本更新将其实现修改成了基于 crossbeam-channel 。不涉及到API的变更,但是修改了一些已有的bug,提升了性能和代码可维护性。用户应该不太会感知到明显的变化。","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.67 » std::sync::mpsc 实现更新","id":"1222","title":"std::sync::mpsc 实现更新"},"1223":{"body":"其它更新细节,和稳定的API列表,参考 原Blog","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.67 » Others","id":"1223","title":"Others"},"1224":{"body":"Rust 1.68 官方 release doc: Announcing Rust 1.68.0 | Rust Blog 通过 rustup 安装的同学可以使用以下命令升级到 1.68 版本: $ rustup update stable","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.68 » Rust 新版解读 | 1.68 | crates index 优化","id":"1224","title":"Rust 新版解读 | 1.68 | crates index 优化"},"1225":{"body":"Cargo的“稀疏”注册协议已经稳定,它是用来读取注册在 crates.io 上的 crates 的索引的基础设施。以前的 git 协议(目前仍然是默认协议)会克隆一个包括所有 crates 的索引的仓库,但这已经开始遇到扩展限制问题,在更新该仓库时会出现明显的延迟。新协议应在访问 crates.io 时提供显着的性能提升,因为它只会下载有关实际用到的 crates 的索引。 要使用新的协议,需要设置环境变量 CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse ,或者编辑 .cargo/config.toml 文件添加: [registries.crates-io]\nprotocol = \"sparse\" 稀疏注册协议目前计划于 1.70.0 版本成为默认的协议。更多细节可以看官方博客的 announcement , RFC 2789 , 当前 Cargo Book 的 文档","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.68 » Cargo 稀疏注册协议 (sparse protocol)","id":"1225","title":"Cargo 稀疏注册协议 (sparse protocol)"},"1226":{"body":"新增的 pin! 宏能够用 T 构造一个 Pin<&mut T> ,从而匿名捕获在局部状态内。这通常叫做 堆栈固定(stack-pinning),同时这个堆栈也可以被 async fn 或者 代码块 来捕获住。这个宏和一些 crates 里提供的(比如 tokio::pin!)很像,但是标准库可以利用 Pin 的内部结构和 临时生命周期拓展( Temporary lifetime extension )来实现出更像表达式的宏。 /// Runs a future to completion.\nfn block_on(future: F) -> F::Output { let waker_that_unparks_thread = todo!(); let mut cx = Context::from_waker(&waker_that_unparks_thread); // Pin the future so it can be polled. let mut pinned_future = pin!(future); loop { match pinned_future.as_mut().poll(&mut cx) { Poll::Pending => thread::park(), Poll::Ready(result) => return result, } }\n} 在这个例子中,原来的 future 将被移动到一个临时的局部区域,由新的 pinned_future 引用,类型为 Pin<&mut F>,并且该 pin 受制于正常的借用检查器以确保它不会超过局部作用域。","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.68 » 局部 Pin 构造","id":"1226","title":"局部 Pin 构造"},"1227":{"body":"当 Rust 内存分配失败时,类似于 Box::new 和 Vec::push 的 API 无法反映出这个错误,从而采取了一些不同的措施。当使用 std 时,程序会打印 stderr 然后中止。从 Rust 1.68.0 开始,包含 std 的二进制程序仍然会继续这样,而不保护 std 只包含 alloc 的二进制程序会对内存分配错误调用 panic!,如果需要可以再进一步通过 #[panic_handler] 来调整其行为。 未来,std 也可能会改成这样。","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.68 » alloc 默认错误处理","id":"1227","title":"alloc 默认错误处理"},"1228":{"body":"其它更新细节,和稳定的API列表,参考 原Blog","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.68 » Others","id":"1228","title":"Others"},"1229":{"body":"Rust 1.69 官方 release doc: Announcing Rust 1.69.0 | Rust Blog 通过 rustup 安装的同学可以使用以下命令升级到 1.69 版本: $ rustup update stable","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.69 » Rust 新版解读 | 1.69 | cargo fix","id":"1229","title":"Rust 新版解读 | 1.69 | cargo fix"},"123":{"body":"在之前的 User 结构体的定义中,有一处细节:我们使用了自身拥有所有权的 String 类型而不是基于引用的 &str 字符串切片类型。这是一个有意而为之的选择:因为我们想要这个结构体拥有它所有的数据,而不是从其它地方借用数据。 你也可以让 User 结构体从其它对象借用数据,不过这么做,就需要引入 生命周期(lifetimes) 这个新概念(也是一个复杂的概念),简而言之,生命周期能确保结构体的作用范围要比它所借用的数据的作用范围要小。 总之,如果你想在结构体中使用一个引用,就必须加上生命周期,否则就会报错: struct User { username: &str, email: &str, sign_in_count: u64, active: bool,\n} fn main() { let user1 = User { email: \"someone@example.com\", username: \"someusername123\", active: true, sign_in_count: 1, };\n} 编译器会抱怨它需要生命周期标识符: error[E0106]: missing lifetime specifier --> src/main.rs:2:15 |\n2 | username: &str, | ^ expected named lifetime parameter // 需要一个生命周期 |\nhelp: consider introducing a named lifetime parameter // 考虑像下面的代码这样引入一个生命周期 |\n1 ~ struct User<'a> {\n2 ~ username: &'a str, | error[E0106]: missing lifetime specifier --> src/main.rs:3:12 |\n3 | email: &str, | ^ expected named lifetime parameter |\nhelp: consider introducing a named lifetime parameter |\n1 ~ struct User<'a> {\n2 | username: &str,\n3 ~ email: &'a str, | 未来在 生命周期 中会讲到如何修复这个问题以便在结构体中存储引用,不过在那之前,我们会避免在结构体中使用引用类型。","breadcrumbs":"Rust 基础入门 » 复合类型 » 结构体 » 结构体数据的所有权","id":"123","title":"结构体数据的所有权"},"1230":{"body":"在 Rust 1.29.0 版本添加的 cargo fix 子命令,能够自动修复一些简单的编译错误。从那以后,能够自动修复的错误/警告原因的数量一直在稳步增加。此外,还增加了对自动修复一些简单的 Clippy 警告的支持。 为了让更多人注意到这些能力,现在当检测到可自动修复的错误时,Cargo 会建议运行 cargo fix 或 cargo clippy --fix 命令: warning: unused import: `std::hash::Hash` --> src/main.rs:1:5 |\n1 | use std::hash::Hash; | ^^^^^^^^^^^^^^^ | = note: `#[warn(unused_imports)]` on by default warning: `foo` (bin \"foo\") generated 1 warning (run `cargo fix --bin \"foo\"` to apply 1 suggestion) 注意上面的完整命令(即包含 --bin foo)仅在你想要精确修复一个单独的 crate 时需要附上。默认执行 workspace 下所有 fixs 只需要 cargo fix 。","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.69 » Cargo 提供自动修复建议","id":"1230","title":"Cargo 提供自动修复建议"},"1231":{"body":"为了提高编译速度,Cargo 现在默认避免在构建脚本中发出调试信息。构建脚本成功执行时不会有可见的效果,但构建脚本中的回溯(backtraces)将包含更少的信息。 所以如果想要 debug 构建脚本,需要额外开启调试信息,在 Cargo.toml 文件里添加 [profile.dev.build-override]\ndebug = true\n[profile.release.build-override]\ndebug = true","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.69 » 构建脚步默认不再包含调试信息","id":"1231","title":"构建脚步默认不再包含调试信息"},"1232":{"body":"其它更新细节,和稳定的API列表,参考 原Blog","breadcrumbs":"Appendix » Rust 历次版本更新解读 » 1.69 » Others","id":"1232","title":"Others"},"124":{"body":"在前面的代码中我们使用 #[derive(Debug)] 对结构体进行了标记,这样才能使用 println!(\"{:?}\", s); 的方式对其进行打印输出,如果不加,看看会发生什么: struct Rectangle { width: u32, height: u32,\n} fn main() { let rect1 = Rectangle { width: 30, height: 50, }; println!(\"rect1 is {}\", rect1);\n} 首先可以观察到,上面使用了 {} 而不是之前的 {:?},运行后报错: error[E0277]: `Rectangle` doesn't implement `std::fmt::Display` 提示我们结构体 Rectangle 没有实现 Display 特征,这是因为如果我们使用 {} 来格式化输出,那对应的类型就必须实现 Display 特征,以前学习的基本类型,都默认实现了该特征: fn main() { let v = 1; let b = true; println!(\"{}, {}\", v, b);\n} 上面代码不会报错,那么结构体为什么不默认实现 Display 特征呢?原因在于结构体较为复杂,例如考虑以下问题:你想要逗号对字段进行分割吗?需要括号吗?加在什么地方?所有的字段都应该显示?类似的还有很多,由于这种复杂性,Rust 不希望猜测我们想要的是什么,而是把选择权交给我们自己来实现:如果要用 {} 的方式打印结构体,那就自己实现 Display 特征。 接下来继续阅读报错: = help: the trait `std::fmt::Display` is not implemented for `Rectangle`\n= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead 上面提示我们使用 {:?} 来试试,这个方式我们在本文的前面也见过,下面来试试: println!(\"rect1 is {:?}\", rect1); 可是依然无情报错了: error[E0277]: `Rectangle` doesn't implement `Debug` 好在,聪明的编译器又一次给出了提示: = help: the trait `Debug` is not implemented for `Rectangle`\n= note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle` 让我们实现 Debug 特征,Oh No,就是不想实现 Display 特征,才用的 {:?},怎么又要实现 Debug,但是仔细看,提示中有一行: add #[derive(Debug)] to Rectangle, 哦?这不就是我们前文一直在使用的吗? 首先,Rust 默认不会为我们实现 Debug,为了实现,有两种方式可以选择: 手动实现 使用 derive 派生实现 后者简单的多,但是也有限制,具体见 附录 D ,这里我们就不再深入讲解,来看看该如何使用: #[derive(Debug)]\nstruct Rectangle { width: u32, height: u32,\n} fn main() { let rect1 = Rectangle { width: 30, height: 50, }; println!(\"rect1 is {:?}\", rect1);\n} 此时运行程序,就不再有错误,输出如下: $ cargo run\nrect1 is Rectangle { width: 30, height: 50 } 这个输出格式看上去也不赖嘛,虽然未必是最好的。这种格式是 Rust 自动为我们提供的实现,看上基本就跟结构体的定义形式一样。 当结构体较大时,我们可能希望能够有更好的输出表现,此时可以使用 {:#?} 来替代 {:?},输出如下: rect1 is Rectangle { width: 30, height: 50,\n} 此时结构体的输出跟我们创建时候的代码几乎一模一样了!当然,如果大家还是不满足,那最好还是自己实现 Display 特征,以向用户更美的展示你的私藏结构体。关于格式化输出的更多内容,我们强烈推荐看看这个 章节 。 还有一个简单的输出 debug 信息的方法,那就是使用 dbg! 宏 ,它会拿走表达式的所有权,然后打印出相应的文件名、行号等 debug 信息,当然还有我们需要的表达式的求值结果。 除此之外,它最终还会把表达式值的所有权返回! dbg! 输出到标准错误输出 stderr,而 println! 输出到标准输出 stdout。 下面的例子中清晰的展示了 dbg! 如何在打印出信息的同时,还把表达式的值赋给了 width: #[derive(Debug)]\nstruct Rectangle { width: u32, height: u32,\n} fn main() { let scale = 2; let rect1 = Rectangle { width: dbg!(30 * scale), height: 50, }; dbg!(&rect1);\n} 最终的 debug 输出如下: $ cargo run\n[src/main.rs:10] 30 * scale = 60\n[src/main.rs:14] &rect1 = Rectangle { width: 60, height: 50,\n} 可以看到,我们想要的 debug 信息几乎都有了:代码所在的文件名、行号、表达式以及表达式的值,简直完美!","breadcrumbs":"Rust 基础入门 » 复合类型 » 结构体 » 使用 #[derive(Debug)] 来打印结构体的信息","id":"124","title":"使用 #[derive(Debug)] 来打印结构体的信息"},"125":{"body":"Rust By Practice ,支持代码在线编辑和运行,并提供详细的 习题解答 。","breadcrumbs":"Rust 基础入门 » 复合类型 » 结构体 » 课后练习","id":"125","title":"课后练习"},"126":{"body":"枚举(enum 或 enumeration)允许你通过列举可能的成员来定义一个 枚举类型 ,例如扑克牌花色: enum PokerSuit { Clubs, Spades, Diamonds, Hearts,\n} 如果在此之前你没有在其它语言中使用过枚举,那么可能需要花费一些时间来理解这些概念,一旦上手,就会发现枚举的强大,甚至对它爱不释手,枚举虽好,可不要滥用哦。 再回到之前创建的 PokerSuit,扑克总共有四种花色,而这里我们枚举出所有的可能值,这也正是 枚举 名称的由来。 任何一张扑克,它的花色肯定会落在四种花色中,而且也只会落在其中一个花色上,这种特性非常适合枚举的使用,因为 枚举值 只可能是其中某一个成员。抽象来看,四种花色尽管是不同的花色,但是它们都是扑克花色这个概念,因此当某个函数处理扑克花色时,可以把它们当作相同的类型进行传参。 细心的读者应该注意到,我们对之前的 枚举类型 和 枚举值 进行了重点标注,这是因为对于新人来说容易混淆相应的概念,总而言之: 枚举类型是一个类型,它会包含所有可能的枚举成员, 而枚举值是该类型中的具体某个成员的实例。","breadcrumbs":"Rust 基础入门 » 复合类型 » 枚举 » 枚举","id":"126","title":"枚举"},"127":{"body":"现在来创建 PokerSuit 枚举类型的两个成员实例: let heart = PokerSuit::Hearts;\nlet diamond = PokerSuit::Diamonds; 我们通过 :: 操作符来访问 PokerSuit 下的具体成员,从代码可以清晰看出,heart 和 diamond 都是 PokerSuit 枚举类型的,接着可以定义一个函数来使用它们: fn main() { let heart = PokerSuit::Hearts; let diamond = PokerSuit::Diamonds; print_suit(heart); print_suit(diamond);\n} fn print_suit(card: PokerSuit) { println!(\"{:?}\",card);\n} print_suit 函数的参数类型是 PokerSuit,因此我们可以把 heart 和 diamond 传给它,虽然 heart 是基于 PokerSuit 下的 Hearts 成员实例化的,但是它是货真价实的 PokerSuit 枚举类型。 接下来,我们想让扑克牌变得更加实用,那么需要给每张牌赋予一个值:A(1)-K(13),这样再加上花色,就是一张真实的扑克牌了,例如红心 A。 目前来说,枚举值还不能带有值,因此先用结构体来实现: enum PokerSuit { Clubs, Spades, Diamonds, Hearts,\n} struct PokerCard { suit: PokerSuit, value: u8\n} fn main() { let c1 = PokerCard { suit: PokerSuit::Clubs, value: 1, }; let c2 = PokerCard { suit: PokerSuit::Diamonds, value: 12, };\n} 这段代码很好的完成了它的使命,通过结构体 PokerCard 来代表一张牌,结构体的 suit 字段表示牌的花色,类型是 PokerSuit 枚举类型,value 字段代表扑克牌的数值。 可以吗?可以!好吗?说实话,不咋地,因为还有简洁得多的方式来实现: enum PokerCard { Clubs(u8), Spades(u8), Diamonds(u8), Hearts(u8),\n} fn main() { let c1 = PokerCard::Spades(5); let c2 = PokerCard::Diamonds(13);\n} 直接将数据信息关联到枚举成员上,省去近一半的代码,这种实现是不是更优雅? 不仅如此,同一个枚举类型下的不同成员还能持有不同的数据类型,例如让某些花色打印 1-13 的字样,另外的花色打印上 A-K 的字样: enum PokerCard { Clubs(u8), Spades(u8), Diamonds(char), Hearts(char),\n} fn main() { let c1 = PokerCard::Spades(5); let c2 = PokerCard::Diamonds('A');\n} 回想一下,遇到这种不同类型的情况,再用我们之前的结构体实现方式,可行吗?也许可行,但是会复杂很多。 再来看一个来自标准库中的例子: struct Ipv4Addr { // --snip--\n} struct Ipv6Addr { // --snip--\n} enum IpAddr { V4(Ipv4Addr), V6(Ipv6Addr),\n} 这个例子跟我们之前的扑克牌很像,只不过枚举成员包含的类型更复杂了,变成了结构体:分别通过 Ipv4Addr 和 Ipv6Addr 来定义两种不同的 IP 数据。 从这些例子可以看出, 任何类型的数据都可以放入枚举成员中 : 例如字符串、数值、结构体甚至另一个枚举。 增加一些挑战?先看以下代码: enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32),\n} fn main() { let m1 = Message::Quit; let m2 = Message::Move{x:1,y:1}; let m3 = Message::ChangeColor(255,255,0);\n} 该枚举类型代表一条消息,它包含四个不同的成员: Quit 没有任何关联数据 Move 包含一个匿名结构体 Write 包含一个 String 字符串 ChangeColor 包含三个 i32 当然,我们也可以用结构体的方式来定义这些消息: struct QuitMessage; // 单元结构体\nstruct MoveMessage { x: i32, y: i32,\n}\nstruct WriteMessage(String); // 元组结构体\nstruct ChangeColorMessage(i32, i32, i32); // 元组结构体 由于每个结构体都有自己的类型,因此我们无法在需要同一类型的地方进行使用,例如某个函数它的功能是接受消息并进行发送,那么用枚举的方式,就可以接收不同的消息,但是用结构体,该函数无法接受 4 个不同的结构体作为参数。 而且从代码规范角度来看,枚举的实现更简洁,代码内聚性更强,不像结构体的实现,分散在各个地方。","breadcrumbs":"Rust 基础入门 » 复合类型 » 枚举 » 枚举值","id":"127","title":"枚举值"},"128":{"body":"最后,再用一个实际项目中的简化片段,来结束枚举类型的语法学习。 例如我们有一个 WEB 服务,需要接受用户的长连接,假设连接有两种:TcpStream 和 TlsStream,但是我们希望对这两个连接的处理流程相同,也就是用同一个函数来处理这两个连接,代码如下: fn new (stream: TcpStream) { let mut s = stream; if tls { s = negotiate_tls(stream) } // websocket是一个WebSocket或者 // WebSocket>类型 websocket = WebSocket::from_raw_socket( stream, ......)\n} 此时,枚举类型就能帮上大忙: enum Websocket { Tcp(Websocket), Tls(Websocket>),\n}","breadcrumbs":"Rust 基础入门 » 复合类型 » 枚举 » 同一化类型","id":"128","title":"同一化类型"},"129":{"body":"在其它编程语言中,往往都有一个 null 关键字,该关键字用于表明一个变量当前的值为空(不是零值,例如整型的零值是 0),也就是不存在值。当你对这些 null 进行操作时,例如调用一个方法,就会直接抛出 null 异常 ,导致程序的崩溃,因此我们在编程时需要格外的小心去处理这些 null 空值。 Tony Hoare, null 的发明者,曾经说过一段非常有名的话: 我称之为我十亿美元的错误。当时,我在使用一个面向对象语言设计第一个综合性的面向引用的类型系统。我的目标是通过编译器的自动检查来保证所有引用的使用都应该是绝对安全的。不过在设计过程中,我未能抵抗住诱惑,引入了空引用的概念,因为它非常容易实现。就是因为这个决策,引发了无数错误、漏洞和系统崩溃,在之后的四十多年中造成了数十亿美元的苦痛和伤害。 尽管如此,空值的表达依然非常有意义,因为空值表示当前时刻变量的值是缺失的。有鉴于此,Rust 吸取了众多教训,决定抛弃 null,而改为使用 Option 枚举变量来表述这种结果。 Option 枚举包含两个成员,一个成员表示含有值:Some(T), 另一个表示没有值:None,定义如下: enum Option { Some(T), None,\n} 其中 T 是泛型参数,Some(T)表示该枚举成员的数据类型是 T,换句话说,Some 可以包含任何类型的数据。 Option 枚举是如此有用以至于它被包含在了 prelude (prelude 属于 Rust 标准库,Rust 会将最常用的类型、函数等提前引入其中,省得我们再手动引入)之中,你不需要将其显式引入作用域。另外,它的成员 Some 和 None 也是如此,无需使用 Option:: 前缀就可直接使用 Some 和 None。总之,不能因为 Some(T) 和 None 中没有 Option:: 的身影,就否认它们是 Option 下的卧龙凤雏。 再来看以下代码: let some_number = Some(5);\nlet some_string = Some(\"a string\"); let absent_number: Option = None; 如果使用 None 而不是 Some,需要告诉 Rust Option 是什么类型的,因为编译器只通过 None 值无法推断出 Some 成员保存的值的类型。 当有一个 Some 值时,我们就知道存在一个值,而这个值保存在 Some 中。当有个 None 值时,在某种意义上,它跟空值具有相同的意义:并没有一个有效的值。那么,Option 为什么就比空值要好呢? 简而言之,因为 Option 和 T(这里 T 可以是任何类型)是不同的类型,例如,这段代码不能编译,因为它尝试将 Option(Option) 与 i8(T) 相加: let x: i8 = 5;\nlet y: Option = Some(5); let sum = x + y; 如果运行这些代码,将得到类似这样的错误信息: error[E0277]: the trait bound `i8: std::ops::Add>` is\nnot satisfied --> |\n5 | let sum = x + y; | ^ no implementation for `i8 + std::option::Option` | 很好!事实上,错误信息意味着 Rust 不知道该如何将 Option 与 i8 相加,因为它们的类型不同。当在 Rust 中拥有一个像 i8 这样类型的值时,编译器确保它总是有一个有效的值,我们可以放心使用而无需做空值检查。只有当使用 Option(或者任何用到的类型)的时候才需要担心可能没有值,而编译器会确保我们在使用值之前处理了为空的情况。 换句话说,在对 Option 进行 T 的运算之前必须将其转换为 T。通常这能帮助我们捕获到空值最常见的问题之一:期望某值不为空但实际上为空的情况。 不再担心会错误的使用一个空值,会让你对代码更加有信心。为了拥有一个可能为空的值,你必须要显式的将其放入对应类型的 Option 中。接着,当使用这个值时,必须明确的处理值为空的情况。只要一个值不是 Option 类型,你就 可以 安全的认定它的值不为空。这是 Rust 的一个经过深思熟虑的设计决策,来限制空值的泛滥以增加 Rust 代码的安全性。 那么当有一个 Option 的值时,如何从 Some 成员中取出 T 的值来使用它呢?Option 枚举拥有大量用于各种情况的方法:你可以查看 它的文档 。熟悉 Option 的方法将对你的 Rust 之旅非常有用。 总的来说,为了使用 Option 值,需要编写处理每个成员的代码。你想要一些代码只当拥有 Some(T) 值时运行,允许这些代码使用其中的 T。也希望一些代码在值为 None 时运行,这些代码并没有一个可用的 T 值。match 表达式就是这么一个处理枚举的控制流结构:它会根据枚举的成员运行不同的代码,这些代码可以使用匹配到的值中的数据。 这里先简单看一下 match 的大致模样,在 模式匹配 中,我们会详细讲解: fn plus_one(x: Option) -> Option { match x { None => None, Some(i) => Some(i + 1), }\n} let five = Some(5);\nlet six = plus_one(five);\nlet none = plus_one(None); plus_one 通过 match 来处理不同 Option 的情况。","breadcrumbs":"Rust 基础入门 » 复合类型 » 枚举 » Option 枚举用于处理空值","id":"129","title":"Option 枚举用于处理空值"},"13":{"body":"连续 6 年最受欢迎的语言当然不是浪得虚名。 无 GC、效率高、工程性强、强安全性以及能同时得到工程派和学院派认可,这些令 Rust 拥有了自己的特色和生存空间。社区的友善,生态的快速发展,大公司的重仓跟进,一切的一切都在说明 Rust 的璀璨未来。 当然,语言毕竟只是工具,我们不能神话它,但是可以给它一个机会,也许,你最终能收获自己的真爱 :) 相信大家听了这么多 Rust 的优点,已经迫不及待想要开始学习旅程,那么容我引用一句 CS(Counter-Strike:反恐精英) 的经典台词:Ok, Let's Rust. 本书是完全开源的,但是并不意味着质量上的妥协,这里的每一个章节都花费了大量的心血和时间才能完成,为此牺牲了陪伴家人、日常娱乐的时间,虽然我们并不后悔,但是如果能得到读者您的鼓励,我们将感激不尽。 既然是开源,那最大的鼓励不是 money,而是 star:) 如果大家觉得这本书作者真的用心了,就帮我们 点一个 🌟 吧,这将是我们继续前行的最大动力","breadcrumbs":"进入 Rust 编程世界 » 总结","id":"13","title":"总结"},"130":{"body":"Rust By Practice ,支持代码在线编辑和运行,并提供详细的 习题解答 。","breadcrumbs":"Rust 基础入门 » 复合类型 » 枚举 » 课后练习","id":"130","title":"课后练习"},"131":{"body":"在日常开发中,使用最广的数据结构之一就是数组,在 Rust 中,最常用的数组有两种,第一种是速度很快但是长度固定的 array,第二种是可动态增长的但是有性能损耗的 Vector,在本书中,我们称 array 为数组,Vector 为动态数组。 不知道你们发现没,这两个数组的关系跟 &str 与 String 的关系很像,前者是长度固定的字符串切片,后者是可动态增长的字符串。其实,在 Rust 中无论是 String 还是 Vector,它们都是 Rust 的高级类型:集合类型,在后面章节会有详细介绍。 对于本章节,我们的重点还是放在数组 array 上。数组的具体定义很简单:将多个类型相同的元素依次组合在一起,就是一个数组。结合上面的内容,可以得出数组的三要素: 长度固定 元素必须有相同的类型 依次线性排列 这里再啰嗦一句, 我们这里说的数组是 Rust 的基本类型,是固定长度的,这点与其他编程语言不同,其它编程语言的数组往往是可变长度的,与 Rust 中的动态数组 Vector 类似 ,希望读者大大牢记此点。","breadcrumbs":"Rust 基础入门 » 复合类型 » 数组 » 数组","id":"131","title":"数组"},"132":{"body":"在 Rust 中,数组是这样定义的: fn main() { let a = [1, 2, 3, 4, 5];\n} 数组语法跟 JavaScript 很像,也跟大多数编程语言很像。由于它的元素类型大小固定,且长度也是固定,因此 数组 array 是存储在栈上 ,性能也会非常优秀。与此对应, 动态数组 Vector 是存储在堆上 ,因此长度可以动态改变。当你不确定是使用数组还是动态数组时,那就应该使用后者,具体见 动态数组 Vector 。 举个例子,在需要知道一年中各个月份名称的程序中,你很可能希望使用的是数组而不是动态数组。因为月份是固定的,它总是只包含 12 个元素: let months = [\"January\", \"February\", \"March\", \"April\", \"May\", \"June\", \"July\", \"August\", \"September\", \"October\", \"November\", \"December\"]; 在一些时候,还需要为 数组声明类型 ,如下所示: let a: [i32; 5] = [1, 2, 3, 4, 5]; 这里,数组类型是通过方括号语法声明,i32 是元素类型,分号后面的数字 5 是数组长度,数组类型也从侧面说明了 数组的元素类型要统一,长度要固定 。 还可以使用下面的语法初始化一个 某个值重复出现 N 次的数组 : let a = [3; 5]; a 数组包含 5 个元素,这些元素的初始化值为 3,聪明的读者已经发现,这种语法跟数组类型的声明语法其实是保持一致的:[3; 5] 和 [类型; 长度]。 在元素重复的场景,这种写法要简单的多,否则你就得疯狂敲击键盘:let a = [3, 3, 3, 3, 3];,不过老板可能很喜欢你的这种疯狂编程的状态。","breadcrumbs":"Rust 基础入门 » 复合类型 » 数组 » 创建数组","id":"132","title":"创建数组"},"133":{"body":"因为数组是连续存放元素的,因此可以通过索引的方式来访问存放其中的元素: fn main() { let a = [9, 8, 7, 6, 5]; let first = a[0]; // 获取a数组第一个元素 let second = a[1]; // 获取第二个元素\n} 与许多语言类似,数组的索引下标是从 0 开始的。此处,first 获取到的值是 9,second 是 8。 越界访问 如果使用超出数组范围的索引访问数组元素,会怎么样?下面是一个接收用户的控制台输入,然后将其作为索引访问数组元素的例子: use std::io; fn main() { let a = [1, 2, 3, 4, 5]; println!(\"Please enter an array index.\"); let mut index = String::new(); // 读取控制台的输出 io::stdin() .read_line(&mut index) .expect(\"Failed to read line\"); let index: usize = index .trim() .parse() .expect(\"Index entered was not a number\"); let element = a[index]; println!( \"The value of the element at index {} is: {}\", index, element );\n} 使用 cargo run 来运行代码,因为数组只有 5 个元素,如果我们试图输入 5 去访问第 6 个元素,则会访问到不存在的数组元素,最终程序会崩溃退出: Please enter an array index.\n5\nthread 'main' panicked at 'index out of bounds: the len is 5 but the index is 5', src/main.rs:19:19\nnote: run with `RUST_BACKTRACE=1` environment variable to display a backtrace 这就是数组访问越界,访问了数组中不存在的元素,导致 Rust 运行时错误。程序因此退出并显示错误消息,未执行最后的 println! 语句。 当你尝试使用索引访问元素时,Rust 将检查你指定的索引是否小于数组长度。如果索引大于或等于数组长度,Rust 会出现 panic 。这种检查只能在运行时进行,比如在上面这种情况下,编译器无法在编译期知道用户运行代码时将输入什么值。 这种就是 Rust 的安全特性之一。在很多系统编程语言中,并不会检查数组越界问题,你会访问到无效的内存地址获取到一个风马牛不相及的值,最终导致在程序逻辑上出现大问题,而且这种问题会非常难以检查。 数组元素为非基础类型 学习了上面的知识,很多朋友肯定觉得已经学会了Rust的数组类型,但现实会给我们一记重锤,实际开发中还会碰到一种情况,就是 数组元素是非基本类型 的,这时候大家一定会这样写。 let array = [String::from(\"rust is good!\"); 8]; println!(\"{:#?}\", array); 然后你会惊喜的得到编译错误。 error[E0277]: the trait bound `String: std::marker::Copy` is not satisfied --> src/main.rs:7:18 |\n7 | let array = [String::from(\"rust is good!\"); 8]; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::marker::Copy` is not implemented for `String` | = note: the `Copy` trait is required because this value will be copied for each element of the array 有些还没有看过特征的小伙伴,有可能不太明白这个报错,不过这个目前可以不提,我们就拿之前所学的 所有权 知识,就可以思考明白,前面几个例子都是Rust的基本类型,而 基本类型在Rust中赋值是以Copy的形式 ,这时候你就懂了吧,let array=[3;5]底层就是不断的Copy出来的,但很可惜复杂类型都没有深拷贝,只能一个个创建。 接着就有小伙伴会这样写。 let array = [String::from(\"rust is good!\"),String::from(\"rust is good!\"),String::from(\"rust is good!\")]; println!(\"{:#?}\", array); 作为一个追求极致完美的Rust开发者,怎么能容忍上面这么难看的代码存在! 正确的写法 ,应该调用std::array::from_fn let array: [String; 8] = std::array::from_fn(|i| String::from(\"rust is good!\")); println!(\"{:#?}\", array);","breadcrumbs":"Rust 基础入门 » 复合类型 » 数组 » 访问数组元素","id":"133","title":"访问数组元素"},"134":{"body":"在之前的 章节 ,我们有讲到 切片 这个概念,它允许你引用集合中的部分连续片段,而不是整个集合,对于数组也是,数组切片允许我们引用数组的一部分: let a: [i32; 5] = [1, 2, 3, 4, 5]; let slice: &[i32] = &a[1..3]; assert_eq!(slice, &[2, 3]); 上面的数组切片 slice 的类型是&[i32],与之对比,数组的类型是[i32;5],简单总结下切片的特点: 切片的长度可以与数组不同,并不是固定的,而是取决于你使用时指定的起始和结束位置 创建切片的代价非常小,因为切片只是针对底层数组的一个引用 切片类型[T]拥有不固定的大小,而切片引用类型&[T]则具有固定的大小,因为 Rust 很多时候都需要固定大小数据类型,因此&[T]更有用,&str字符串切片也同理","breadcrumbs":"Rust 基础入门 » 复合类型 » 数组 » 数组切片","id":"134","title":"数组切片"},"135":{"body":"最后,让我们以一个综合性使用数组的例子,来结束本章节的学习: fn main() { // 编译器自动推导出one的类型 let one = [1, 2, 3]; // 显式类型标注 let two: [u8; 3] = [1, 2, 3]; let blank1 = [0; 3]; let blank2: [u8; 3] = [0; 3]; // arrays是一个二维数组,其中每一个元素都是一个数组,元素类型是[u8; 3] let arrays: [[u8; 3]; 4] = [one, two, blank1, blank2]; // 借用arrays的元素用作循环中 for a in &arrays { print!(\"{:?}: \", a); // 将a变成一个迭代器,用于循环 // 你也可以直接用for n in a {}来进行循环 for n in a.iter() { print!(\"\\t{} + 10 = {}\", n, n+10); } let mut sum = 0; // 0..a.len,是一个 Rust 的语法糖,其实就等于一个数组,元素是从0,1,2一直增加到到a.len-1 for i in 0..a.len() { sum += a[i]; } println!(\"\\t({:?} = {})\", a, sum); }\n} 做个总结,数组虽然很简单,但是其实还是存在几个要注意的点: 数组类型容易跟数组切片混淆 ,[T;n]描述了一个数组的类型,而[T]描述了切片的类型, 因为切片是运行期的数据结构,它的长度无法在编译期得知,因此不能用[T;n]的形式去描述 [u8; 3]和[u8; 4]是不同的类型,数组的长度也是类型的一部分 在实际开发中,使用最多的是数组切片[T] ,我们往往通过引用的方式去使用&[T],因为后者有固定的类型大小 至此,关于数据类型部分,我们已经全部学完了,对于 Rust 学习而言,我们也迈出了坚定的第一步,后面将开始更高级特性的学习。未来如果大家有疑惑需要检索知识,一样可以继续回顾过往的章节,因为本书不仅仅是一门 Rust 的教程,还是一本厚重的 Rust 工具书。","breadcrumbs":"Rust 基础入门 » 复合类型 » 数组 » 总结","id":"135","title":"总结"},"136":{"body":"Rust By Practice ,支持代码在线编辑和运行,并提供详细的 习题解答 。","breadcrumbs":"Rust 基础入门 » 复合类型 » 数组 » 课后练习","id":"136","title":"课后练习"},"137":{"body":"80 后应该都对学校的小混混记忆犹新,在那个时代,小混混们往往都认为自己是地下王者,管控着地下事务的流程,在我看来,他们就像代码中的流程控制一样,无处不在,很显眼,但是又让人懒得重视。 言归正传,Rust 程序是从上而下顺序执行的,在此过程中,我们可以通过循环、分支等流程控制方式,更好的实现相应的功能。","breadcrumbs":"Rust 基础入门 » 流程控制 » 流程控制","id":"137","title":"流程控制"},"138":{"body":"if else 无处不在 -- 鲁迅 但凡你能找到一门编程语言没有 if else,那么一定更要反馈给鲁迅,反正不是我说的:) 总之,只要你拥有其它语言的编程经验,就一定会有以下认知:if else 表达式 根据条件执行不同的代码分支: if condition == true { // A...\n} else { // B...\n} 该代码读作:若 condition 的值为 true,则执行 A 代码,否则执行 B 代码。 先看下面代码: fn main() { let condition = true; let number = if condition { 5 } else { 6 }; println!(\"The value of number is: {}\", number);\n} 以上代码有以下几点要注意: if 语句块是表达式 ,这里我们使用 if 表达式的返回值来给 number 进行赋值:number 的值是 5 用 if 来赋值时,要保证每个分支返回的类型一样(事实上,这种说法不完全准确,见 这里 ),此处返回的 5 和 6 就是同一个类型,如果返回类型不一致就会报错 error[E0308]: if and else have incompatible types --> src/main.rs:4:18 |\n4 | let number = if condition { | __________________^\n5 | | 5\n6 | | } else {\n7 | | \"six\"\n8 | | }; | |_____^ expected integer, found &str // 期望整数类型,但却发现&str字符串切片 | = note: expected type `{integer}` found type `&str`","breadcrumbs":"Rust 基础入门 » 流程控制 » 使用 if 来做分支控制","id":"138","title":"使用 if 来做分支控制"},"139":{"body":"可以将 else if 与 if、else 组合在一起实现更复杂的条件分支判断: fn main() { let n = 6; if n % 4 == 0 { println!(\"number is divisible by 4\"); } else if n % 3 == 0 { println!(\"number is divisible by 3\"); } else if n % 2 == 0 { println!(\"number is divisible by 2\"); } else { println!(\"number is not divisible by 4, 3, or 2\"); }\n} 程序执行时,会按照自上至下的顺序执行每一个分支判断,一旦成功,则跳出 if 语句块,最终本程序会匹配执行 else if n % 3 == 0 的分支,输出 \"number is divisible by 3\"。 有一点要注意,就算有多个分支能匹配,也只有第一个匹配的分支会被执行! 如果代码中有大量的 else if 会让代码变得极其丑陋,不过不用担心,下一章的 match 专门用以解决多分支模式匹配的问题。","breadcrumbs":"Rust 基础入门 » 流程控制 » 使用 else if 来处理多重条件","id":"139","title":"使用 else if 来处理多重条件"},"14":{"body":"很多人都在学 Rust ing,也有很多人在放弃 ing。想要顺利学完 Rust,大家需要谨记本文列出的内容,否则这极有可能是又双叒叕从入门到放弃之旅。 Rust 是一门全新的语言,它会带给你前所未有的体验,提升你的通用编程水平,甚至于赋予你全新的编程思想。在此时此刻,大家可能还半信半疑,但是当学完它再回头看时,你肯定也会认同这些貌似浮夸的赞美。","breadcrumbs":"避免从入门到放弃 » 避免从入门到放弃","id":"14","title":"避免从入门到放弃"},"140":{"body":"循环无处不在,上到数钱,下到数年,你能想象的很多场景都存在循环,因此它也是流程控制中最重要的组成部分之一。 在 Rust 语言中有三种循环方式:for、while 和 loop,其中 for 循环是 Rust 循环王冠上的明珠。","breadcrumbs":"Rust 基础入门 » 流程控制 » 循环控制","id":"140","title":"循环控制"},"141":{"body":"for 循环是 Rust 的大杀器: fn main() { for i in 1..=5 { println!(\"{}\", i); }\n} 以上代码循环输出一个从 1 到 5 的序列,简单粗暴,核心就在于 for 和 in 的联动,语义表达如下: for 元素 in 集合 { // 使用元素干一些你懂我不懂的事情\n} 这个语法跟 JavaScript 还蛮像,应该挺好理解。 注意,使用 for 时我们往往使用集合的引用形式,除非你不想在后面的代码中继续使用该集合(比如我们这里使用了 container 的引用)。如果不使用引用的话,所有权会被转移(move)到 for 语句块中,后面就无法再使用这个集合了): for item in &container { // ...\n} 对于实现了 copy 特征的数组(例如 [i32; 10] )而言, for item in arr 并不会把 arr 的所有权转移,而是直接对其进行了拷贝,因此循环之后仍然可以使用 arr 。 如果想在循环中, 修改该元素 ,可以使用 mut 关键字: for item in &mut collection { // ...\n} 总结如下: 使用方法 等价使用方式 所有权 for item in collection for item in IntoIterator::into_iter(collection) 转移所有权 for item in &collection for item in collection.iter() 不可变借用 for item in &mut collection for item in collection.iter_mut() 可变借用 如果想在循环中 获取元素的索引 : fn main() { let a = [4, 3, 2, 1]; // `.iter()` 方法把 `a` 数组变成一个迭代器 for (i, v) in a.iter().enumerate() { println!(\"第{}个元素是{}\", i + 1, v); }\n} 有同学可能会想到,如果我们想用 for 循环控制某个过程执行 10 次,但是又不想单独声明一个变量来控制这个流程,该怎么写? for _ in 0..10 { // ...\n} 可以用 _ 来替代 i 用于 for 循环中,在 Rust 中 _ 的含义是忽略该值或者类型的意思,如果不使用 _,那么编译器会给你一个 变量未使用的 的警告。 两种循环方式优劣对比 以下代码,使用了两种循环方式: // 第一种\nlet collection = [1, 2, 3, 4, 5];\nfor i in 0..collection.len() { let item = collection[i]; // ...\n} // 第二种\nfor item in collection { } 第一种方式是循环索引,然后通过索引下标去访问集合,第二种方式是直接循环集合中的元素,优劣如下: 性能 :第一种使用方式中 collection[index] 的索引访问,会因为边界检查(Bounds Checking)导致运行时的性能损耗 —— Rust 会检查并确认 index 是否落在集合内,但是第二种直接迭代的方式就不会触发这种检查,因为编译器会在编译时就完成分析并证明这种访问是合法的 安全 :第一种方式里对 collection 的索引访问是非连续的,存在一定可能性在两次访问之间,collection 发生了变化,导致脏数据产生。而第二种直接迭代的方式是连续访问,因此不存在这种风险( 由于所有权限制,在访问过程中,数据并不会发生变化)。 由于 for 循环无需任何条件限制,也不需要通过索引来访问,因此是最安全也是最常用的,通过与下面的 while 的对比,我们能看到为什么 for 会更加安全。","breadcrumbs":"Rust 基础入门 » 流程控制 » for 循环","id":"141","title":"for 循环"},"142":{"body":"使用 continue 可以跳过当前当次的循环,开始下次的循环: for i in 1..4 { if i == 2 { continue; } println!(\"{}\", i); } 上面代码对 1 到 3 的序列进行迭代,且跳过值为 2 时的循环,输出如下: 1\n3","breadcrumbs":"Rust 基础入门 » 流程控制 » continue","id":"142","title":"continue"},"143":{"body":"使用 break 可以直接跳出当前整个循环: for i in 1..4 { if i == 2 { break; } println!(\"{}\", i); } 上面代码对 1 到 3 的序列进行迭代,在遇到值为 2 时的跳出整个循环,后面的循环不再执行,输出如下: 1","breadcrumbs":"Rust 基础入门 » 流程控制 » break","id":"143","title":"break"},"144":{"body":"如果你需要一个条件来循环,当该条件为 true 时,继续循环,条件为 false,跳出循环,那么 while 就非常适用: fn main() { let mut n = 0; while n <= 5 { println!(\"{}!\", n); n = n + 1; } println!(\"我出来了!\");\n} 该 while 循环,只有当 n 小于等于 5 时,才执行,否则就立刻跳出循环,因此在上述代码中,它会先从 0 开始,满足条件,进行循环,然后是 1,满足条件,进行循环,最终到 6 的时候,大于 5,不满足条件,跳出 while 循环,执行 我出来了 的打印,然后程序结束: 0!\n1!\n2!\n3!\n4!\n5!\n我出来了! 当然,你也可以用其它方式组合实现,例如 loop(无条件循环,将在下面介绍) + if + break: fn main() { let mut n = 0; loop { if n > 5 { break } println!(\"{}\", n); n+=1; } println!(\"我出来了!\");\n} 可以看出,在这种循环场景下,while 要简洁的多。 while vs for 我们也能用 while 来实现 for 的功能: fn main() { let a = [10, 20, 30, 40, 50]; let mut index = 0; while index < 5 { println!(\"the value is: {}\", a[index]); index = index + 1; }\n} 这里,代码对数组中的元素进行计数。它从索引 0 开始,并接着循环直到遇到数组的最后一个索引(这时,index < 5 不再为真)。运行这段代码会打印出数组中的每一个元素: the value is: 10\nthe value is: 20\nthe value is: 30\nthe value is: 40\nthe value is: 50 数组中的所有五个元素都如期被打印出来。尽管 index 在某一时刻会到达值 5,不过循环在其尝试从数组获取第六个值(会越界)之前就停止了。 但这个过程很容易出错;如果索引长度不正确会导致程序 panic 。这也使程序更慢,因为编译器增加了运行时代码来对每次循环的每个元素进行条件检查。 for循环代码如下: fn main() { let a = [10, 20, 30, 40, 50]; for element in a.iter() { println!(\"the value is: {}\", element); }\n} 可以看出,for 并不会使用索引去访问数组,因此更安全也更简洁,同时避免 运行时的边界检查,性能更高。","breadcrumbs":"Rust 基础入门 » 流程控制 » while 循环","id":"144","title":"while 循环"},"145":{"body":"对于循环而言,loop 循环毋庸置疑,是适用面最高的,它可以适用于所有循环场景(虽然能用,但是在很多场景下, for 和 while 才是最优选择),因为 loop 就是一个简单的无限循环,你可以在内部实现逻辑通过 break 关键字来控制循环何时结束。 使用 loop 循环一定要打起精神,否则你会写出下面的跑满你一个 CPU 核心的疯子代码: fn main() { loop { println!(\"again!\"); }\n} 该循环会不停的在终端打印输出,直到你使用 Ctrl-C 结束程序: again!\nagain!\nagain!\nagain!\n^Cagain! 注意 ,不要轻易尝试上述代码,如果你电脑配置不行,可能会死机!!! 因此,当使用 loop 时,必不可少的伙伴是 break 关键字,它能让循环在满足某个条件时跳出: fn main() { let mut counter = 0; let result = loop { counter += 1; if counter == 10 { break counter * 2; } }; println!(\"The result is {}\", result);\n} 以上代码当 counter 递增到 10 时,就会通过 break 返回一个 counter * 2 的值,最后赋给 result 并打印出来。 这里有几点值得注意: break 可以单独使用,也可以带一个返回值 ,有些类似 return loop 是一个表达式 ,因此可以返回一个值","breadcrumbs":"Rust 基础入门 » 流程控制 » loop 循环","id":"145","title":"loop 循环"},"146":{"body":"Rust By Practice ,支持代码在线编辑和运行,并提供详细的 习题解答 。","breadcrumbs":"Rust 基础入门 » 流程控制 » 课后练习","id":"146","title":"课后练习"},"147":{"body":"模式匹配,这个词,对于非函数语言编程来说,真的还蛮少听到,因为它经常出现在函数式编程里,用于为复杂的类型系统提供一个轻松的解构能力。 曾记否?在枚举和流程控制那章,我们遗留了两个问题,都是关于 match 的,第一个是如何对 Option 枚举进行进一步处理,另外一个是如何用 match 来替代 else if 这种丑陋的多重分支使用方式。那么让我们先一起来揭开 match 的神秘面纱。","breadcrumbs":"Rust 基础入门 » 模式匹配 » 模式匹配","id":"147","title":"模式匹配"},"148":{"body":"在 Rust 中,模式匹配最常用的就是 match 和 if let,本章节将对两者及相关的概念进行详尽介绍。 先来看一个关于 match 的简单例子: enum Direction { East, West, North, South,\n} fn main() { let dire = Direction::South; match dire { Direction::East => println!(\"East\"), Direction::North | Direction::South => { println!(\"South or North\"); }, _ => println!(\"West\"), };\n} 这里我们想去匹配 dire 对应的枚举类型,因此在 match 中用三个匹配分支来完全覆盖枚举变量 Direction 的所有成员类型,有以下几点值得注意: match 的匹配必须要穷举出所有可能,因此这里用 _ 来代表未列出的所有可能性 match 的每一个分支都必须是一个表达式,且所有分支的表达式最终返回值的类型必须相同 X | Y ,类似逻辑运算符 或,代表该分支可以匹配 X 也可以匹配 Y,只要满足一个即可 其实 match 跟其他语言中的 switch 非常像,_ 类似于 switch 中的 default。","breadcrumbs":"Rust 基础入门 » 模式匹配 » match 和 if let » match 和 if let","id":"148","title":"match 和 if let"},"149":{"body":"首先来看看 match 的通用形式: match target { 模式1 => 表达式1, 模式2 => { 语句1; 语句2; 表达式2 }, _ => 表达式3\n} 该形式清晰的说明了何为模式,何为模式匹配:将模式与 target 进行匹配,即为模式匹配,而模式匹配不仅仅局限于 match,后面我们会详细阐述。 match 允许我们将一个值与一系列的模式相比较,并根据相匹配的模式执行对应的代码,下面让我们来一一详解,先看一个例子: enum Coin { Penny, Nickel, Dime, Quarter,\n} fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => { println!(\"Lucky penny!\"); 1 }, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, }\n} value_in_cents 函数根据匹配到的硬币,返回对应的美分数值。match 后紧跟着的是一个表达式,跟 if 很像,但是 if 后的表达式必须是一个布尔值,而 match 后的表达式返回值可以是任意类型,只要能跟后面的分支中的模式匹配起来即可,这里的 coin 是枚举 Coin 类型。 接下来是 match 的分支。一个分支有两个部分: 一个模式和针对该模式的处理代码 。第一个分支的模式是 Coin::Penny,其后的 => 运算符将模式和将要运行的代码分开。这里的代码就仅仅是表达式 1,不同分支之间使用逗号分隔。 当 match 表达式执行时,它将目标值 coin 按顺序依次与每一个分支的模式相比较,如果模式匹配了这个值,那么模式之后的代码将被执行。如果模式并不匹配这个值,将继续执行下一个分支。 每个分支相关联的代码是一个表达式,而表达式的结果值将作为整个 match 表达式的返回值。如果分支有多行代码,那么需要用 {} 包裹,同时最后一行代码需要是一个表达式。 使用 match 表达式赋值 还有一点很重要,match 本身也是一个表达式,因此可以用它来赋值: enum IpAddr { Ipv4, Ipv6\n} fn main() { let ip1 = IpAddr::Ipv6; let ip_str = match ip1 { IpAddr::Ipv4 => \"127.0.0.1\", _ => \"::1\", }; println!(\"{}\", ip_str);\n} 因为这里匹配到 _ 分支,所以将 \"::1\" 赋值给了 ip_str。 模式绑定 模式匹配的另外一个重要功能是从模式中取出绑定的值,例如: #[derive(Debug)]\nenum UsState { Alabama, Alaska, // --snip--\n} enum Coin { Penny, Nickel, Dime, Quarter(UsState), // 25美分硬币\n} 其中 Coin::Quarter 成员还存放了一个值:美国的某个州(因为在 1999 年到 2008 年间,美国在 25 美分(Quarter)硬币的背后为 50 个州印刷了不同的标记,其它硬币都没有这样的设计)。 接下来,我们希望在模式匹配中,获取到 25 美分硬币上刻印的州的名称: fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter(state) => { println!(\"State quarter from {:?}!\", state); 25 }, }\n} 上面代码中,在匹配 Coin::Quarter(state) 模式时,我们把它内部存储的值绑定到了 state 变量上,因此 state 变量就是对应的 UsState 枚举类型。 例如有一个印了阿拉斯加州标记的 25 分硬币:Coin::Quarter(UsState::Alaska), 它在匹配时,state 变量将被绑定 UsState::Alaska 的枚举值。 再来看一个更复杂的例子: enum Action { Say(String), MoveTo(i32, i32), ChangeColorRGB(u16, u16, u16),\n} fn main() { let actions = [ Action::Say(\"Hello Rust\".to_string()), Action::MoveTo(1,2), Action::ChangeColorRGB(255,255,0), ]; for action in actions { match action { Action::Say(s) => { println!(\"{}\", s); }, Action::MoveTo(x, y) => { println!(\"point from (0, 0) move to ({}, {})\", x, y); }, Action::ChangeColorRGB(r, g, _) => { println!(\"change color into '(r:{}, g:{}, b:0)', 'b' has been ignored\", r, g, ); } } }\n} 运行后输出: $ cargo run Compiling world_hello v0.1.0 (/Users/sunfei/development/rust/world_hello) Finished dev [unoptimized + debuginfo] target(s) in 0.16s Running `target/debug/world_hello`\nHello Rust\npoint from (0, 0) move to (1, 2)\nchange color into '(r:255, g:255, b:0)', 'b' has been ignored 穷尽匹配 在文章的开头,我们简单总结过 match 的匹配必须穷尽所有情况,下面来举例说明,例如: enum Direction { East, West, North, South,\n} fn main() { let dire = Direction::South; match dire { Direction::East => println!(\"East\"), Direction::North | Direction::South => { println!(\"South or North\"); }, };\n} 我们没有处理 Direction::West 的情况,因此会报错: error[E0004]: non-exhaustive patterns: `West` not covered // 非穷尽匹配,`West` 没有被覆盖 --> src/main.rs:10:11 |\n1 | / enum Direction {\n2 | | East,\n3 | | West, | | ---- not covered\n4 | | North,\n5 | | South,\n6 | | } | |_- `Direction` defined here\n...\n10 | match dire { | ^^^^ pattern `West` not covered // 模式 `West` 没有被覆盖 | = help: ensure that all possible cases are being handled, possibly by adding wildcards or more match arms = note: the matched value is of type `Direction` 不禁想感叹,Rust 的编译器 真强大 ,忍不住想爆粗口了,sorry,如果你以后进一步深入使用 Rust 也会像我这样感叹的。Rust 编译器清晰地知道 match 中有哪些分支没有被覆盖, 这种行为能强制我们处理所有的可能性,有效避免传说中价值 十亿美金 的 null 陷阱。 _ 通配符 当我们不想在匹配时列出所有值的时候,可以使用 Rust 提供的一个特殊 模式 ,例如,u8 可以拥有 0 到 255 的有效的值,但是我们只关心 1、3、5 和 7 这几个值,不想列出其它的 0、2、4、6、8、9 一直到 255 的值。那么, 我们不必一个一个列出所有值, 因为可以使用特殊的模式 _ 替代: let some_u8_value = 0u8;\nmatch some_u8_value { 1 => println!(\"one\"), 3 => println!(\"three\"), 5 => println!(\"five\"), 7 => println!(\"seven\"), _ => (),\n} 通过将 _ 其放置于其他分支后,_ 将会匹配所有遗漏的值。() 表示返回 单元类型 与所有分支返回值的类型相同,所以当匹配到 _ 后,什么也不会发生。 除了_通配符,用一个变量来承载其他情况也是可以的。 #[derive(Debug)]\nenum Direction { East, West, North, South,\n} fn main() { let dire = Direction::South; match dire { Direction::East => println!(\"East\"), other => println!(\"other direction: {:?}\", other), };\n} 然而,在某些场景下,我们其实只关心 某一个值是否存在 ,此时 match 就显得过于啰嗦。","breadcrumbs":"Rust 基础入门 » 模式匹配 » match 和 if let » match 匹配","id":"149","title":"match 匹配"},"15":{"body":"在学习 Go、Python 等编程语言时,你可能会一边工作、一边轻松愉快的学习它们,但是 Rust 不行。原因如文章开头所说,在学习 Rust 的同时你会收获很多语言之外的知识,因此 Rust 在入门阶段比很多编程语言要更难,但是一旦入门,你将收获一个全新的自己,成为一个更加优秀的程序员。 在学习过程中,一开始可能会轻松愉快,但是在开始接触 Rust 核心概念时(所有权、借用、生命周期、智能指针等),难度会陡然提升,此时就需要认真对待起来,否则会为后面埋下难以填补的坑: 结果最后你可能只有两个选择 - 重新学 or 放弃。 因此,在学习过程中,给大家三点建议: 要提前做好会遇到困难的准备,因为如上所说,学习 Rust 不仅仅是在学习一门编程语言 不要抱着试一试的心态去试一试,否则是浪费时间和消耗学习激情,作为连续七年荣获全世界最受喜欢桂冠的语言,Rust 不仅仅是值得试一试 :) 深入学习一本好书或教程 总之,Rust 入门难,但是在你一次次克服艰难险阻的同时,也一次次收获了与众不同的编程经验,最后历经九九八十一难,立地成大佬。 给自己一个机会,也给 Rust 一个机会 :)","breadcrumbs":"避免从入门到放弃 » 避免试一试的心态","id":"15","title":"避免试一试的心态"},"150":{"body":"有时会遇到只有一个模式的值需要被处理,其它值直接忽略的场景,如果用 match 来处理就要写成下面这样: let v = Some(3u8); match v { Some(3) => println!(\"three\"), _ => (), } 我们只想要对 Some(3) 模式进行匹配, 不想处理任何其他 Some 值或 None 值。但是为了满足 match 表达式(穷尽性)的要求,写代码时必须在处理完这唯一的成员后加上 _ => (),这样会增加不少无用的代码。 俗话说“杀鸡焉用牛刀”,我们完全可以用 if let 的方式来实现: if let Some(3) = v { println!(\"three\");\n} 这两种匹配对于新手来说,可能有些难以抉择,但是只要记住一点就好: 当你只要匹配一个条件,且忽略其他条件时就用 if let ,否则都用 match 。","breadcrumbs":"Rust 基础入门 » 模式匹配 » match 和 if let » if let 匹配","id":"150","title":"if let 匹配"},"151":{"body":"Rust 标准库中提供了一个非常实用的宏:matches!,它可以将一个表达式跟模式进行匹配,然后返回匹配的结果 true or false。 例如,有一个动态数组,里面存有以下枚举: enum MyEnum { Foo, Bar\n} fn main() { let v = vec![MyEnum::Foo,MyEnum::Bar,MyEnum::Foo];\n} 现在如果想对 v 进行过滤,只保留类型是 MyEnum::Foo 的元素,你可能想这么写: v.iter().filter(|x| x == MyEnum::Foo); 但是,实际上这行代码会报错,因为你无法将 x 直接跟一个枚举成员进行比较。好在,你可以使用 match 来完成,但是会导致代码更为啰嗦,是否有更简洁的方式?答案是使用 matches!: v.iter().filter(|x| matches!(x, MyEnum::Foo)); 很简单也很简洁,再来看看更多的例子: let foo = 'f';\nassert!(matches!(foo, 'A'..='Z' | 'a'..='z')); let bar = Some(4);\nassert!(matches!(bar, Some(x) if x > 2));","breadcrumbs":"Rust 基础入门 » 模式匹配 » match 和 if let » matches!宏","id":"151","title":"matches!宏"},"152":{"body":"无论是 match 还是 if let,这里都是一个新的代码块,而且这里的绑定相当于新变量,如果你使用同名变量,会发生变量遮蔽: fn main() { let age = Some(30); println!(\"在匹配前,age是{:?}\",age); if let Some(age) = age { println!(\"匹配出来的age是{}\",age); } println!(\"在匹配后,age是{:?}\",age);\n} cargo run 运行后输出如下: 在匹配前,age是Some(30)\n匹配出来的age是30\n在匹配后,age是Some(30) 可以看出在 if let 中,= 右边 Some(i32) 类型的 age 被左边 i32 类型的新 age 遮蔽了,该遮蔽一直持续到 if let 语句块的结束。因此第三个 println! 输出的 age 依然是 Some(i32) 类型。 对于 match 类型也是如此: fn main() { let age = Some(30); println!(\"在匹配前,age是{:?}\",age); match age { Some(age) => println!(\"匹配出来的age是{}\",age), _ => () } println!(\"在匹配后,age是{:?}\",age);\n} 需要注意的是, match 中的变量遮蔽其实不是那么的容易看出 ,因此要小心!其实这里最好不要使用同名,避免难以理解,如下。 fn main() { let age = Some(30); println!(\"在匹配前,age是{:?}\", age); match age { Some(x) => println!(\"匹配出来的age是{}\", x), _ => () } println!(\"在匹配后,age是{:?}\", age);\n}","breadcrumbs":"Rust 基础入门 » 模式匹配 » match 和 if let » 变量遮蔽","id":"152","title":"变量遮蔽"},"153":{"body":"Rust By Practice ,支持代码在线编辑和运行,并提供详细的 习题解答 。","breadcrumbs":"Rust 基础入门 » 模式匹配 » match 和 if let » 课后练习","id":"153","title":"课后练习"},"154":{"body":"在枚举那章,提到过 Option 枚举,它用来解决 Rust 中变量是否有值的问题,定义如下: enum Option { Some(T), None,\n} 简单解释就是: 一个变量要么有值:Some(T), 要么为空:None 。 那么现在的问题就是该如何去使用这个 Option 枚举类型,根据我们上一节的经验,可以通过 match 来实现。 因为 Option,Some,None 都包含在 prelude 中,因此你可以直接通过名称来使用它们,而无需以 Option::Some 这种形式去使用,总之,千万不要因为调用路径变短了,就忘记 Some 和 None 也是 Option 底下的枚举成员!","breadcrumbs":"Rust 基础入门 » 模式匹配 » 解构 Option » 解构 Option","id":"154","title":"解构 Option"},"155":{"body":"使用 Option,是为了从 Some 中取出其内部的 T 值以及处理没有值的情况,为了演示这一点,下面一起来编写一个函数,它获取一个 Option,如果其中含有一个值,将其加一;如果其中没有值,则函数返回 None 值: fn plus_one(x: Option) -> Option { match x { None => None, Some(i) => Some(i + 1), }\n} let five = Some(5);\nlet six = plus_one(five);\nlet none = plus_one(None); plus_one 接受一个 Option 类型的参数,同时返回一个 Option 类型的值(这种形式的函数在标准库内随处所见),在该函数的内部处理中,如果传入的是一个 None ,则返回一个 None 且不做任何处理;如果传入的是一个 Some(i32),则通过模式绑定,把其中的值绑定到变量 i 上,然后返回 i+1 的值,同时用 Some 进行包裹。 为了进一步说明,假设 plus_one 函数接受的参数值 x 是 Some(5),来看看具体的分支匹配情况: 传入参数 Some(5) None => None, 首先是匹配 None 分支,因为值 Some(5) 并不匹配模式 None,所以继续匹配下一个分支。 Some(i) => Some(i + 1), Some(5) 与 Some(i) 匹配吗?当然匹配!它们是相同的成员。i 绑定了 Some 中包含的值,因此 i 的值是 5。接着匹配分支的代码被执行,最后将 i 的值加一并返回一个含有值 6 的新 Some。 传入参数 None 接着考虑下 plus_one 的第二个调用,这次传入的 x 是 None, 我们进入 match 并与第一个分支相比较。 None => None, 匹配上了!接着程序继续执行该分支后的代码:返回表达式 None 的值,也就是返回一个 None,因为第一个分支就匹配到了,其他的分支将不再比较。","breadcrumbs":"Rust 基础入门 » 模式匹配 » 解构 Option » 匹配 Option","id":"155","title":"匹配 Option"},"156":{"body":"","breadcrumbs":"Rust 基础入门 » 模式匹配 » 模式适用场景 » 模式适用场景","id":"156","title":"模式适用场景"},"157":{"body":"模式是 Rust 中的特殊语法,它用来匹配类型中的结构和数据,它往往和 match 表达式联用,以实现强大的模式匹配能力。模式一般由以下内容组合而成: 字面值 解构的数组、枚举、结构体或者元组 变量 通配符 占位符","breadcrumbs":"Rust 基础入门 » 模式匹配 » 模式适用场景 » 模式","id":"157","title":"模式"},"158":{"body":"match 分支 match VALUE { PATTERN => EXPRESSION, PATTERN => EXPRESSION, PATTERN => EXPRESSION,\n} 如上所示,match 的每个分支就是一个 模式 ,因为 match 匹配是穷尽式的,因此我们往往需要一个特殊的模式 _,来匹配剩余的所有情况: match VALUE { PATTERN => EXPRESSION, PATTERN => EXPRESSION, _ => EXPRESSION,\n} if let 分支 if let 往往用于匹配一个模式,而忽略剩下的所有模式的场景: if let PATTERN = SOME_VALUE { } while let 条件循环 一个与 if let 类似的结构是 while let 条件循环,它允许只要模式匹配就一直进行 while 循环。下面展示了一个使用 while let 的例子: // Vec是动态数组\nlet mut stack = Vec::new(); // 向数组尾部插入元素\nstack.push(1);\nstack.push(2);\nstack.push(3); // stack.pop从数组尾部弹出元素\nwhile let Some(top) = stack.pop() { println!(\"{}\", top);\n} 这个例子会打印出 3、2 接着是 1。pop 方法取出动态数组的最后一个元素并返回 Some(value),如果动态数组是空的,将返回 None,对于 while 来说,只要 pop 返回 Some 就会一直不停的循环。一旦其返回 None,while 循环停止。我们可以使用 while let 来弹出栈中的每一个元素。 你也可以用 loop + if let 或者 match 来实现这个功能,但是会更加啰嗦。 for 循环 let v = vec!['a', 'b', 'c']; for (index, value) in v.iter().enumerate() { println!(\"{} is at index {}\", value, index);\n} 这里使用 enumerate 方法产生一个迭代器,该迭代器每次迭代会返回一个 (索引,值) 形式的元组,然后用 (index,value) 来匹配。 let 语句 let PATTERN = EXPRESSION; 是的, 该语句我们已经用了无数次了,它也是一种模式匹配: let x = 5; 这其中,x 也是一种模式绑定,代表将 匹配的值绑定到变量 x 上 。因此,在 Rust 中, 变量名也是一种模式 ,只不过它比较朴素很不起眼罢了。 let (x, y, z) = (1, 2, 3); 上面将一个元组与模式进行匹配( 模式和值的类型必需相同! ),然后把 1, 2, 3 分别绑定到 x, y, z 上。 模式匹配要求两边的类型必须相同,否则就会导致下面的报错: let (x, y) = (1, 2, 3); error[E0308]: mismatched types --> src/main.rs:4:5 |\n4 | let (x, y) = (1, 2, 3); | ^^^^^^ --------- this expression has type `({integer}, {integer}, {integer})` | | | expected a tuple with 3 elements, found one with 2 elements | = note: expected tuple `({integer}, {integer}, {integer})` found tuple `(_, _)`\nFor more information about this error, try `rustc --explain E0308`.\nerror: could not compile `playground` due to previous error 对于元组来说,元素个数也是类型的一部分! 函数参数 函数参数也是模式: fn foo(x: i32) { // 代码\n} 其中 x 就是一个模式,你还可以在参数中匹配元组: fn print_coordinates(&(x, y): &(i32, i32)) { println!(\"Current location: ({}, {})\", x, y);\n} fn main() { let point = (3, 5); print_coordinates(&point);\n} &(3, 5) 会匹配模式 &(x, y),因此 x 得到了 3,y 得到了 5。 let 和 if let 对于以下代码,编译器会报错: let Some(x) = some_option_value; 因为右边的值可能不为 Some,而是 None,这种时候就不能进行匹配,也就是上面的代码遗漏了 None 的匹配。 类似 let , for和match 都必须要求完全覆盖匹配,才能通过编译( 不可驳模式匹配 )。 但是对于 if let,就可以这样使用: if let Some(x) = some_option_value { println!(\"{}\", x);\n} 因为 if let 允许匹配一种模式,而忽略其余的模式( 可驳模式匹配 )。","breadcrumbs":"Rust 基础入门 » 模式匹配 » 模式适用场景 » 所有可能用到模式的地方","id":"158","title":"所有可能用到模式的地方"},"159":{"body":"在本书中我们已领略过许多不同类型模式的例子,本节的目标就是把这些模式语法都罗列出来,方便大家检索查阅(模式匹配在我们的开发中会经常用到)。","breadcrumbs":"Rust 基础入门 » 模式匹配 » 全模式列表 » 全模式列表","id":"159","title":"全模式列表"},"16":{"body":"Rust 跟其它语言不同,你无法看了一遍语法,然后就能上手写代码,对,我说的就是对比 Go 语言,后者的简单易用是有目共睹的。 这些年,我遇到过太多在网上看了一遍菜鸟教程(或其它简易教程)就上手写 demo 甚至项目的同学,无一例外,都各种碰壁、趟坑,最后要么放弃,要么回炉重造,之前的时间和精力基本等同浪费。 因此,大家一定要舍得投入时间,沉下心去读一本好书,这本书会带你深入浅出地学习使用 Rust 所需的各种知识,还会带你提前趟坑,这些坑往往是需要大量的时间才能领悟的。 在以前我可能会推荐看 Rust Book + async book + nomicon 这几本英文书的组合,但是现在有了一本更适合中国用户的书籍,那就是...你们猜,内容好坏大家一读即知,光就文字而言,那绝对是行云流水般的阅读体验,可以极大提升学习效率,也不再因为反复读也读不懂一句话而烦闷不堪。","breadcrumbs":"避免从入门到放弃 » 深入学习一本好书","id":"16","title":"深入学习一本好书"},"160":{"body":"let x = 1; match x { 1 => println!(\"one\"), 2 => println!(\"two\"), 3 => println!(\"three\"), _ => println!(\"anything\"),\n} 这段代码会打印 one 因为 x 的值是 1,如果希望代码获得特定的具体值,那么这种语法很有用。","breadcrumbs":"Rust 基础入门 » 模式匹配 » 全模式列表 » 匹配字面值","id":"160","title":"匹配字面值"},"161":{"body":"在 match 中,我们有讲过变量遮蔽的问题,这个在 匹配命名变量 时会遇到: fn main() { let x = Some(5); let y = 10; match x { Some(50) => println!(\"Got 50\"), Some(y) => println!(\"Matched, y = {:?}\", y), _ => println!(\"Default case, x = {:?}\", x), } println!(\"at the end: x = {:?}, y = {:?}\", x, y);\n} 让我们看看当 match 语句运行的时候发生了什么。第一个匹配分支的模式并不匹配 x 中定义的值,所以代码继续执行。 第二个匹配分支中的模式引入了一个新变量 y,它会匹配任何 Some 中的值。因为这里的 y 在 match 表达式的作用域中,而不是之前 main 作用域中,所以这是一个新变量,不是开头声明为值 10 的那个 y。这个新的 y 绑定会匹配任何 Some 中的值,在这里是 x 中的值。因此这个 y 绑定了 x 中 Some 内部的值。这个值是 5,所以这个分支的表达式将会执行并打印出 Matched,y = 5。 如果 x 的值是 None 而不是 Some(5),头两个分支的模式不会匹配,所以会匹配模式 _。这个分支的模式中没有引入变量 x,所以此时表达式中的 x 会是外部没有被遮蔽的 x,也就是 None。 一旦 match 表达式执行完毕,其作用域也就结束了,同理内部 y 的作用域也结束了。最后的 println! 会打印 at the end: x = Some(5), y = 10。 如果你不想引入变量遮蔽,可以使用另一个变量名而非 y,或者使用匹配守卫(match guard)的方式,稍后在 匹配守卫提供的额外条件 中会讲解。","breadcrumbs":"Rust 基础入门 » 模式匹配 » 全模式列表 » 匹配命名变量","id":"161","title":"匹配命名变量"},"162":{"body":"在 match 表达式中,可以使用 | 语法匹配多个模式,它代表 或 的意思。例如,如下代码将 x 的值与匹配分支相比较,第一个分支有 或 选项,意味着如果 x 的值匹配此分支的任何一个模式,它就会运行: let x = 1; match x { 1 | 2 => println!(\"one or two\"), 3 => println!(\"three\"), _ => println!(\"anything\"),\n} 上面的代码会打印 one or two。","breadcrumbs":"Rust 基础入门 » 模式匹配 » 全模式列表 » 单分支多模式","id":"162","title":"单分支多模式"},"163":{"body":"在 数值类型 中我们有讲到一个序列语法,该语法不仅可以用于循环中,还能用于匹配模式。 ..= 语法允许你匹配一个闭区间序列内的值。在如下代码中,当模式匹配任何在此序列内的值时,该分支会执行: let x = 5; match x { 1..=5 => println!(\"one through five\"), _ => println!(\"something else\"),\n} 如果 x 是 1、2、3、4 或 5,第一个分支就会匹配。这相比使用 | 运算符表达相同的意思更为方便;相比 1..=5,使用 | 则不得不指定 1 | 2 | 3 | 4 | 5 这五个值,而使用 ..= 指定序列就简短的多,比如希望匹配比如从 1 到 1000 的数字的时候! 序列只允许用于数字或字符类型,原因是:它们可以连续,同时编译器在编译期可以检查该序列是否为空,字符和数字值是 Rust 中仅有的可以用于判断是否为空的类型。 如下是一个使用字符类型序列的例子: let x = 'c'; match x { 'a'..='j' => println!(\"early ASCII letter\"), 'k'..='z' => println!(\"late ASCII letter\"), _ => println!(\"something else\"),\n} Rust 知道 'c' 位于第一个模式的序列内,所以会打印出 early ASCII letter。","breadcrumbs":"Rust 基础入门 » 模式匹配 » 全模式列表 » 通过序列 ..= 匹配值的范围","id":"163","title":"通过序列 ..= 匹配值的范围"},"164":{"body":"也可以使用模式来解构结构体、枚举、元组、数组和引用。 解构结构体 下面代码展示了如何用 let 解构一个带有两个字段 x 和 y 的结构体 Point: struct Point { x: i32, y: i32,\n} fn main() { let p = Point { x: 0, y: 7 }; let Point { x: a, y: b } = p; assert_eq!(0, a); assert_eq!(7, b);\n} 这段代码创建了变量 a 和 b 来匹配结构体 p 中的 x 和 y 字段,这个例子展示了 模式中的变量名不必与结构体中的字段名一致 。不过通常希望变量名与字段名一致以便于理解变量来自于哪些字段。 因为变量名匹配字段名是常见的,同时因为 let Point { x: x, y: y } = p; 中 x 和 y 重复了,所以对于匹配结构体字段的模式存在简写:只需列出结构体字段的名称,则模式创建的变量会有相同的名称。下例与上例有着相同行为的代码,不过 let 模式创建的变量为 x 和 y 而不是 a 和 b: struct Point { x: i32, y: i32,\n} fn main() { let p = Point { x: 0, y: 7 }; let Point { x, y } = p; assert_eq!(0, x); assert_eq!(7, y);\n} 这段代码创建了变量 x 和 y,与结构体 p 中的 x 和 y 字段相匹配。其结果是变量 x 和 y 包含结构体 p 中的值。 也可以使用字面值作为结构体模式的一部分进行解构,而不是为所有的字段创建变量。这允许我们测试一些字段为特定值的同时创建其他字段的变量。 下文展示了固定某个字段的匹配方式: # struct Point {\n# x: i32,\n# y: i32,\n# }\n#\nfn main() { let p = Point { x: 0, y: 7 }; match p { Point { x, y: 0 } => println!(\"On the x axis at {}\", x), Point { x: 0, y } => println!(\"On the y axis at {}\", y), Point { x, y } => println!(\"On neither axis: ({}, {})\", x, y), }\n} 首先是 match 第一个分支,指定匹配 y 为 0 的 Point; 然后第二个分支在第一个分支之后,匹配 y 不为 0,x 为 0 的 Point; 最后一个分支匹配 x 不为 0,y 也不为 0 的 Point。 在这个例子中,值 p 因为其 x 包含 0 而匹配第二个分支,因此会打印出 On the y axis at 7。 解构枚举 下面代码以 Message 枚举为例,编写一个 match 使用模式解构每一个内部值: enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32),\n} fn main() { let msg = Message::ChangeColor(0, 160, 255); match msg { Message::Quit => { println!(\"The Quit variant has no data to destructure.\") } Message::Move { x, y } => { println!( \"Move in the x direction {} and in the y direction {}\", x, y ); } Message::Write(text) => println!(\"Text message: {}\", text), Message::ChangeColor(r, g, b) => { println!( \"Change the color to red {}, green {}, and blue {}\", r, g, b ) } }\n} 这里老生常谈一句话,模式匹配一样要类型相同,因此匹配 Message::Move{1,2} 这样的枚举值,就必须要用 Message::Move{x,y} 这样的同类型模式才行。 这段代码会打印出 Change the color to red 0, green 160, and blue 255。尝试改变 msg 的值来观察其他分支代码的运行。 对于像 Message::Quit 这样没有任何数据的枚举成员,不能进一步解构其值。只能匹配其字面值 Message::Quit,因此模式中没有任何变量。 对于另外两个枚举成员,就用相同类型的模式去匹配出对应的值即可。 解构嵌套的结构体和枚举 目前为止,所有的例子都只匹配了深度为一级的结构体或枚举。 match 也可以匹配嵌套的项! 例如使用下面的代码来同时支持 RGB 和 HSV 色彩模式: enum Color { Rgb(i32, i32, i32), Hsv(i32, i32, i32),\n} enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(Color),\n} fn main() { let msg = Message::ChangeColor(Color::Hsv(0, 160, 255)); match msg { Message::ChangeColor(Color::Rgb(r, g, b)) => { println!( \"Change the color to red {}, green {}, and blue {}\", r, g, b ) } Message::ChangeColor(Color::Hsv(h, s, v)) => { println!( \"Change the color to hue {}, saturation {}, and value {}\", h, s, v ) } _ => () }\n} match 第一个分支的模式匹配一个 Message::ChangeColor 枚举成员,该枚举成员又包含了一个 Color::Rgb 的枚举成员,最终绑定了 3 个内部的 i32 值。第二个,就交给亲爱的读者来思考完成。 解构结构体和元组 我们甚至可以用复杂的方式来混合、匹配和嵌套解构模式。如下是一个复杂结构体的例子,其中结构体和元组嵌套在元组中,并将所有的原始类型解构出来: struct Point { x: i32, y: i32, } let ((feet, inches), Point {x, y}) = ((3, 10), Point { x: 3, y: -10 }); 这种将复杂类型分解匹配的方式,可以让我们单独得到感兴趣的某个值。 解构数组 对于数组,我们可以用类似元组的方式解构,分为两种情况: 定长数组 let arr: [u16; 2] = [114, 514];\nlet [x, y] = arr; assert_eq!(x, 114);\nassert_eq!(y, 514); 不定长数组 let arr: &[u16] = &[114, 514]; if let [x, ..] = arr { assert_eq!(x, &114);\n} if let &[.., y] = arr { assert_eq!(y, 514);\n} let arr: &[u16] = &[]; assert!(matches!(arr, [..]));\nassert!(!matches!(arr, [x, ..]));","breadcrumbs":"Rust 基础入门 » 模式匹配 » 全模式列表 » 解构并分解值","id":"164","title":"解构并分解值"},"165":{"body":"有时忽略模式中的一些值是很有用的,比如在 match 中的最后一个分支使用 _ 模式匹配所有剩余的值。 你也可以在另一个模式中使用 _ 模式,使用一个以下划线开始的名称,或者使用 .. 忽略所剩部分的值。 使用 _ 忽略整个值 虽然 _ 模式作为 match 表达式最后的分支特别有用,但是它的作用还不限于此。例如可以将其用于函数参数中: fn foo(_: i32, y: i32) { println!(\"This code only uses the y parameter: {}\", y);\n} fn main() { foo(3, 4);\n} 这段代码会完全忽略作为第一个参数传递的值 3,并会打印出 This code only uses the y parameter: 4。 大部分情况当你不再需要特定函数参数时,最好修改签名不再包含无用的参数。在一些情况下忽略函数参数会变得特别有用,比如实现特征时,当你需要特定类型签名但是函数实现并不需要某个参数时。此时编译器就 不会警告说存在未使用的函数参数 ,就跟使用命名参数一样。 使用嵌套的 _ 忽略部分值 可以在一个模式内部使用 _ 忽略部分值: let mut setting_value = Some(5);\nlet new_setting_value = Some(10); match (setting_value, new_setting_value) { (Some(_), Some(_)) => { println!(\"Can't overwrite an existing customized value\"); } _ => { setting_value = new_setting_value; }\n} println!(\"setting is {:?}\", setting_value); 这段代码会打印出 Can't overwrite an existing customized value 接着是 setting is Some(5)。 第一个匹配分支,我们不关心里面的值,只关心元组中两个元素的类型,因此对于 Some 中的值,直接进行忽略。 剩下的形如 (Some(_),None),(None, Some(_)), (None,None) 形式,都由第二个分支 _ 进行分配。 还可以在一个模式中的多处使用下划线来忽略特定值,如下所示,这里忽略了一个五元元组中的第二和第四个值: let numbers = (2, 4, 8, 16, 32); match numbers { (first, _, third, _, fifth) => { println!(\"Some numbers: {}, {}, {}\", first, third, fifth) },\n} 老生常谈:模式匹配一定要类型相同,因此匹配 numbers 元组的模式,也必须有五个值(元组中元素的数量也属于元组类型的一部分)。 这会打印出 Some numbers: 2, 8, 32, 值 4 和 16 会被忽略。 使用下划线开头忽略未使用的变量 如果你创建了一个变量却不在任何地方使用它,Rust 通常会给你一个警告,因为这可能会是个 BUG。但是有时创建一个不会被使用的变量是有用的,比如你正在设计原型或刚刚开始一个项目。这时你希望告诉 Rust 不要警告未使用的变量,为此可以用下划线作为变量名的开头: fn main() { let _x = 5; let y = 10;\n} 这里得到了警告说未使用变量 y,至于 x 则没有警告。 注意, 只使用 _ 和使用以下划线开头的名称有些微妙的不同:比如 _x 仍会将值绑定到变量,而 _ 则完全不会绑定 。 let s = Some(String::from(\"Hello!\")); if let Some(_s) = s { println!(\"found a string\");\n} println!(\"{:?}\", s); s 是一个拥有所有权的动态字符串,在上面代码中,我们会得到一个错误,因为 s 的值会被转移给 _s,在 println! 中再次使用 s 会报错: error[E0382]: borrow of partially moved value: `s` --> src/main.rs:8:22 |\n4 | if let Some(_s) = s { | -- value partially moved here\n...\n8 | println!(\"{:?}\", s); | ^ value borrowed here after partial move 只使用下划线本身,则并不会绑定值,因为 s 没有被移动进 _: let s = Some(String::from(\"Hello!\")); if let Some(_) = s { println!(\"found a string\");\n} println!(\"{:?}\", s); 用 .. 忽略剩余值 对于有多个部分的值,可以使用 .. 语法来只使用部分值而忽略其它值,这样也不用再为每一个被忽略的值都单独列出下划线。.. 模式会忽略模式中剩余的任何没有显式匹配的值部分。 struct Point { x: i32, y: i32, z: i32,\n} let origin = Point { x: 0, y: 0, z: 0 }; match origin { Point { x, .. } => println!(\"x is {}\", x),\n} 这里列出了 x 值,接着使用了 .. 模式来忽略其它字段,这样的写法要比一一列出其它字段,然后用 _ 忽略简洁的多。 还可以用 .. 来忽略元组中间的某些值: fn main() { let numbers = (2, 4, 8, 16, 32); match numbers { (first, .., last) => { println!(\"Some numbers: {}, {}\", first, last); }, }\n} 这里用 first 和 last 来匹配第一个和最后一个值。.. 将匹配并忽略中间的所有值。 然而使用 .. 必须是无歧义的。如果期望匹配和忽略的值是不明确的,Rust 会报错。下面代码展示了一个带有歧义的 .. 例子,因此不能编译: fn main() { let numbers = (2, 4, 8, 16, 32); match numbers { (.., second, ..) => { println!(\"Some numbers: {}\", second) }, }\n} 如果编译上面的例子,会得到下面的错误: error: `..` can only be used once per tuple pattern // 每个元组模式只能使用一个 `..` --> src/main.rs:5:22 |\n5 | (.., second, ..) => { | -- ^^ can only be used once per tuple pattern | | | previously used here // 上一次使用在这里 error: could not compile `world_hello` due to previous error ^^ Rust 无法判断,second 应该匹配 numbers 中的第几个元素,因此这里使用两个 .. 模式,是有很大歧义的!","breadcrumbs":"Rust 基础入门 » 模式匹配 » 全模式列表 » 忽略模式中的值","id":"165","title":"忽略模式中的值"},"166":{"body":"匹配守卫 ( match guard )是一个位于 match 分支模式之后的额外 if 条件,它能为分支模式提供更进一步的匹配条件。 这个条件可以使用模式中创建的变量: let num = Some(4); match num { Some(x) if x < 5 => println!(\"less than five: {}\", x), Some(x) => println!(\"{}\", x), None => (),\n} 这个例子会打印出 less than five: 4。当 num 与模式中第一个分支匹配时,Some(4) 可以与 Some(x) 匹配,接着匹配守卫检查 x 值是否小于 5,因为 4 小于 5,所以第一个分支被选择。 相反如果 num 为 Some(10),因为 10 不小于 5 ,所以第一个分支的匹配守卫为假。接着 Rust 会前往第二个分支,因为这里没有匹配守卫所以会匹配任何 Some 成员。 模式中无法提供类如 if x < 5 的表达能力,我们可以通过匹配守卫的方式来实现。 在之前,我们提到可以使用匹配守卫来解决模式中变量覆盖的问题,那里 match 表达式的模式中新建了一个变量而不是使用 match 之外的同名变量。内部变量覆盖了外部变量,意味着此时不能够使用外部变量的值,下面代码展示了如何使用匹配守卫修复这个问题。 fn main() { let x = Some(5); let y = 10; match x { Some(50) => println!(\"Got 50\"), Some(n) if n == y => println!(\"Matched, n = {}\", n), _ => println!(\"Default case, x = {:?}\", x), } println!(\"at the end: x = {:?}, y = {}\", x, y);\n} 现在这会打印出 Default case, x = Some(5)。现在第二个匹配分支中的模式不会引入一个覆盖外部 y 的新变量 y,这意味着可以在匹配守卫中使用外部的 y。相比指定会覆盖外部 y 的模式 Some(y),这里指定为 Some(n)。此新建的变量 n 并没有覆盖任何值,因为 match 外部没有变量 n。 匹配守卫 if n == y 并不是一个模式所以没有引入新变量。这个 y 正是 外部的 y 而不是新的覆盖变量 y,这样就可以通过比较 n 和 y 来表达寻找一个与外部 y 相同的值的概念了。 也可以在匹配守卫中使用 或 运算符 | 来指定多个模式, 同时匹配守卫的条件会作用于所有的模式 。下面代码展示了匹配守卫与 | 的优先级。这个例子中看起来好像 if y 只作用于 6,但实际上匹配守卫 if y 作用于 4、5 和 6 ,在满足 x 属于 4 | 5 | 6 后才会判断 y 是否为 true: let x = 4;\nlet y = false; match x { 4 | 5 | 6 if y => println!(\"yes\"), _ => println!(\"no\"),\n} 这个匹配条件表明此分支只匹配 x 值为 4、5 或 6 同时 y 为 true 的情况。 虽然在第一个分支中,x 匹配了模式 4 ,但是对于匹配守卫 if y 来说,因为 y 是 false,因此该守卫条件的值永远是 false,也意味着第一个分支永远无法被匹配。 下面的文字图解释了匹配守卫作用于多个模式时的优先级规则,第一张是正确的: (4 | 5 | 6) if y => ... 而第二张图是错误的 4 | 5 | (6 if y) => ... 可以通过运行代码时的情况看出这一点:如果匹配守卫只作用于由 | 运算符指定的值列表的最后一个值,这个分支就会匹配且程序会打印出 yes。","breadcrumbs":"Rust 基础入门 » 模式匹配 » 全模式列表 » 匹配守卫提供的额外条件","id":"166","title":"匹配守卫提供的额外条件"},"167":{"body":"@(读作 at)运算符允许为一个字段绑定另外一个变量。下面例子中,我们希望测试 Message::Hello 的 id 字段是否位于 3..=7 范围内,同时也希望能将其值绑定到 id_variable 变量中以便此分支中相关的代码可以使用它。我们可以将 id_variable 命名为 id,与字段同名,不过出于示例的目的这里选择了不同的名称。 enum Message { Hello { id: i32 },\n} let msg = Message::Hello { id: 5 }; match msg { Message::Hello { id: id_variable @ 3..=7 } => { println!(\"Found an id in range: {}\", id_variable) }, Message::Hello { id: 10..=12 } => { println!(\"Found an id in another range\") }, Message::Hello { id } => { println!(\"Found some other id: {}\", id) },\n} 上例会打印出 Found an id in range: 5。通过在 3..=7 之前指定 id_variable @,我们捕获了任何匹配此范围的值并同时将该值绑定到变量 id_variable 上。 第二个分支只在模式中指定了一个范围,id 字段的值可以是 10、11 或 12,不过这个模式的代码并不知情也不能使用 id 字段中的值,因为没有将 id 值保存进一个变量。 最后一个分支指定了一个没有范围的变量,此时确实拥有可以用于分支代码的变量 id,因为这里使用了结构体字段简写语法。不过此分支中没有像头两个分支那样对 id 字段的值进行测试:任何值都会匹配此分支。 当你既想要限定分支范围,又想要使用分支的变量时,就可以用 @ 来绑定到一个新的变量上,实现想要的功能。 @前绑定后解构(Rust 1.56 新增) 使用 @ 还可以在绑定新变量的同时,对目标进行解构: #[derive(Debug)]\nstruct Point { x: i32, y: i32,\n} fn main() { // 绑定新变量 `p`,同时对 `Point` 进行解构 let p @ Point {x: px, y: py } = Point {x: 10, y: 23}; println!(\"x: {}, y: {}\", px, py); println!(\"{:?}\", p); let point = Point {x: 10, y: 5}; if let p @ Point {x: 10, y} = point { println!(\"x is 10 and y is {} in {:?}\", y, p); } else { println!(\"x was not 10 :(\"); }\n} @新特性(Rust 1.53 新增) 考虑下面一段代码: fn main() { match 1 { num @ 1 | 2 => { println!(\"{}\", num); } _ => {} }\n} 编译不通过,是因为 num 没有绑定到所有的模式上,只绑定了模式 1,你可能会试图通过这个方式来解决: num @ (1 | 2) 但是,如果你用的是 Rust 1.53 之前的版本,那这种写法会报错,因为编译器不支持。 至此,模式匹配的内容已经全部完结,复杂但是详尽,想要一次性全部记住属实不易,因此读者可以先留一个印象,等未来需要时,再来翻阅寻找具体的模式实现方式。","breadcrumbs":"Rust 基础入门 » 模式匹配 » 全模式列表 » @绑定","id":"167","title":"@绑定"},"168":{"body":"Rust By Practice ,支持代码在线编辑和运行,并提供详细的 习题解答 。","breadcrumbs":"Rust 基础入门 » 模式匹配 » 全模式列表 » 课后练习","id":"168","title":"课后练习"},"169":{"body":"从面向对象语言过来的同学对于方法肯定不陌生,class 里面就充斥着方法的概念。在 Rust 中,方法的概念也大差不差,往往和对象成对出现: object.method() 例如读取一个文件写入缓冲区,如果用函数的写法 read(f, buffer),用方法的写法 f.read(buffer)。不过与其它语言 class 跟方法的联动使用不同(这里可能要修改下),Rust 的方法往往跟结构体、枚举、特征(Trait)一起使用,特征将在后面几章进行介绍。","breadcrumbs":"Rust 基础入门 » 方法 Method » 方法 Method","id":"169","title":"方法 Method"},"17":{"body":"CS(Computer Science:计算机科学)课程中咱们会学习大量的常用数据结构和算法,因此大家都养成了一种好习惯:学习一门新语言,先用它写个链表或图试试。 我的天,在 Rust 中 千万别这么干 ,你是在扼杀自己之前的努力!因为不像其它语言,链表在 Rust 中简直是地狱一般的难度,我见过太多英雄好汉难过链表关,最终黯然退幕。我不希望正在阅读此文的你也成为其中一个 :( 这些自引用类型(一种数据结构,它内部的某个字段又引用了其自身),它们堪称恶魔:不仅仅在蹂躏着新手,还在折磨着老手。有意思的是,它们的难恰恰是 Rust 的优点导致的:无 GC 也无手动内存管理还要做到内存安全。 这些优点并不是凭空产生,而是来源于 Rust 那一套强大、优美的机制,这些机制一旦你学到,就会被它巧妙的构思和设计征服,进而被 Rust 深深吸引!但是一切选择都有利弊,这种机制的弊端就在于实现链表这类数据结构时,会变得非常非常复杂。 你需要糅合各种知识,才能解决这个问题,但是这显然不是一个新手应该独自去面对的。总之,不会链表对于 Rust 的学习和写项目,真的没有任何影响,直接使用大神已经写好的数据结构就可以。 如果想要练手,我们可以换个方向开始,例如书中的入门和进阶实战项目都是非常好的选择。当然如果你就是喜欢征服困难,那没问题,就从链表开始。但是无论选择哪个,本书都将给你莫大的帮助,包括如何实现一个链表!","breadcrumbs":"避免从入门到放弃 » 千万别从链表或图开始练手","id":"17","title":"千万别从链表或图开始练手"},"170":{"body":"Rust 使用 impl 来定义方法,例如以下代码: struct Circle { x: f64, y: f64, radius: f64,\n} impl Circle { // new是Circle的关联函数,因为它的第一个参数不是self,且new并不是关键字 // 这种方法往往用于初始化当前结构体的实例 fn new(x: f64, y: f64, radius: f64) -> Circle { Circle { x: x, y: y, radius: radius, } } // Circle的方法,&self表示借用当前的Circle结构体 fn area(&self) -> f64 { std::f64::consts::PI * (self.radius * self.radius) }\n} 我们这里先不详细展开讲解,只是先建立对方法定义的大致印象。下面的图片将 Rust 方法定义与其它语言的方法定义做了对比: 可以看出,其它语言中所有定义都在 class 中,但是 Rust 的对象定义和方法定义是分离的,这种数据和使用分离的方式,会给予使用者极高的灵活度。 再来看一个例子: #[derive(Debug)]\nstruct Rectangle { width: u32, height: u32,\n} impl Rectangle { fn area(&self) -> u32 { self.width * self.height }\n} fn main() { let rect1 = Rectangle { width: 30, height: 50 }; println!( \"The area of the rectangle is {} square pixels.\", rect1.area() );\n} 该例子定义了一个 Rectangle 结构体,并且在其上定义了一个 area 方法,用于计算该矩形的面积。 impl Rectangle {} 表示为 Rectangle 实现方法(impl 是实现 implementation 的缩写),这样的写法表明 impl 语句块中的一切都是跟 Rectangle 相关联的。 self、&self 和 &mut self 接下来的内容非常重要,请大家仔细看。在 area 的签名中,我们使用 &self 替代 rectangle: &Rectangle,&self 其实是 self: &Self 的简写(注意大小写)。在一个 impl 块内,Self 指代被实现方法的结构体类型,self 指代此类型的实例,换句话说,self 指代的是 Rectangle 结构体实例,这样的写法会让我们的代码简洁很多,而且非常便于理解:我们为哪个结构体实现方法,那么 self 就是指代哪个结构体的实例。 需要注意的是,self 依然有所有权的概念: self 表示 Rectangle 的所有权转移到该方法中,这种形式用的较少 &self 表示该方法对 Rectangle 的不可变借用 &mut self 表示可变借用 总之,self 的使用就跟函数参数一样,要严格遵守 Rust 的所有权规则。 回到上面的例子中,选择 &self 的理由跟在函数中使用 &Rectangle 是相同的:我们并不想获取所有权,也无需去改变它,只是希望能够读取结构体中的数据。如果想要在方法中去改变当前的结构体,需要将第一个参数改为 &mut self。仅仅通过使用 self 作为第一个参数来使方法获取实例的所有权是很少见的,这种使用方式往往用于把当前的对象转成另外一个对象时使用,转换完后,就不再关注之前的对象,且可以防止对之前对象的误调用。 简单总结下,使用方法代替函数有以下好处: 不用在函数签名中重复书写 self 对应的类型 代码的组织性和内聚性更强,对于代码维护和阅读来说,好处巨大 方法名跟结构体字段名相同 在 Rust 中,允许方法名跟结构体的字段名相同: impl Rectangle { fn width(&self) -> bool { self.width > 0 }\n} fn main() { let rect1 = Rectangle { width: 30, height: 50, }; if rect1.width() { println!(\"The rectangle has a nonzero width; it is {}\", rect1.width); }\n} 当我们使用 rect1.width() 时,Rust 知道我们调用的是它的方法,如果使用 rect1.width,则是访问它的字段。 一般来说,方法跟字段同名,往往适用于实现 getter 访问器,例如: pub struct Rectangle { width: u32, height: u32,\n} impl Rectangle { pub fn new(width: u32, height: u32) -> Self { Rectangle { width, height } } pub fn width(&self) -> u32 { return self.width; }\n} fn main() { let rect1 = Rectangle::new(30, 50); println!(\"{}\", rect1.width());\n} 用这种方式,我们可以把 Rectangle 的字段设置为私有属性,只需把它的 new 和 width 方法设置为公开可见,那么用户就可以创建一个矩形,同时通过访问器 rect1.width() 方法来获取矩形的宽度,因为 width 字段是私有的,当用户访问 rect1.width 字段时,就会报错。注意在此例中,Self 指代的就是被实现方法的结构体 Rectangle。","breadcrumbs":"Rust 基础入门 » 方法 Method » 定义方法","id":"170","title":"定义方法"},"171":{"body":"在 C/C++ 语言中,有两个不同的运算符来调用方法:. 直接在对象上调用方法,而 -> 在一个对象的指针上调用方法,这时需要先解引用指针。换句话说,如果 object 是一个指针,那么 object->something() 和 (*object).something() 是一样的。 Rust 并没有一个与 -> 等效的运算符;相反,Rust 有一个叫 自动引用和解引用 的功能。方法调用是 Rust 中少数几个拥有这种行为的地方。 他是这样工作的:当使用 object.something() 调用方法时,Rust 会自动为 object 添加 &、&mut 或 * 以便使 object 与方法签名匹配。也就是说,这些代码是等价的: # #[derive(Debug,Copy,Clone)]\n# struct Point {\n# x: f64,\n# y: f64,\n# }\n#\n# impl Point {\n# fn distance(&self, other: &Point) -> f64 {\n# let x_squared = f64::powi(other.x - self.x, 2);\n# let y_squared = f64::powi(other.y - self.y, 2);\n#\n# f64::sqrt(x_squared + y_squared)\n# }\n# }\n# let p1 = Point { x: 0.0, y: 0.0 };\n# let p2 = Point { x: 5.0, y: 6.5 };\np1.distance(&p2);\n(&p1).distance(&p2); 第一行看起来简洁的多。这种自动引用的行为之所以有效,是因为方法有一个明确的接收者———— self 的类型。在给出接收者和方法名的前提下,Rust 可以明确地计算出方法是仅仅读取(&self),做出修改(&mut self)或者是获取所有权(self)。事实上,Rust 对方法接收者的隐式借用让所有权在实践中更友好。","breadcrumbs":"Rust 基础入门 » 方法 Method » -> 运算符到哪去了?","id":"171","title":"-> 运算符到哪去了?"},"172":{"body":"方法和函数一样,可以使用多个参数: impl Rectangle { fn area(&self) -> u32 { self.width * self.height } fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height }\n} fn main() { let rect1 = Rectangle { width: 30, height: 50 }; let rect2 = Rectangle { width: 10, height: 40 }; let rect3 = Rectangle { width: 60, height: 45 }; println!(\"Can rect1 hold rect2? {}\", rect1.can_hold(&rect2)); println!(\"Can rect1 hold rect3? {}\", rect1.can_hold(&rect3));\n}","breadcrumbs":"Rust 基础入门 » 方法 Method » 带有多个参数的方法","id":"172","title":"带有多个参数的方法"},"173":{"body":"现在大家可以思考一个问题,如何为一个结构体定义一个构造器方法?也就是接受几个参数,然后构造并返回该结构体的实例。其实答案在开头的代码片段中就给出了,很简单,参数中不包含 self 即可。 这种定义在 impl 中且没有 self 的函数被称之为 关联函数 : 因为它没有 self,不能用 f.read() 的形式调用,因此它是一个函数而不是方法,它又在 impl 中,与结构体紧密关联,因此称为关联函数。 在之前的代码中,我们已经多次使用过关联函数,例如 String::from,用于创建一个动态字符串。 # #[derive(Debug)]\n# struct Rectangle {\n# width: u32,\n# height: u32,\n# }\n#\nimpl Rectangle { fn new(w: u32, h: u32) -> Rectangle { Rectangle { width: w, height: h } }\n} Rust 中有一个约定俗成的规则,使用 new 来作为构造器的名称,出于设计上的考虑,Rust 特地没有用 new 作为关键字。 因为是函数,所以不能用 . 的方式来调用,我们需要用 :: 来调用,例如 let sq = Rectangle::new(3, 3);。这个方法位于结构体的命名空间中::: 语法用于关联函数和模块创建的命名空间。","breadcrumbs":"Rust 基础入门 » 方法 Method » 关联函数","id":"173","title":"关联函数"},"174":{"body":"Rust 允许我们为一个结构体定义多个 impl 块,目的是提供更多的灵活性和代码组织性,例如当方法多了后,可以把相关的方法组织在同一个 impl 块中,那么就可以形成多个 impl 块,各自完成一块儿目标: # #[derive(Debug)]\n# struct Rectangle {\n# width: u32,\n# height: u32,\n# }\n#\nimpl Rectangle { fn area(&self) -> u32 { self.width * self.height }\n} impl Rectangle { fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height }\n} 当然,就这个例子而言,我们没必要使用两个 impl 块,这里只是为了演示方便。","breadcrumbs":"Rust 基础入门 » 方法 Method » 多个 impl 定义","id":"174","title":"多个 impl 定义"},"175":{"body":"枚举类型之所以强大,不仅仅在于它好用、可以 同一化类型 ,还在于,我们可以像结构体一样,为枚举实现方法: #![allow(unused)]\nenum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32),\n} impl Message { fn call(&self) { // 在这里定义方法体 }\n} fn main() { let m = Message::Write(String::from(\"hello\")); m.call();\n} 除了结构体和枚举,我们还能为特征(trait)实现方法,这将在下一章进行讲解,在此之前,先来看看泛型。","breadcrumbs":"Rust 基础入门 » 方法 Method » 为枚举实现方法","id":"175","title":"为枚举实现方法"},"176":{"body":"Rust By Practice ,支持代码在线编辑和运行,并提供详细的 习题解答 。","breadcrumbs":"Rust 基础入门 » 方法 Method » 课后练习","id":"176","title":"课后练习"},"177":{"body":"泛型和特征是 Rust 中最最重要的抽象类型,也是你在学习 Rust 路上的拦路虎,但是挑战往往与乐趣并存,一旦学会,在后面学习 Rust 的路上,你将一往无前。","breadcrumbs":"Rust 基础入门 » 泛型和特征 » 泛型和特征","id":"177","title":"泛型和特征"},"178":{"body":"Go 语言在 2022 年,就要正式引入泛型,被视为在 1.0 版本后,语言特性发展迈出的一大步,为什么泛型这么重要?到底什么是泛型?Rust 的泛型有几种? 本章将一一为你讲解。 我们在编程中,经常有这样的需求:用同一功能的函数处理不同类型的数据,例如两个数的加法,无论是整数还是浮点数,甚至是自定义类型,都能进行支持。在不支持泛型的编程语言中,通常需要为每一种类型编写一个函数: fn add_i8(a:i8, b:i8) -> i8 { a + b\n}\nfn add_i32(a:i32, b:i32) -> i32 { a + b\n}\nfn add_f64(a:f64, b:f64) -> f64 { a + b\n} fn main() { println!(\"add i8: {}\", add_i8(2i8, 3i8)); println!(\"add i32: {}\", add_i32(20, 30)); println!(\"add f64: {}\", add_f64(1.23, 1.23));\n} 上述代码可以正常运行,但是很啰嗦,如果你要支持更多的类型,那么会更繁琐。程序员或多或少都有强迫症,一个好程序员的公认特征就是 —— 懒,这么勤快的写一大堆代码,显然不是咱们的优良传统,是不? 在开始讲解 Rust 的泛型之前,先来看看什么是多态。 在编程的时候,我们经常利用多态。通俗的讲,多态就是好比坦克的炮管,既可以发射普通弹药,也可以发射制导炮弹(导弹),也可以发射贫铀穿甲弹,甚至发射子母弹,没有必要为每一种炮弹都在坦克上分别安装一个专用炮管,即使生产商愿意,炮手也不愿意,累死人啊。所以在编程开发中,我们也需要这样“通用的炮管”,这个“通用的炮管”就是多态。 实际上,泛型就是一种多态。泛型主要目的是为程序员提供编程的便利,减少代码的臃肿,同时可以极大地丰富语言本身的表达能力,为程序员提供了一个合适的炮管。想想,一个函数,可以代替几十个,甚至数百个函数,是一件多么让人兴奋的事情: fn add(a:T, b:T) -> T { a + b\n} fn main() { println!(\"add i8: {}\", add(2i8, 3i8)); println!(\"add i32: {}\", add(20, 30)); println!(\"add f64: {}\", add(1.23, 1.23));\n} 将之前的代码改成上面这样,就是 Rust 泛型的初印象,这段代码虽然很简洁,但是并不能编译通过,我们会在后面进行详细讲解,现在只要对泛型有个大概的印象即可。","breadcrumbs":"Rust 基础入门 » 泛型和特征 » 泛型 Generics » 泛型 Generics","id":"178","title":"泛型 Generics"},"179":{"body":"上面代码的 T 就是 泛型参数 ,实际上在 Rust 中,泛型参数的名称你可以任意起,但是出于惯例,我们都用 T ( T 是 type 的首字母)来作为首选,这个名称越短越好,除非需要表达含义,否则一个字母是最完美的。 使用泛型参数,有一个先决条件,必需在使用前对其进行声明: fn largest(list: &[T]) -> T { 该泛型函数的作用是从列表中找出最大的值,其中列表中的元素类型为 T。首先 largest 对泛型参数 T 进行了声明,然后才在函数参数中进行使用该泛型参数 list: &[T] (还记得 &[T] 类型吧?这是 数组切片 )。 总之,我们可以这样理解这个函数定义:函数 largest 有泛型类型 T,它有个参数 list,其类型是元素为 T 的数组切片,最后,该函数返回值的类型也是 T。 具体的泛型函数实现如下: fn largest(list: &[T]) -> T { let mut largest = list[0]; for &item in list.iter() { if item > largest { largest = item; } } largest\n} fn main() { let number_list = vec![34, 50, 25, 100, 65]; let result = largest(&number_list); println!(\"The largest number is {}\", result); let char_list = vec!['y', 'm', 'a', 'q']; let result = largest(&char_list); println!(\"The largest char is {}\", result);\n} 运行后报错: error[E0369]: binary operation `>` cannot be applied to type `T` // `>`操作符不能用于类型`T` --> src/main.rs:5:17 |\n5 | if item > largest { | ---- ^ ------- T | | | T |\nhelp: consider restricting type parameter `T` // 考虑对T进行类型上的限制 : |\n1 | fn largest(list: &[T]) -> T { | ++++++++++++++++++++++ 因为 T 可以是任何类型,但不是所有的类型都能进行比较,因此上面的错误中,编译器建议我们给 T 添加一个类型限制:使用 std::cmp::PartialOrd 特征(Trait)对 T 进行限制,特征在下一节会详细介绍,现在你只要理解,该特征的目的就是让 类型实现可比较的功能 。 还记得我们一开始的 add 泛型函数吗?如果你运行它,会得到以下的报错: error[E0369]: cannot add `T` to `T` // 无法将 `T` 类型跟 `T` 类型进行相加 --> src/main.rs:2:7 |\n2 | a + b | - ^ - T | | | T |\nhelp: consider restricting type parameter `T` |\n1 | fn add>(a:T, b:T) -> T { | +++++++++++++++++++++++++++ 同样的,不是所有 T 类型都能进行相加操作,因此我们需要用 std::ops::Add