赞
踩
Rust 是一门系统级编程语言,被设计为保证内存和线程安全,并防止段错误。作为系统级编程语言,它的基本理念是 “零开销抽象”。理论上来说,它的速度与 C / C++ 同级。
Rust 可以被归为通用的、多范式、编译型的编程语言,类似 C 或者 C++。与这两门编程语言不同的是,Rust 是线程安全的!
Rust 编程语言的目标是,创建一个高度安全和并发的软件系统。它强调安全性、并发和内存控制。尽管 Rust 借用了 C 和 C++ 的语法,它不允许空指针和悬挂指针,二者是 C 和 C++ 中系统崩溃、内存泄露和不安全代码的根源。
Rust 中有诸如 if else 和循环语句 for 和 while 的通用控制结构。和 C 和 C++ 风格的编程语言一样,代码段放在花括号中。
Rust 使用实现(implementation)、特征(trait)和结构化类型(structured type)而不是类(class)。这点,与基于继承的OO语言 C++, Java 有相当大的差异。而跟 Ocaml, Haskell 这类函数式语言更加接近。
Rust 做到了内存安全而无需 .NET 和 Java 编程语言中实现自动垃圾收集器的开销,这是通过所有权/借用机制、生命周期、以及类型系统来达到的。
Rust 运行在以下操作系统上:Linux, OS X, Windows, FreeBSD, Android, iOS。
https://www.rust-lang.org/zh-CN/other-installers.html
Rust的安装必须依赖官方源,但是国内速度很慢,有一些加速的办法。
中国科学技术大学镜像源包含 rust-static,梯子暂时出问题的同学可以尝试从这里下载编译器;除此之外。还有 Crates 源,详见这里的说明。
更多细节,参考之前的Rust绿色版教程。
目前没有最好的IDE,按照个人喜欢使用VSCode, Atom, Idea都可以。
rustup 是rust官方的版本管理工具。应当作为安装 Rust 的首选。
项目主页是: https://github.com/rust-lang-nursery/rustup.rs
管理安装多个官方版本的 Rust 二进制程序。
配置基于目录的 Rust 工具链。
安装和更新来自 Rust 的发布通道: nightly, beta 和 stable。
接收来自发布通道更新的通知。
从官方安装历史版本的 nightly 工具链。
通过指定 stable 版本来安装。
安装额外的 std 用于交叉编译。
安装自定义的工具链。
独立每个安装的 Cargo metadata。
校验下载的 hash 值。
校验签名 (如果 GPG 存在)。
断点续传。
只依赖 bash, curl 和常见 unix 工具。
支持 Linux, OS X, Windows(via MSYS2)。
常用命令:
rustup default [toolchain] 配置默认工具链。
rustup show 显示当前安装的工具链信息。
rustup update 检查安装更新。
rustup toolchain [SUBCOMMAND] 配置工具链
rustup override [SUBCOMMAND] 配置一个目录以及其子目录的默认工具链
rustup target [SUBCOMMAND] 配置工具链的可用目标
rustup component 配置 rustup 安装的组件
racer是一个由rust的爱好者提供的rust自动补全和语法分析工具,被用来提供基本的补全功能和定义跳转功能。其本身完全由rust写成,补全功能已经比较完善了。
cargo install racer
为了对Rust标准库进行补全,racer需要获取Rust源码路径。
设置名为RUST_SRC_PATH的环境变量为[path_to_your_rust_source]/src
Rust Langular Server(下文简称RLS)可以为很多IDE或编辑器提供包括不限于自动补全、跳转定义、重命名、跳转类型的功能支持。
cargo install rustfmt
rust文件以.rs
为后缀,下面给出一个典型的helloworld代码:
// hello.rs
fn main() {
println!("Hello World!");
}
编译:rustc hello.rs -O #加入优化选项
解释:
fn
表示定义一个函数,main
是这个函数的名字,花括号{}
里的语句则表示这个函数的内容。main
的函数有特殊的用途,那就是作为程序的入口,也就是说程序每次都从这个函数开始运行。println!("Hello World!");
,这里println!
是一个Rust语言自带的宏, 这个宏的功能就是打印文本(结尾会换行),而"Hello World!"
这个用引号包起来的东西是一个字符串,就是我们要打印的文本。;
吧, 在Rust语言中,分号用来把语句分隔开,也就是说语句的末尾一般用分号做为结束标志。Rust中一般使用cargo管理项目,比如:
cargo new hellorust --bin
cargo build --release
cargo run --release
cargo clean
rust使用snake_case
风格来命名函数,即所有字母小写并使用下划线类分隔单词,如:foo_bar
。
// 固定变量
let a1 = 5;
let a2:i32 = 2;
let a3 = 2.3f32; // 类似a2
assert_eq!(a1,a2);
// 可变变量
let mut b:f64 = 1.0;
b = 1.2;
let b = b; // 变为固定变量
// let高级用法
let (a, mut b): (bool,bool) = (true, false);
println!("a = {:?}, b = {:?}", a, b);
Rust内置的原生类型 (primitive types) 有以下几类:
true
和false
。str
,更常用的是字符串切片&str
和堆分配字符串String
, 其中字符串切片是静态分配的,有固定的大小,并且不可变,而堆分配字符串是可变的。[T;N]
具有固定大小,并且元素都是同种类型。Vec<T>
, 其中T是一个泛型。它不是基础类型,但是用的很广泛。 &[T] 或者&mut [T]
引用一个数组的部分数据并且不需要拷贝。(1,true,"xxx")
具有固定大小的有序列表,每个元素都有自己的类型,通过解构或者索引来获得每个元素的值。*const T
和*mut T
,但解引用它们是不安全的,必须放到unsafe
块里。()
,其唯一的值也是()
。类似void
的作用。// boolean type
let t = true;
let f: bool = false;
// char type
let c = 'c';
// numeric types
let x = 42;
let y: u32 = 123_456;
let z: f64 = 1.23e+2;
let zero = z.abs_sub(123.4);
let bin = 0b1111_0000;
let oct = 0o7320_1546;
let hex = 0xf23a_b049;
// string types
let str = "Hello, world!";
let mut string = str.to_string();
// arrays and slices
let a = [0, 1, 2, 3, 4];
let middle = &a[1..4];
let mut ten_zeros: [i64; 10] = [0; 10];
// tuples
let tuple: (i32, &str) = (50, "hello");
let (fifty, _) = tuple;
let hello = tuple.1;
// raw pointers
let x = 5;
let raw = &x as *const i32;
let points_at = unsafe { *raw };
// functions
fn foo(x: i32) -> i32 { x }
let bar: fn(i32) -> i32 = foo;
有几点是需要特别注意的:
_
分隔符来增加可读性。b'H'
以及单字节字符串b"Hello"
,仅限制于ASCII字符。 此外,还可以使用r#"..."#
标记来表示原始字符串,不需要对特殊字符进行转义。&
符号将String
类型转换成&str
类型很廉价, 但是使用to_string()
方法将&str
转换到String
类型涉及到分配内存, 除非很有必要否则不要这么做。Vec (vector)
,可以使用宏vec!
创建。==
和!=
运算符来判断是否相同。as
关键字显式转换。type
关键字定义某个类型的别名,并且应该采用驼峰命名法。// explicit conversion
let decimal = 65.4321_f32;
let integer = decimal as u8;
let character = integer as char;
// type aliases
type NanoSecond = u64;
type Point = (u8, u8);
数组 array
Rust 使用数组存储相同类型的数据集。 [T; N]表示一个拥有 T 类型,N 个元素的数组。数组的大小是固定。
fn main() {
let mut array: [i32; 3] = [0; 3];
array[1] = 1;
array[2] = 2;
assert_eq!([1, 2], &array[1..]);
// This loop prints: 0 1 2
for x in &array {
println!("{} ", x);
}
}
动态数组 Vec
动态数组是一种基于堆内存申请的连续动态数据类型,拥有 O(1) 时间复杂度的索引、压入(push)、弹出(pop)。
//创建空Vec
let v: Vec<i32> = Vec::new();
//使用宏创建空Vec
let v: Vec<i32> = vec![];
//创建包含5个元素的Vec
let v = vec![1, 2, 3, 4, 5];
//创建十个零
let v = vec![0; 10];
//创建可变的Vec,并压入元素3
let mut v = vec![1, 2];
v.push(3);
//创建拥有两个元素的Vec,并弹出一个元素
let mut v = vec![1, 2];
let two = v.pop();
//创建包含三个元素的可变Vec,并索引一个值和修改一个值
let mut v = vec![1, 2, 3];
let three = v[2];
v[1] = v[1] + 5;
字符串
Rust 里面有两种字符串类型。String
和str
。
&str
, str 类型基本上不怎么使用,通常使用 &str
类型,它其实是[u8]
类型的切片形式 &[u8]
。这是一种固定大小的字符串类型。 常见的的字符串字面值就是 &'static str
类型。这是一种带有 'static
生命周期的&str
类型。
// 字符串字面值
let hello = "Hello, world!";
// 附带显式类型标识
let hello: &'static str = "Hello, world!";
String 是一个带有的 vec:Vec 成员的结构体,你可以理解为 str 类型的动态形式。 它们的关系相当于 [T] 和 Vec 的关系。 显然 String 类型也有压入和弹出。
// 创建一个空的字符串
let mut s = String::new();
// 从 `&str` 类型转化成 `String` 类型
let mut hello = String::from("Hello, ");
// 压入字符和压入字符串切片
hello.push('w');
hello.push_str("orld!");
// 弹出字符。
let mut s = String::from("foo");
assert_eq!(s.pop(), Some('o'));
assert_eq!(s.pop(), Some('o'));
assert_eq!(s.pop(), Some('f'));
assert_eq!(s.pop(), None);
// structs
struct Point {
x: i32,
y: i32,
}
let point = Point { x: 0, y: 0 };
// tuple structs
struct Color(u8, u8, u8);
let android_green = Color(0xa4, 0xc6, 0x39);
let Color(red, green, _) = android_green; // 这个对一般的语言差异比较大
// A tuple struct’s constructors can be used as functions.
struct Digit(i32);
let v = vec![0, 1, 2];
let d: Vec<Digit> = v.into_iter().map(Digit).collect();
// newtype: a tuple struct with only one element
struct Inches(i32);
let length = Inches(10);
let Inches(integer_length) = length;
// unit-like structs
struct EmptyStruct;
let empty = EmptyStruct;
一个包含..
的struct可以用来从其它结构体拷贝一些值或者在解构时忽略一些域:
struct Point3d {
x: i32,
y: i32,
z: i32,
}
let origin = Point3d::default();
let point = Point3d { y: 1, ..origin };
let Point3d { x: x0, y: y0, .. } = point;
需要注意,Rust在语言级别不支持域可变性 (field mutability),所以不能这么写:
struct Point {
mut x: i32,
y: i32,
}
这是因为可变性是绑定的一个属性,而不是结构体自身的。可以使用Cell来模拟:
use std::cell::Cell;
struct Point {
x: i32,
y: Cell<i32>,
}
let point = Point { x: 5, y: Cell::new(6) };
point.y.set(7);
此外,结构体的域默认是私有的,可以使用pub关键字将其设置成公开。
Rust有一个集合类型,称为枚举 (enum),代表一系列子数据类型的集合。 其中子数据结构可以为空-如果全部子数据结构都是空的,就等价于C语言中的enum。 我们需要使用::来获得每个元素的名称。
// enums
enum Message {
Quit,
ChangeColor(i32, i32, i32),
Move { x: i32, y: i32 },
Write(String),
}
let x: Message = Message::Move { x: 3, y: 4 };
与结构体一样,枚举中的元素默认不能使用关系运算符进行比较 (如==, !=, >=), 也不支持像+和*这样的双目运算符,需要自己实现,或者使用match进行匹配。
枚举默认也是私有的,如果使用pub使其变为公有,则它的元素也都是默认公有的。 这一点是与结构体不同的:即使结构体是公有的,它的域仍然是默认私有的。 此外,枚举和结构体也可以是递归的 (recursive)。
Rust有if for while match
四种流逻辑控制,但是与传统的有很大区别:
If是分支 (branch) 的一种特殊形式,也可以使用else和else if。 与C语言不同的是,逻辑条件不需要用小括号括起来,但是条件后面必须跟一个代码块。 Rust中的if是一个表达式 (expression),可以赋给一个变量:
let x = 5;
let y = if x == 5 { 10 } else { 15 };
Rust是基于表达式的编程语言,有且仅有两种语句 (statement):
表达式如果返回,总是返回一个值,但是语句不返回值或者返回(),所以以下代码会报错:
let y = (let x = 5);
let z: i32 = if x == 5 { 10; } else { 15; };
值得注意的是,在Rust中赋值 (如x = 5
) 也是一个表达式,返回unit的值()
。
Rust中的for循环与C语言的风格非常不同,抽象结构如下:
for var in expression {
code
}
其中expression是一个迭代器 (iterator),具体的例子为0..10
(不包含最后一个值), 或者[0, 1, 2].iter()
。
for (i,j) in (5..10).enumerate() {
println!("i = {} and j = {}", i, j);
}
Rust中的while循环与C语言中的类似。对于无限循环,Rust有一个专用的关键字loop。 如果需要提前退出循环,可以使用关键字break或者continue, 还允许在循环的开头设定标签 (同样适用于for循环):
loop
与 while true
的主要区别在编译阶段的静态分析。
// while
let mut x = 5; // mut x: i32
let mut done = false; // mut done: bool
while !done {
x += x - 3;
println!("{}", x);
if x % 5 == 0 {
done = true;
}
}
// loop & label
'outer: loop {
println!("Entered the outer loop");
'inner: loop {
println!("Entered the inner loop");
break 'outer; // 跳出`outer标签所在的循环
}
println!("This point will never be reached");
}
println!("Exited the outer loop");
Rust中的match表达式非常强大,首先看一个例子:
let day = 5;
match day {
0 | 6 => println!("weekend"),
1 ... 5 => println!("weekday"),
_ => println!("invalid"),
}
其中|
用于匹配多个值,...
匹配一个范围 (包含最后一个值),并且_
在这里是必须的, 因为match强制进行穷尽性检查 (exhaustiveness checking),必须覆盖所有的可能值。 如果需要得到|
或者...
匹配到的值,可以使用@绑定变量:
let x = 1;
match x {
e @ 1 ... 5 => println!("got a range element {}", e),
_ => println!("anything"),
}
使用ref关键字来得到一个引用:
let x = 5;
let mut y = 5;
match x {
// the `r` inside the match has the type `&i32`
ref r => println!("Got a reference to {}", r),
}
match y {
// the `mr` inside the match has the type `&i32` and is mutable
ref mut mr => println!("Got a mutable reference to {}", mr),
}
再看一个使用match表达式来解构元组的例子:
let pair = (0, -2);
match pair {
(0, y) => println!("x is `0` and `y` is `{:?}`", y),
(x, 0) => println!("`x` is `{:?}` and y is `0`", x),
_ => println!("It doesn't matter what they are"),
}
match的这种解构同样适用于结构体或者枚举。如果有必要,还可以使用..
来忽略域或者数据:
struct Point {
x: i32,
y: i32,
}
let origin = Point { x: 0, y: 0 };
match origin {
Point { x, .. } => println!("x is {}", x),
}
enum OptionalInt {
Value(i32),
Missing,
}
let x = OptionalInt::Value(5);
match x {
// 这里是 match 的 if guard 表达式,我们将在以后的章节进行详细介绍
OptionalInt::Value(i) if i > 5 => println!("Got an int bigger than five!"),
OptionalInt::Value(..) => println!("Got an int!"),
OptionalInt::Missing => println!("No such luck."),
}
此外,Rust还引入了if let
和while let
进行模式匹配:
let number = Some(7);
let mut optional = Some(0);
// If `let` destructures `number` into `Some(i)`, evaluate the block.
if let Some(i) = number {
println!("Matched {:?}!", i);
} else {
println!("Didn't match a number!");
}
// While `let` destructures `optional` into `Some(i)`, evaluate the block.
while let Some(i) = optional {
if i > 9 {
println!("Greater than 9, quit!");
optional = None;
} else {
println!("`i` is `{:?}`. Try again.", i);
optional = Some(i + 1);
}
}
在rust中,任何函数都有返回类型,当函数返回时,会返回一个该类型的值。
当一个函数返回()
时,可以省略。
rust也有return关键字,不过一般用于提前返回。
rust的函数不支持多返回值,但是我们可以利用元组来返回多个值
发散函数(diverging function)是rust中的一个特性。发散函数不返回,它使用感叹号!作为返回类型,一般用来处理错误,终止
函数
要声明一个函数,需要使用关键字fn,后面跟上函数名,比如
fn add_one(x: i32) -> i32 {
x + 1
}
其中函数参数的类型不能省略,可以有多个参数,但是最多只能返回一个值, 提前返回使用return关键字。Rust编译器会对未使用的函数提出警告, 可以使用属性#[allow(dead_code)]禁用无效代码检查。
Rust有一个特殊特性适用于发散函数 (diverging function),它不返回:
fn diverges() -> ! {
panic!("This function never returns!");
}
其中panic!是一个宏,使当前执行线程崩溃并打印给定信息。返回类型!可用作任何类型:
let x: i32 = diverges();
let y: String = diverges();
匿名函数
Rust使用闭包 (closure) 来创建匿名函数:
let num = 5;
let plus_num = |x: i32| x + num;
其中闭包plus_num借用了它作用域中的let绑定num。如果要让闭包获得所有权, 可以使用move关键字:
let mut num = 5;
{
let mut add_num = move |x: i32| num += x; // 闭包通过move获取了num的所有权
add_num(5);
}
// 下面的num在被move之后还能继续使用是因为其实现了Copy特性
// 具体可见所有权(Owership)章节
assert_eq!(5, num);
高阶函数
Rust 还支持高阶函数 (high order function),允许把闭包作为参数来生成新的函数:
fn add_one(x: i32) -> i32 { x + 1 }
fn apply<F>(f: F, y: i32) -> i32
where F: Fn(i32) -> i32
{
f(y) * y
}
fn factory(x: i32) -> Box<Fn(i32) -> i32> {
Box::new(move |y| x + y)
}
fn main() {
let transform: fn(i32) -> i32 = add_one;
let f0 = add_one(2i32) * 2;
let f1 = apply(add_one, 2);
let f2 = apply(transform, 2);
println!("{}, {}, {}", f0, f1, f2);
let closure = |x: i32| x + 1;
let c0 = closure(2i32) * 2;
let c1 = apply(closure, 2);
let c2 = apply(|x| x + 1, 2);
println!("{}, {}, {}", c0, c1, c2);
let box_fn = factory(1i32);
let b0 = box_fn(2i32) * 2;
let b1 = (*box_fn)(2i32) * 2;
let b2 = (&box_fn)(2i32) * 2;
println!("{}, {}, {}", b0, b1, b2);
let add_num = &(*box_fn);
let translate: &Fn(i32) -> i32 = add_num;
let z0 = add_num(2i32) * 2;
let z1 = apply(add_num, 2);
let z2 = apply(translate, 2);
println!("{}, {}, {}", z0, z1, z2);
}
方法
Rust通过impl关键字在struct、enum或者trait对象上实现方法调用语法 (method call syntax)。 关联函数 (associated function) 的第一个参数通常为self参数,有3种变体:
不含self参数的关联函数称为静态方法 (static method)。
struct Circle {
x: f64,
y: f64,
radius: f64,
}
impl Circle {
fn new(x: f64, y: f64, radius: f64) -> Circle {
Circle {
x: x,
y: y,
radius: radius,
}
}
fn area(&self) -> f64 {
std::f64::consts::PI * (self.radius * self.radius)
}
}
fn main() {
let c = Circle { x: 0.0, y: 0.0, radius: 2.0 };
println!("{}", c.area());
// use associated function and method chaining
println!("{}", Circle::new(0.0, 0.0, 2.0).area());
}
为了描述类型可以实现的抽象接口 (abstract interface), Rust引入了特性 (trait) 来定义函数类型签名 (function type signature):
trait HasArea {
fn area(&self) -> f64;
}
struct Circle {
x: f64,
y: f64,
radius: f64,
}
impl HasArea for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * (self.radius * self.radius)
}
}
struct Square {
x: f64,
y: f64,
side: f64,
}
impl HasArea for Square {
fn area(&self) -> f64 {
self.side * self.side
}
}
fn print_area<T: HasArea>(shape: T) {
println!("This shape has an area of {}", shape.area());
}
其中函数print_area()
中的泛型参数T被添加了一个名为HasArea
的特性约束 (trait constraint), 用以确保任何实现了HasArea
的类型将拥有一个.area()
方法。 如果需要多个特性限定 (multiple trait bounds),可以使用+
:
use std::fmt::Debug;
fn foo<T: Clone, K: Clone + Debug>(x: T, y: K) {
x.clone();
y.clone();
println!("{:?}", y);
}
fn bar<T, K>(x: T, y: K)
where T: Clone,
K: Clone + Debug
{
x.clone();
y.clone();
println!("{:?}", y);
}
其中第二个例子使用了更灵活的where
从句,它还允许限定的左侧可以是任意类型, 而不仅仅是类型参数。
定义在特性中的方法称为默认方法 (default method),可以被该特性的实现覆盖。 此外,特性之间也可以存在继承 (inheritance):
trait Foo {
fn foo(&self);
// default method
fn bar(&self) { println!("We called bar."); }
}
// inheritance
trait FooBar : Foo {
fn foobar(&self);
}
struct Baz;
impl Foo for Baz {
fn foo(&self) { println!("foo"); }
}
impl FooBar for Baz {
fn foobar(&self) { println!("foobar"); }
}
如果两个不同特性的方法具有相同的名称,可以使用通用函数调用语法 (universal function call syntax):
// short-hand form
Trait::method(args);
// expanded form
<Type as Trait>::method(args);
关于实现特性的几条限制:
下面列举几个非常有用的标准库特性:
drop(&mut self)
方法。Deref<Target=T>
用于把&U
类型的值自动转换为&T类型。&[T]
和Box<Trait>
。泛型和多态
泛型 (generics) 在类型理论中称作参数多态 (parametric polymorphism), 意为对于给定参数可以有多种形式的函数或类型。先看Rust中的一个泛型例子:
Option在rust标准库中的定义:
enum Option<T> {
Some(T),
None,
}
Option的典型用法:
let x: Option<i32> = Some(5);
let y: Option<f64> = Some(5.0f64);
其中<T>
部分表明它是一个泛型数据类型。当然,泛型参数也可以用于函数参数和结构体域:
// generic functions
fn make_pair<T, U>(a: T, b: U) -> (T, U) {
(a, b)
}
let couple = make_pair("man", "female");
// generic structs
struct Point<T> {
x: T,
y: T,
}
let int_origin = Point { x: 0, y: 0 };
let float_origin = Point { x: 0.0, y: 0.0 };
对于多态函数,存在两种派分 (dispatch) 机制:静态派分和动态派分。
前者类似于C++的模板,Rust会生成适用于指定类型的特殊函数,然后在被调用的位置进行替换, 好处是允许函数被内联调用,运行比较快,但是会导致代码膨胀 (code bloat);
后者类似于Java或Go的interface,Rust通过引入特性对象 (trait object) 来实现, 在运行期查找虚表 (vtable) 来选择执行的方法。
特性对象&Foo
具有和特性Foo相同的名称, 通过转换 (casting) 或者强制多态化 (coercing) 一个指向具体类型的指针来创建。
当然,特性也可以接受泛型参数。但是,往往更好的处理方式是使用关联类型 (associated type):
// use generic parameters
trait Graph<N, E> {
fn has_edge(&self, &N, &N) -> bool;
fn edges(&self, &N) -> Vec<E>;
}
fn distance<N, E, G: Graph<N, E>>(graph: &G, start: &N, end: &N) -> u32 {
}
// use associated types
trait Graph {
type N;
type E;
fn has_edge(&self, &Self::N, &Self::N) -> bool;
fn edges(&self, &Self::N) -> Vec<Self::E>;
}
fn distance<G: Graph>(graph: &G, start: &G::N, end: &G::N) -> uint {
}
struct Node;
struct Edge;
struct SimpleGraph;
impl Graph for SimpleGraph {
type N = Node;
type E = Edge;
fn has_edge(&self, n1: &Node, n2: &Node) -> bool {
}
fn edges(&self, n: &Node) -> Vec<Edge> {
}
}
let graph = SimpleGraph;
let object = Box::new(graph) as Box<Graph<N=Node, E=Edge>>;
注释:
// 单行注释
/*
* 多行注释,支持,但是不推荐
*/
文档:
Rust 自带有文档功能的注释,分别是///
和//!
。支持 Markdown 格式。
///
用来描述的它后面接着的项。用于模块级注释。 //!
用来描述包含它的项,一般用在模块文件的头部。用于文档级注释。 最后,通过rustdoc xxx.rs
或者cargo doc
生成html文档。
//! # The first line
//! The second line
/// Adds one to the number given.
///
/// # Examples
///
/// ```
/// let five = 5;
///
/// assert_eq!(6, add_one(5));
/// # fn add_one(x: i32) -> i32 {
/// # x + 1
/// # }
/// ```
fn add_one(x: i32) -> i32 {
x + 1
}
标准输入
标准输入也叫作控制台输入,是常见输入的一种。
// 例子1
use std::io;
fn read_input() -> io::Result<()> {
let mut input = String::new();
try!(io::stdin().read_line(&mut input));
println!("You typed: {}", input.trim());
Ok(())
}
fn main() {
read_input();
}
// 例子2:
use std::io;
fn main() {
let mut input = String::new();
io::stdin().read_line(&mut input).expect("WTF!");
println!("You typed: {}", input.trim());
}
这里体现了常见的标准输入的处理方式。两个例子都是声明了一个可变的字符串来保存输入的数据。 他们的不同之处在在于处理潜在输入异常的方式。
try!
宏。这个宏会返回 Result<(), io::Error>
类型,io::Result<()>
就是这个类型的别名。所以例子1 需要单独使用一个 read_input
函数来接收这个类型,而不是在 main
函数里面,因为 main
函数并没有接收 io::Result<()>
作为返回类型。Result<(), io::Error>
类型的 expect
方法来接收 io::stdin().read_line
的返回类型。并处理可能潜在的 io
异常。标准输出
标准输出也叫控制台输出,Rust 里面常见的标准输出宏有 print! 和 println!。它们的区别是后者比前者在末尾多输出一个换行符。
// 例子1:
fn main() {
print!("this ");
print!("will ");
print!("be ");
print!("on ");
print!("the ");
print!("same ");
print!("line ");
print!("this string has a newline, why not choose println! instead?\n");
}
//例子2:
fn main() {
println!("hello there!");
println!("format {} arguments", "some");
}
这里两个例子都比较简单。读者可以运行一下查看输出结果对比一下他们的区别。 值得注意的是例子 2 中,{ }
会被 "some"
所替换。这是 rust 里面的一种格式化输出。
标准化的输出是行缓冲(line-buffered)的,这就导致标准化的输出在遇到一个新行之前并不会被隐式刷新。 换句话说 print!
和 println!
二者的效果并不总是相同的。 如果说得更简单明了一点就是,您不能把 print!
当做是C语言中的 printf 譬如:
use std::io;
fn main() {
print!("请输入一个字符串:");
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.expect("读取失败");
print!("您输入的字符串是:{}\n", input);
}
在这段代码运行时则不会先出现预期的提示字符串,因为行没有被刷新。 如果想要达到预期的效果就要显示的刷新:
use std::io::{self, Write};
fn main() {
print!("请输入一个字符串:");
io::stdout().flush().unwrap();
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.expect("读取失败");
print!("您输入的字符串是:{}\n", input);
}
文件输入
文件输入和标准输入都差不多,除了输入流指向了文件而不是控制台。下面例子采用了模式匹配来处理潜在的输入错误
use std::error::Error;
use std::fs::File;
use std::io::prelude::*;
use std::path::Path;
fn main() {
// 创建一个文件路径
let path = Path::new("hello.txt");
let display = path.display();
// 打开文件只读模式, 返回一个 `io::Result<File>` 类型
let mut file = match File::open(&path) {
// 处理打开文件可能潜在的错误
Err(why) => panic!("couldn't open {}: {}", display,
Error::description(&why)),
Ok(file) => file,
};
// 文件输入数据到字符串,并返回 `io::Result<usize>` 类型
let mut s = String::new();
match file.read_to_string(&mut s) {
Err(why) => panic!("couldn't read {}: {}", display,
Error::description(&why)),
Ok(_) => print!("{} contains:\n{}", display, s),
}
}
文件输出
文件输出和标准库输出也差不多,只不过是把输出流重定向到文件中。下面详细看例子。
// 输出文本
static LOREM_IPSUM: &'static str =
"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
";
use std::error::Error;
use std::io::prelude::*;
use std::fs::File;
use std::path::Path;
fn main() {
let path = Path::new("out/lorem_ipsum.txt");
let display = path.display();
// 用只写模式打开一个文件,并返回 `io::Result<File>` 类型
let mut file = match File::create(&path) {
Err(why) => panic!("couldn't create {}: {}",
display,
Error::description(&why)),
Ok(file) => file,
};
// 写入 `LOREM_IPSUM` 字符串到文件中, 并返回 `io::Result<()>` 类型
match file.write_all(LOREM_IPSUM.as_bytes()) {
Err(why) => {
panic!("couldn't write to {}: {}", display,
Error::description(&why))
},
Ok(_) => println!("successfully wrote to {}", display),
}
}
Rust的操作符号和C++大部分都是一模一样的。
一元操作符
顾名思义,一元操作符是专门对一个Rust元素进行操纵的操作符,主要包括以下几个:
-
: 取负,专门用于数值类型。*
: 解引用。这是一个很有用的符号,和Deref(DerefMut)这个trait关联密切。!
: 取反。取反操作相信大家都比较熟悉了,不多说了。有意思的是,当这个操作符对数字类型使用的时候,会将其每一位都置反!也就是说,你对一个1u8进行!的话你将会得到一个254u8。&
和&mut
: 租借,borrow。向一个owner租借其使用权,分别是租借一个只读使用权和读写使用权。二元操作符
算数运算符都有对应的trait
的,他们都在std::ops
下:
+
: 加法。实现了std::ops::Add
。-
: 减法。实现了std::ops::Sub
。*
: 乘法。实现了std::ops::Mul
。/
: 除法。实现了std::ops::Div
。%
: 取余。实现了std::ops::Rem
。和算数运算符差不多的是,位运算也有对应的trait。
&
: 与操作。实现了std::ops::BitAnd
。|
: 或操作。实现了std::ops::BitOr
。^
: 异或。实现了std::ops::BitXor
。<<
: 左移运算符。实现了std::ops::Shl
。>>
: 右移运算符。实现了std::ops::Shr
。逻辑运算符有三个,分别是&&、||、!
。其中前两个叫做惰性boolean运算符,之所以叫这个名字,是因为在Rust里也会出现其他类C语言的逻辑短路问题。所以取了这么一个高大上然并卵的名字。
其作用和C语言里的一毛一样啊!哦,对了,有点不同的是Rust里这个运算符只能用在bool类型变量上。什么 1 && 1
之类的表达式给我死开。
比较运算符
比较运算符其实也是某些trait的语法糖啦,不同的是比较运算符所实现的trait只有两个std::cmp::PartialEq
和std::cmp::PartialOrd
其中, ==
和!=
实现的是PartialEq
。 而,<、>、>=、<=
实现的是PartialOrd
。
类型转换运算符
其实这个并不算运算符,因为他是个单词as。
这个就是C语言中各位熟悉的显式类型转换了。
fn avg(vals: &[f64]) -> f64 {
let sum: f64 = sum(vals);
let num: f64 = len(vals) as f64;
sum / num
}
说起格式化字符串,Rust采取了一种类似Python里面format的用法,其核心组成是五个宏和两个trait:format!、format_arg!、print!、println!、write!;Debug、Display
。
相信你们在写Rust版本的Hello World的时候用到了print!
或者println!
这两个宏,但是其实最核心的是format!
,前两个宏只不过将format!
的结果输出到了console而已。
那么,我们来探究一下format!这个神奇的宏吧。
在这里呢,列举format!
的定义是没卵用的,因为太复杂。我只为大家介绍几种典型用法。学会了基本上就能覆盖你平时80%的需求。
首先我们来分析一下format的一个典型调用:
fn main() {
let s = format!("{1}是个有着{0:>0width$}KG重,{height:?}cm高的大胖子",
81, "wayslog", width=4, height=178);
// 我被逼的牺牲了自己了……
print!("{}", s);
}
我们可以看到,format!
宏调用的时候参数可以是任意类型,而且是可以position
参数和key-value
参数混合使用的。但是要注意的一点是,key-value
的值只能出现在position
值之后并且不占position
。例如例子里你用3$
引用到的绝对不是width,而是会报错。
这里面关于参数稍微有一个规则就是,参数类型必须要实现 std::fmt mod
下的某些trait。比如我们看到原生类型大部分都实现了Display
和Debug
这两个宏,其中整数类型还会额外实现一个Binary,等等。
当然了,我们可以通过{:type}
的方式去调用这些参数。
format!("{:b}", 2);
// 调用 `Binary` trait
// Get : 10
format!("{:?}", "Hello");
// 调用 `Debug`
// Get : "Hello"
另外请记住:type
这个地方为空的话默认调用的是Display
这个trait。关于:
号后面的东西其实还有更多式子,我们从上面的{0:>0width$}
来分析它。
首先>
是一个语义,它表示的是生成的字符串向右对齐,于是我们得到了0081
这个值。与之相对的还有<
(向左对齐)和^
(居中)。
再接下来0
是一种特殊的填充语法,他表示用0补齐数字的空位,要注意的是,当0
作用于负数的时候,比如上面例子中wayslog
的体重是-81
,那么你最终将得到-0081
;当然了,什么都不写表示用空格填充啦; 在这一位上,还会出现+、#
的语法,使用比较诡异,一般情况下用不上。
最后是一个组合式子width$
,这里呢,大家很快就能认出来是表示后面key-value
值对中的width=4
。你们没猜错,这个值表示格式化完成后字符串的长度。它可以是一个精确的长度数值,也可以是一个以$
为结尾的字符串,$
前面的部分可以写一个key
或者一个postion
。
最后,你需要额外记住的是,在width
和type
之间会有一个叫精度的区域(可以省略不写如例子),他们的表示通常是以.
开始的,比如.4
表示小数点后四位精度。最让人遭心的是,你仍然可以在这个位置引用参数,只需要和上面width
一样,用.N$
来表示一个position
的参数,但是就是不能引用key-value
类型的。这一位有一个特殊用法,那就是.*
,它不表示一个值,而是表示两个值!第一个值表示精确的位数,第二个值表示这个值本身。
这是一种很尴尬的用法,而且极度容易匹配到其他参数。因此,我建议在各位能力或者时间不欠缺的时候尽量把格式化表达式用标准的形式写的清楚明白。尤其在面对一个复杂的格式化字符串的时候。
好了好了,说了这么多,估计你也头昏脑涨的了吧,下面来跟我写一下format
宏的完整用法。仔细体会并提炼每一个词的意思和位置。
// 简直眼花缭乱
format_string := <text> [ format <text> ] *
format := '{' [ argument ] [ ':' format_spec ] '}'
argument := integer | identifier
format_spec := [[fill]align][sign]['#'][0][width]['.' precision][type]
fill := character
align := '<' | '^' | '>'
sign := '+' | '-'
width := count
precision := count | '*'
type := identifier | ''
count := parameter | integer
parameter := integer '$'
对上述cargo默认的项目结构解释如下:
- cargo.toml
和cargo.lock
文件总是位于项目根目录下。
- 源代码位于src
目录下。
- 默认的库入口文件是src/lib.rs
。
- 默认的可执行程序入口文件是src/main.rs
。
- 其他可选的可执行文件位于src/bin/*.rs
(这里每一个rs文件均对应一个可执行文件)。
- 外部测试源代码文件位于tests
目录下。
- 示例程序源代码文件位于examples
。
- 基准测试源代码文件位于benches
目录下。
好了,大家一定谨记这些默认规则,最好按照这种模式来组织自己的rust项目。
cargo.toml
是cargo特有的项目数据描述文件,对于猿们而言,cargo.toml
文件存储了项目的所有信息,它直接面向rust猿,猿们如果想让自己的rust项目能够按照期望的方式进行构建、测试和运行,那么,必须按照合理的方式构建'cargo.toml'
。
而cargo.lock
文件则不直接面向猿,猿们也不需要直接去修改这个文件。lock文件是cargo工具根据同一项目的toml文件生成的项目依赖详细清单文件,所以我们一般不用不管他。
[package]
name = "rsPrj"
version = "0.1.0"
authors = ["xin.zhang <xin.zhang@xxxx.com>"]
[dependencies]
package段落
[package]
段落描述了软件开发者对本项目的各种元数据描述信息,例如[name]
字段定义了项目的名称,[version]
字段定义了项目的当前版本,[authors]
定义了该项目的所有作者,当然,[package]
段落不仅仅包含这些字段,[package]
段落的其他可选字段详见cargo参数配置章节。
定义项目依赖
使用cargo工具的最大优势就在于,能够对该项目的各种依赖项进行方便、统一和灵活的管理。这也是使用cargo对rust 的项目进行管理的重要目标之一。在cargo的toml文件描述中,主要通过各种依赖段落来描述该项目的各种依赖项。toml中常用的依赖段落包括一下几种:
这三种形式具体写法如下:
[dependencies]
typemap = "0.3"
plugin = "0.2*"
hammer = { version = "0.5.0"}
color = { git = "https://github.com/bjz/color-rs" }
geometry = { path = "crates/geometry" }
上述例子中,2-4行为方法一的写法,第5行为方法二的写法,第6行为方法三的写法。这三种写法各有用处:
1. 如果项目需要使用crates.io官方仓库来管理项目依赖项,推荐使用第一种方法。
2. 如果项目开发者更倾向于使用git仓库中最新的源码,可以使用方法二。方法二也经常用于当官方仓库的依赖项编译不通过时的备选方案。
3. 方法三主要用于源代码位于本地的依赖项。
定义集成测试用例
cargo另一个重要的功能,即将软件开发过程中必要且非常重要的测试环节进行集成,并通过代码属性声明或者toml文件描述来对测试进行管理。
其中,单元测试主要通过在项目代码的测试代码部分前用#[test]
属性来描述,而集成测试,则一般都会通过toml文件中的[[test]]
段落进行描述。
例如,假设集成测试文件均位于tests
文件夹下,则toml
可以这样来写:
[[test]]
name = "testinit"
path = "tests/testinit.rs"
[[test]]
name = "testtime"
path = "tests/testtime.rs"
上述例子中,name字段定义了集成测试的名称,path字段定义了集成测试文件相对于本toml文件的路径。 看看,定义集成测试就是如此简单。 需要注意的是:
定义项目示例和可执行程序
上面我们介绍了cargo项目管理中常用的三个功能,还有两个经常使用的功能:example用例的描述以及bin用例的描述。其描述方法和test用例描述方法类似。不过,这时候段落名称’[[test]]’分别替换为:’[[example]]’或者’[[bin]]’。例如:
[[example]]
name = "timeout"
path = "examples/timeout.rs"
[[bin]]
name = "bin1"
path = "bin/bin1.rs"
对于'[[example]]'
和'[[bin]]'
段落中声明的examples和bins,需要通过'cargo run --example NAME'
或者'cargo run --bin NAME'
来运行,其中NAME对应于你在name字段中定义的名称。
rust是一个基于表达式的语言,不过它也有语句。rust只有两种语句:声明语句和表达式语句,其他的都是表达式。基于表达式是函数式语言的一个重要特征,表达式总是返回值。
rust的声明语句可以分为两种,一种为变量声明语句,另一种为Item声明语句。
let a = 8;
let b: Vec<f64> = Vec::new();
let (a, c) = ("hi", false);
let b = (let a = 8); // 错误,由于let是语句,所以不能将let语句赋给其他值
Item声明
指函数(function)、结构体(structure)、类型别名(type)、静态变量(static)、特质(trait)、实现(implementation)或模块(module)的声明。这些声明可以嵌套在任意块(block)中。
表达式语句,由一个表达式和一个分号组成,即在表达式后面加一个分号就将一个表达式转变为了一个语句。所以,有多少种表达式,就有多少种表达式语句。
// 字面表达式(literal expression)
(); // unit type
"hello"; // string type
'1'; // character type
15; // integer type
// 元组表达式(Tuple expression):
(0.0, 4.5);
("a", 4usize, true);
//通常不使用一个元素的元组,不过如果你坚持的话,rust也是允许的,不过需要在元素后加一个逗号:
(0,); // single-element tuple
(0); // zero in parentheses
// 结构体表达式(structure expression) 由于结构体有多种形式,所以结构体表达式也有多种形式。
Point {x: 10.0, y: 20.0};
TuplePoint(10.0, 20.0);
let u = game::User {name: "Joe", age: 35, score: 100_000};
some_fn::<Cookie>(Cookie);
// 结构体表达式一般用于构造一个结构体对象,它除了以上从零构建的形式外,还可以在另一个对象的基础上进行构建:
let base = Point3d {x: 1, y: 2, z: 3};
Point3d {y: 0, z: 10, .. base};
// 块表达式(block expression): 块表达式就是用花括号{}括起来的一组表达式的集合,表达式间一般以分号分隔。块表达式的值,就是最后一个表达式的值。
let x: i32 = { println!("Hello."); 5 };
// 如果以语句结尾,则块表达式的值为():
let x: () = { println!("Hello."); };
// 范围表达式(range expression): 可以使用范围操作符..来构建范围对象(variant of std::ops::Range):
1..2; // std::ops::Range
3..; // std::ops::RangeFrom
..4; // std::ops::RangeTo
..; // std::ops::RangeFull
// if表达式(if expression):
let a = 9;
let b = if a%2 == 0 {"even"} else {"odd"};
// 除了以上这些外,还有许多,如:
path expression
mehond-call expression
field expression
array expression
index expression
unary operator expression
binary operator expression
return expression
grouped expression
match expression
if expression
lambda expression
... ...
这里无法详细展开,读者可以到Rust Reference去查看。
match所罗列的匹配,必须穷举出其所有可能。当然,你也可以用 _ 这个符号来代表其余的所有可能性情况,就类似于switch中的default语句。
match的每一个分支都必须是一个表达式,并且,除非一个分支一定会触发panic,这些分支的所有表达式的最终返回值类型必须相同。
模式,是Rust另一个强大的特性。它可以被用在let和match表达式里面。相信大家应该还记得我们在复合类型中提到的关于在let表达式中解构元组的例子,实际上这就是一个模式。
let tup = (0u8, 1u8);
let (x, y) = tup;
而且我们需要知道的是,如果一个模式中出现了和当前作用域中已存在的同名的绑定,那么它会覆盖掉外部的绑定。比如:
let x = 1;
let c = 'c';
match c {
x => println!("x: {} c: {}", x, c),
}
println!("x: {}", x);
// 它的输出结果是:
x: c c: c
x: 1
在以上代码中,match作用域里的x
这个绑定被覆盖成了'c'
,而出了这个作用域,绑定x
又恢复为1
。这和变量绑定的行为是一致的。
在上一节里,我们初步了解了模式匹配在解构enum时候的便利性,事实上,在Rust中模式可以被用来对任何复合类型进行解构——struct/tuple/enum。现在我们要讲述一个复杂点的例子,对struct进行解构。
首先,我们可以对一个结构体进行标准的解构:
struct Point {
x: i64,
y: i64,
}
let point = Point { x: 0, y: 0 };
match point {
Point { x, y } => println!("({},{})", x, y),
}
最终,我们拿到了Point内部的值。有人说了,那我想改个名字怎么办? 很简单,你可以使用 :来对一个struct的字段进行重命名,如下:
struct Point {
x: i64,
y: i64,
}
let point = Point { x: 0, y: 0 };
match point {
Point { x: x1, y: y1} => println!("({},{})", x1, y1),
}
另外,有的时候我们其实只对某些字段感兴趣,就可以用..来省略其他字段。
struct Point {
x: i64,
y: i64,
}
let point = Point { x: 0, y: 0 };
match point {
Point { y, .. } => println!("y is {}", y),
}
总结一下,我们遇到了两种不同的模式忽略的情况——_
和..
。这里要注意,模式匹配中被忽略的字段是不会被move
的,而且实现Copy
的也会优先被Copy
而不是被move
。
说的有点拗口,上代码:
let tuple: (u32, String) = (5, String::from("five"));
let (x, s) = tuple;
// 以下行将导致编译错误,因为String类型并未实现Copy, 所以tuple被整体move掉了。
// println!("Tuple is: {:?}", tuple);
let tuple = (5, String::from("five"));
// 忽略String类型,而u32实现了Copy,则tuple不会被move
let (x, _) = tuple;
println!("Tuple is: {:?}", tuple);
模式匹配可以被用来匹配单种可能,当然也就能被用来匹配多种情况:
在模式匹配中,当我想要匹配一个数字(字符)范围的时候,我们可以用...
来表示:
let x = 1;
match x {
1 ... 10 => println!("一到十"),
_ => println!("其它"),
}
let c = 'w';
match c {
'a' ... 'z' => println!("小写字母"),
'A' ... 'Z' => println!("大写字母"),
_ => println!("其他字符"),
}
当我们只是单纯的想要匹配多种情况的时候,可以使用 | 来分隔多个匹配条件
let x = 1;
match x {
1 | 2 => println!("一或二"),
_ => println!("其他"),
}
前面我们了解到,当被模式匹配命中的时候,未实现Copy的类型会被默认的move掉,因此,原owner就不再持有其所有权。但是有些时候,我们只想要从中拿到一个变量的(可变)引用,而不想将其move出作用域,怎么做呢?答:用ref或者ref mut。
let mut x = 5;
match x {
ref mut mr => println!("mut ref :{}", mr),
}
// 当然了……在let表达式里也能用
let ref mut mrx = x;
在模式匹配的过程内部,我们可以用@来绑定一个变量名,这在复杂的模式匹配中是再方便不过的,比如一个具名的范围匹配如下:
let x = 1u32;
match x {
e @ 1 ... 5 | e @ 10 ... 15 => println!("get:{}", e),
_ => (),
}
如代码所示,e
绑定了x
的值。
当然,变量绑定是一个极其有用的语法,下面是一个来自官方doc里的例子:
struct Person {
name: Option<String>,
}
let name = "Steve".to_string();
let x: Option<Person> = Some(Person { name: Some(name) });
match x {
Some(Person { name: ref a @ Some(_), .. }) => println!("{:?}", a),
_ => {}
}
// 输出:
Some("Steve")
一个后置的if
表达式可以被放在match
的模式之后,被称为match guards
。例如如下代码:
let x = 4;
let y = false;
match x {
4 | 5 if y => println!("yes"),
_ => println!("no"),
}
猜一下上面代码的输出?
答案是no
。因为guard
是后置条件,是整个匹配的后置条件:所以上面的式子表达的逻辑实际上是:
// 伪代码表示
IF y AND (x IN List[4, 5])
Rust的语法兼顾C++和Golang一类语言的优点,集合了自己的特点,从语法层次来讲,做的非常不错。但是强大的同时带来的是复杂性,作为一个和C++比肩的语言,学习难度还是相当高的。
虽然这里已经了解了很多了,但是还有很多细节没有涉及到,在后续的文章中继续。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。