You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Rust 中的 if 语句必须接收一个布尔值,不像 JavaScript 这门语言会自动转换,还可以省略括号。
fnmain(){let number = 1;if number < 2{println!("true");// true}else{println!("false");}}
如果预期不是一个布尔值,编译阶段就会报错。
fnmain(){let number = 1;if number {println!("true");}}
运行之后,报错如下:
cargo run
Compiling hello-rust v0.1.0(/Users/xxxxx/study/hello-rust)
error[E0308]: mismatched types
--> src/main.rs:3:8
|
3 | if number {
| ^^^^^^ expected `bool`, found integer
error:aborting due to previous error
在 let 中使用 if 表达式,注意 if else 分支的数据类型要一致,因为 Rust 是静态类型,需要在编译期间确定所有的类型。
fnmain(){let condition = true;let num = if condition {1}else{0};println!("num: {}", num);// 1}
Rust 提供的匹配模式允许将一个值与一系列的模式比较,并根据匹配的模式执行相应的代码块,使用表达式 match 表示。
定义 match 匹配模式示例
举一个例子,我们可以定义一个 Language 枚举,代表编程语言,之后定义一个函数 get_url_by_language 根据语言获取一个对应的地址,match 表达式的结果就是这个函数的结果。看起来有点像 if 表达式,但是 if 只能返回 true 或 false,match 表达式可以返回任何类型。
$ cargo run
Compiling hello-rust v0.1.0(/Users/xxx/study/hello-rust)
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:4:28
|
2 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait3 | let s2 = s1;
| -- value moved here
4 | print!("s1 {}, s2 {}", s1, s2);
| ^^ value borrowed here after move
pubstructWorker{pubname:String,pubage:i32}implPersonforWorker{fnintro(&self) -> String{format!("My name is {}, age {}, is a worker", self.name, self.age)}}
在 main.rs 中引入 person.rs 模块。
mod 关键字用来加载需要引入的文件为我们的模块,use 表示加载 person 这个模块定义的 trait 和结构体。
mod person;use person::{Person,Worker};fnmain(){let may_jun = Worker{name:String::from("五月君"),age:20};println!("{}", may_jun.intro());}
学习 Rust 之前,在知乎等平台也看到过一些回答,认为 Rust 学习曲线陡峭、难学,个人觉得如果有些 C/C++ 的基础其实学起来也还好,只不过 Rust 有很多独有的概念,这一点是和现有很多主流语言是不同的,需要花点时间看下。
本文是笔者在 Rust 学习过程中所记录的,在掘金看到了 Rust 技术征文,整理出这份基础的不完全入门指南,也希望能帮助一些学习 Rust 的朋友。
脑图
一个脑图概括本文所有知识点。
发展历史
Rust 语言是 Mozilla 员工 Craydon Hoare 在 2006 年创建的一个业余项目,2012 年 Mozilla 宣布推出基于 Rust 语言开发的以内存安全性、 并发性为首要原则的新浏览器引擎 Servo 这也是其首个完整的大型项目。
2015 年发布首个 Rust v1.0 版本,这是第一个重要的里程碑,近期在过去的 2020 年由于疫情原因,Mozilla 宣布裁员,涉及到一些 Rust 项目和 Rust 社区中的活跃成员,这对外界对于 Rust 猜测又增加了更多不确定性。
今年的 2 月 9 号,Rust 基金会 https://foundation.rust-lang.org/ 宣布成立,从 Mozilla 脱离出来,目前的基金会董事成员包括:亚马逊、Google、微软、Mozilla 和国内的华为,由五大科技巨头支持,对 Rust 来说总归是好事,可以为这门语言促进更好的发展,也有着更好的前景,社区看到的一句话:“Go 是当下,Rust 是未来”。
特点
let a = 1;
声明一个变量,看似给 JavaScript 一样的,Rust 中类型推断的结果可能是这样的let a: i32 = 1;
安装
在线尝试
如果你不想在本地电脑上安装,想尽快尝试下 Rust 可通过其提供的在线代码编辑器。
https://play.rust-lang.org
类 Unix 系统
如果使用 MacOS、Linux 或其它的类 Unix 系统,可以下载
Rustup
安装 Rust,Rustup
即是一个 Rust 安装器又是一个版本管理工具。终端运行如下命令,根据提示完成即可。
Rust 目前的升级很频繁,如果你已安装很长一段时间,可通过
rustup update
命令更新最新版本。Rust 的所有工具存在于
~/.cargo/bin
目录下,正常情况下安装时会配置环境变量,但是由于不同平台、shell 之间存在的差异,可能会存在一些问题,导致在终端未重启或用户未重新登陆之前,rustup
对环境变量的修改不生效,如果存在问题rustc --version
命令就会执行失败。Windows 系统
在 Windows 平台上,通过下载可执行应用程序 rustup-init.exe 安装。
编辑器
Rust 支持多种编辑器 VS CODE、SUBLIME TEXT 3、ATOM、INTELLIJ IDEA、ECLIPSE、VIM、EMACS、GEANY。
以笔者常用的 VS CODE 做一个介绍。
推荐两个 Rust 的 VS CODE 插件: rust-analyzer、rust。
Hello Rust!
创建一个项目
cargo new hello-rust
查看目录结构
tree -a
看下
Cargo.toml
的内容,这个类似于 Node.js 中的 package.json 声明了项目所需的信息,对于 Rust 项目来说就是声明了Cargo
编译程序包所需的元数据,以.toml
文件格式编写。TOML 一种新的配置文件格式。
主程序 main.rs 使用 function 的简写
fn
声明了一个函数,注意,里面 println! 后面加上了符号!
并不是一个函数,而是一个宏。编译我们的 Rust 项目,之后会在 target/debug/ 目录下生成编译好的文件,如下所示:
Rust 的 Release 编译模式,如下所示:
开发阶段每次编译再执行,你可能会感觉很麻烦,Rust 允许我们使用
cargo run
直接编译运行,该命令会自动帮我们做编译操作,如下所示:数据类型
变量
Rust 使用
let
声明一个变量,通常来说变量将是可变的,但是在 Rust 中默认设置的变量是预设不可变的,这也是 Rust 推动你能充分利用其提供的安全性来写程序的方式之一,Rust 中鼓励你多多使用不可变的,当然如果你明确知道该变量是可变得,也是可以的。以下我们声明的变量 num 并没有明确其类型,这也是 Rust 的特性之一类型推断
运行之后会得到一个
cannot assign twice to immutable variable 'num'
错误,这在编译器阶段就是不会通过的。在变量名称前加上
mut
关键字,表明该变量是可变的。常量
常量使用 const 声明,之后是不可变的,在声明时必须指定变量类型,这也是与 let 的不同,还需注意的是常量名称一定要大写,否则编译阶段也是会报错的。
作用域
一个变量只有在其作用域内是生效的。下例,变量 y 在花括号内即它的块级作用域内是有效的,当离开花括号如果想在外部打印,会报 cannot find value
y
in this scope 错误。基本数据类型
Rust 是一个静态数据类型的语言,这意味着在编译时它就要知道变量的类型。
Rust 包含四种基本数据类型分别为:整形、浮点型、布尔型、字符型。
整型
Rust 里的整型又分为带符号的整型(signed)和非带符号整型(unsigned),两者之间的区别是数字是否是负数。$-(2^{n-1})$ 到 $2^{n-1}-1$ ,n 就是下面的长度。
带符号整型的安全存储范围为
非带符号整型的安全存储范围为 0 到$2^{n}-1$ 。
isize 和 usize 是根据系统架构决定的,例如带符号整型,如果系统是 64 位,类型为 i64,如果系统是 32 位,类型为 i32。
浮点型
Rust 的浮点型提供了两种数据类型 f32、f64,分别表示为 32 位与 64 位,默认情况下是 64 位。
布尔型
和大多数编程语言一样,Rust 中的布尔型包含两个值:true 和 false。
字符型
Rust 中的字符型为一个 Unicode 码,大小为 4 bytes,char 类型使用单引号包括。
复合类型
复合类型可以组合多个数值为一个类别,复合类型包含两种:元组(tuples)和数组(arrays)。
元组
元组是将多个不同数值组合为一个复合类型的常见方法,元组拥有固定长度,一旦声明无法更改。
我们通过解构的方式,分别从声明的元组中取出数据,如下例所示:
除此之外我们还可通过数值的索引来访问元组中的数据。
数组
与元组不同的是数组中的所有元素类型必须一致,Rust 中的 Array 与其它语言不太一样,因为其 Array 的长度是固定的和元组一样。
流程控制
if 表达式
Rust 中的 if 语句必须接收一个布尔值,不像 JavaScript 这门语言会自动转换,还可以省略括号。
如果预期不是一个布尔值,编译阶段就会报错。
运行之后,报错如下:
在 let 中使用 if 表达式,注意 if else 分支的数据类型要一致,因为 Rust 是静态类型,需要在编译期间确定所有的类型。
loop 循环
loop 表达式会无限的循环执行代码块,如果想终止循环,可配合 break 语句使用。
while 循环
使用 while 可以加上条件判断决定是否还要循环多少次,如果条件为 true 继续循环,条件为 false 则退出循环。
for 循环
使用 for 循环遍历集合元素,例如在访问一个数组时,增加了程序的安全性不会出现超出数组大小或读取长度不足的情况。
在 Rust 中使用 for 循环的另一种方式。
结构体/函数/方法/枚举
函数
在 Rust 代码中函数随处可见,例如我们使用的 main 函数,关于函数的几个特点总结如下:
->
后声明返回类型,默认情况下返回最后一个表达式,注意不要有分号;
;
。结构体
结构体是一种自定义数据类型,它由一系列属性组成(这个属性拥有自己的属性和值),结构体是数据的集合,就像面向对象编程语言中一个无方法的轻量级类,因为 Rust 本身不是一门面向对象的语言,合理的使用结构体可以使我们的程序更加的结构化。
定义一个结构体
使用 struct 关键字定义一个结构体,创建一个结构体实例也很简单,如下例所示:
方法
方法与函数类似,使用 fn 关键字声明,拥有参数和返回值,不同的是方法在结构体的上下文中定义,方法的第一个参数始终为
self
表示调用该方法的结构体实例。改写上面的结构体示例,在结构体 User 上定义一个方法 info 打印信息,这里用到一个关键字
impl
,它是 implementation 的缩写。枚举
模式匹配
Rust 提供的匹配模式允许将一个值与一系列的模式比较,并根据匹配的模式执行相应的代码块,使用表达式 match 表示。
定义 match 匹配模式示例
举一个例子,我们可以定义一个 Language 枚举,代表编程语言,之后定义一个函数 get_url_by_language 根据语言获取一个对应的地址,match 表达式的结果就是这个函数的结果。看起来有点像 if 表达式,但是 if 只能返回 true 或 false,match 表达式可以返回任何类型。
这个示例分为三个小知识点:
Go
匹配,因为这个分支我们仅需要返回一个值,可以不使用大括号。Rust
匹配,这次我们需要在分支中执行多行代码,可以使用大括号。JavaScript
匹配,这次我们想对匹配的模式绑定一个值,可以修改枚举的一个成员来存放数据,这种模式称为绑定值的模式。匹配 Option 与 Some(value)
Option 是 Rust 系统定义的一个枚举类型,它有两个变量:None 表示失败、Some(value) 是元组结构体,封装了一个范型类型的值 value。
Rust 匹配模式还有一个概念**匹配是穷尽的,**上例中
None => None
是必须写的,否则会报pattern None not covered
错误,编译阶段就不会通过的。一个简单的示例,看懂 Rust 多种模式匹配
|
符号实现多值匹配。..=
符号实现范围匹配,注意,之前是使用...
现在该方式已废弃。_
符号是匹配穷进行,Rust 要检查所有被覆盖的情况。if let 简单控制流
我们想仅在
Some(value)
匹配时做些处理,其它情况不想考虑,为了满足match
表达式穷进性的要求,还要在写上_ => ()
来匹配其它的情况,类似代码多了显然繁琐。只针对一种模式做匹配处理的场景下,可以使用 if let 语法,可以更少的代码来写,如下所示:
所有权
什么是所有权?
所有权是 Rust 的核心特色之一,它让 Rust 无需垃圾回收即可保证内存安全。我们先记住这样一句话: 在 Rust 里每一个值都有一个唯一的所有者,如果当我们对这个值做一个赋值操作,那么这个值的所有权也将发生转移,当所有者离开作用域,这个值也会随之被销毁。
对许多人来说这是一个全新的概念,在接下来我们慢慢来了解它。
内存管理方式
也不得不说下目前的内存管理方式, 一类是类似于 C 语言这样的需要手动分配、释放内存,在 C 语言中可以使用 malloc/free 这两个函数,手动管理内存如果出现遗漏释放、过早释放、重复释放等这些问题也是最容易制造 Bug 的。 **另一类是类似于 JavaScript、Java 这类的高级语言由垃圾回收机制自动管理内存, **你不需要担心有什么内容会忘记释放,也不需要担心过早的释放。
Rust 采用了第三种方式,通过所有权系统管理内存,编译器在编译时会根据一系列规则做检查,如果出现错误,例如堆上的一个值其所在的变量 x 被赋值给一个新的变量 y,如果此后的程序还在使用 x,就会报错,因为一个值只有一个所有者,下文 "复杂数据类型 — 移动" 这个小节会讲到这个示例。
Rust 内存分配与自动释放
基本数据类型,类似于 i32、char 这些的长度都是固定已知的,程序可以轻松的分配一定的内存给它,且它们都是存储在栈上在离开所在的作用域时也会被移除栈。对于一些复杂的数据类型,例如 String 它的长度在编写时是未知的,成勋运行过程中是有可能改变它的长度的,这个类型就存储在堆上。
类似 String 类型的数据它的过程是这样的:
看一段小示例:
如下图所示, 左侧是存储在栈上的数据,ptr 指向存放字符串内容的指针,len 是 s1 内容使用了多少字节的内存,capacity 是容量表示从操作系统获取了多少字节的内存。 右侧是存储在堆上的数据。
当我们执行
String::from("hello")
时,这是第一步操作,实现请求所需的内存。在变量 s1 离开作用域后会被自动释放掉,这是第二步操作,但这块不需要开发者手动操作,Rust 会为我们调用一个特殊的函数 drop,在这里 String 的作者可以放置释放内存的代码。Rust 在结尾的}
处自动调用 drop。基本数据类型 — 赋值
声明基本数据类型 x 将其赋值给变量 y,由于这是一个已知固定大小的值,因此被放入了栈中,赋值的过程也是一个拷贝的过程,现在栈中既有 x 也有 y,程序是可以正常执行的。
复杂数据类型 — 移动
接下来让我们看一个复杂数据类型的赋值,和上面的示例类似,不过这次使用的 String 类型。
运行之后,报错如下:
String 类型的数据存储在堆上,那么赋值时也并不会在堆上拷贝一份,如果真的是拷贝在运行时过程中一旦堆上数据比较大这对性能的影响也是很大的。
为了确保安全,Rust 在这种场景下有一个值得注意的细节,**当尝试拷贝被分配的内存时,Rust 会使第一个变量无效,这个过程在 Rust 中称为移动,**可以看作 s1 被移动到了 s2 当 s2 离开作用域时,它就会自动释放自己的内存,这里也再次证明一点,在 Rust 同一时间每一个值仅有一个所有者。
复杂数据类型 — 拷贝
基本数据类型存储在栈上,赋值就是一个拷贝的过程,在堆上的数据当你需要拷贝时,也是可以通过一个 clone 的通用函数实现的。
所有权与函数
将值传递给函数与变量赋值类似,值的所有权会发生变化,如下示例所示,外层 s1 处是会报错的,这在编译阶段会报 borrow of moved value:
s1
错误。解决这个问题的一个办法是转移函数返回值的所有权。
但是这样实现起来难免啰嗦,还可以通过 引用 简单的实现。
所有权与引用、借用
引用
符号 & 表示引用,&s1 为我们创建一个指向值 s1 的引用,但并不拥有它,所有权没有发生转移。
借用
引用也许还可以理解,那么借用又是什么呢?在 Rust 中我们获取引用做为函数的参数称为 借用,这里就需要注意了,预设变量默认是不可变的,想修改引用的值还需使用可变引用,在特定作用域中数据有且只有一个可变引用,好处是在编译时即可避免数据竞争。
范型
范型是对具体类型的一种抽象,常用在强类型编程语言中,高效的处理重复概念。例如我们定义一个函数,参数可能有多种类型的值传递,那么就不能用具体的类型来声明,可以在编写代码时使用范型来指定类型,在实例化时做为参数指明这些类型。
在函数中定义范型
一个比较大小的范型示例
为了开启比较功能,我们要使用到 trial 标准库中定义的
std::cmp::PartialOrd
两个数相加的范型示例
在结构体中定义范型
我们通常使用 T 来标识一个范型,例如我们定义一个坐标结构体,x、y 可能同时是不同的类型,这样就需要定义两个范型参数。也要尽可能的避免在参数中定义太多的范型参数,这会让代码看起来很难阅读和理解。
在方法中定义范型
必须在 impl 后声明范型 T,对应我们的示例因为有多个范型参数,所以就是 T1、T2。
在枚举中定义范型
Option 和 Result 这两个枚举都拥有范型,由 Rust 标准库提供。
以 Resut 举例,如果我们读取一个环境变量,成功时触发 Ok 函数,失败时触发 Err 函数。
trait — 定义共享的行为
trait 是告诉 Rust 编译器某种类型具有哪些可以与其它类型共享的功能,抽象的定义共享行为,简单来说就是把方法的签名放在一起,定义实现某一目的所必须的一组行为。与其它语言的接口(Interface)类似,也还有些区别。
trait 定义
创建文件 person.rs 使用 trait 定义行为 Person,例如,每个人都有一个简单的介绍,在大括号内声明实现这个 trail 行为所需要的签名方法。
注意,trait 定义的类型里只有方法签名,没有具体实现,每一个方法签名独占一行,以
;
结尾,pub 代表公用的,可被外部其它模块调用。trait 实现
实现 trait 定义的行为,使用 impl 后跟自定义的 trait,for 后跟自定义的结构体,大括号内编写实现 Person 这个行为类型声明的方法签名,这点看起来和其它面向对象编程语言中的 Interface 相似。
format!() 是格式化为一个字符串返回。
在 main.rs 中引入 person.rs 模块。
mod 关键字用来加载需要引入的文件为我们的模块,use 表示加载 person 这个模块定义的 trait 和结构体。
trait 默认实现
可以为 trait 中某些签名方法提供默认的行为,这样当某个特定的类型实现 trait 时,可以选择保留或重载签名方法提供的默认行为。
修改 person.rs,当为方法提供默认行为之后,无法在 Worker 中再次定义 intro 方法了,但是可以在 Worker 实例后调用。
trait 做为参数
把 trait 做为参数,例如定义 intro 方法,传入的 item 需要是实现了 Person 这个 trait 的参数类型,这样我们可以直接调用 Person 这个 trait 上定义的 intro() 方法。
修改 main.rs 引入 person.rs 文件里定义的 print_intro() 执行时传入实现了 Person 这个 trait 的结构体类型。
trait 做为返回值
可以定义实现了某个 trait 的类型为返回值,与上面的例子结合使用,修改 main.rs 文件。
Trait Bounds
Trait Bounds 适用于复杂的场景,例如方法参数中定义多个实现了 trait 的类型。
Trait Bounds 与范型参数声明在一起,改写上面的例子如下所示:
总结与思考
以上仅是关于 Rust 一些基础知识、独有概念的一些介绍,篇幅有限也还有很多内容没有写,欢迎关注【五月君】后续也会继续分享。
对 Rust 感兴趣的朋友推荐去看看《The Rust Programming Language》这本书,官方开源的 Rust 教程,写的挺好,也有中文版的翻译,想看视频的可以上 bilibili 搜索下 Rust 入门,有几个视频版本的讲解基本上也是参考的这本书,重点还是要多思考、多实践。
社区上会看到一些话题:“Rust 可能取代 C 语言吗?”,Rust 提供了系统编程的能力,从能力上来讲 C 的一些实现 Rust 也是有能力去做的,但是 C/C++ 这个地位是很难被取代的,生态已经很大了,也并不是所有的项目都需要重写,也是有成本在的生态、人员等也不是一蹴而就的。
编程语言只是工具,为我们实现某些业务或功能的编程工具,不要盲目互吹或黑某一门语言,例如某乎上经常看到的 “xxxx 年了,xxx 凉了吗?”,多学习不同编程语言背后的设计思想、优势与劣势,磨练技艺、突破自我、适时选择。
Reference
The text was updated successfully, but these errors were encountered: