rust语言基础
Rust是一门系统编程语言,无GC(垃圾回收)且能保证内存安全、并发安全和高性能而著称。
自2008年开始由Graydon Hoare私⼈研发,2009年得到Mozilla赞助,2010年⾸次发
布 0.1.0 版本,⽤于Servo引擎的研发,于2015年5⽉15号发布 1.0版本。2021年2⽉
9号,Rust 基⾦会宣布成⽴。华为、AWS、Google、微软、Mozilla、Facebook等科
技⾏业领军巨头加⼊Rust基⾦会,成为⽩⾦成员,以致⼒于在全球范围内推⼴和发展Rust
语⾔。自发布以来,Rust历经6年半的发展,已稳步上升、趋于成熟。
2.Rust环境搭建及tips
2.1 Cargo是什么
Cargo 是 Rust 的构建系统和包管理器。
Rust 开发者常用 Cargo 来管理 Rust 工程和获取工程所依赖的库。
2.2 Rust接口参考
Rust文档路径
rustup doc --path
浏览器打开本地Rust文档
rustup doc
浏览器打开本地Rust标准库std文档
rustup doc --std
获取系统默认使用的浏览器
xdg-settings get default-web-browser
设置系统默认打开的浏览器
export BROWSER=google-chrome
Rust注释语法
// 行注释:本行跟在//后面的内容都是注释的部分
/* 块注释。*/
三个斜线///开头的行注释是Rust的文档注释:cargo doc命令可以把这样的注释自动提取成文档,输出到target/doc目录下。这对于生成程序接口的参考手册非常方便。
3. 学习rust宏打印Hello rust
Rust 输出文字的方式主要有两种:println!() 和 print!()。println 不是一个函数,而是一个宏规则。这两个"函数"都是向命令行输出字符串的方法,区别仅在于前者会在输出的最后附加输出一个换行符。当用这两个"函数"输出信息的时候,第一个参数是格式字符串,后面是一串可变参数,对应着格式字符串中的"占位符",这一点与 C 语言中的 printf 函数很相似。但是,Rust 中格式字符串中的占位符不是 "% + 字母" 的形式,而是一对 {}。
编写helloRust.rs文件
fn main() { let a = 12; println!("a is {}", a); }
编译及运行
$ rustc helloRust.rs
$ ./helloRust
Rust中的宏,也是在预编译阶段进行处理。宏不仅仅是替换内容和展开,还可以像功能函数一样,接收参数(宏的输入参数是包含在宏内容体内的,通过 match 的方式进行查找或者匹配的)、调用其他的宏。
宏的名称和功能函数名称很像,只不过在函数名称后面有一个叹号!
宏的内容体除了用 {}大括号之外,还可以使用 (); 和 [];
使用大括号的时候,大括号后面没有分号;
而使用()和[]的时候,后面有一个分号。
大括号中的内容被称为 macro rule 宏规则。一个宏可以包含任意(0 -n)多个 macro rule 宏规则
宏规则的格式有三种:
(【matcher 匹配器】) => {【transcriber 转换器】};
(【matcher 匹配器】) => [【transcriber 转换器】];
(【matcher 匹配器】) => (【transcriber 转换器】);
举例:
1 macro_rules! print_test { 2 //($msg:expr) => {println!("hello{}", $msg)}; 3 (x => $e:expr) => (println!("mode X:{}", $e)); 4 } 5 fn main() { 6 //print_test!(", test!"); 7 print_test!(x => 7); 8 }
指示符(designator)
ident: 标识符,用来表示函数或变量名
expr: 表达式,前面例子就是这种
block: 代码块,用花括号包起来的多个语句
pat: 模式,普通模式匹配(非宏本身的模式)中的模式,例如 Some(t), (3, ‘a’, _)
path: 路径,注意这里不是操作系统中的文件路径,而是用双冒号分隔的限定名(qualified name),如 std::cmp::PartialOrd
tt: 单个语法树
ty: 类型,语义层面的类型,如 i32, char
item: 条目,
meta: 元条目
stmt: 单条语句,如 let a = 42;
指示符都是以开 头 的 , 这 个 一 定 要 重 视 。 开头的,这个一定要重视。开头的,这个一定要重视。
符后面跟的都是语法元素,这也符合Rust中对宏的定义。$后的指示符表示了各种语法的元素内容
macro_export] 把宏提到crate根部且可供crate外部使用,或者用#[macro_use]在模块间使用
宏调用时对名称的处理和C/C++一样,必须保证在调用之前声明,否则就会报编译错误。但是普通函数则不会有这个问题。
4. Rust 基础杂项概览
1).一般Rust源代码的后缀名是使用.rs表示。源码一定要注意使用utf-8编码。
2).Rust是静态强类型语言,所有的变量都有严格的编译期语法检查。
3).局部变量声明使用let关键字开头;
4).每条语句使用分号结尾;
5).prelude 是 Rust 自动导入每个 Rust 程序的内容的列表;
Rust的代码从逻辑上是分crate和mod管理的。所谓的crate可以理解为“项目”。每个crate是一个完整的编译单元,它可以生成一个lib或者exe可执行文件。而在crate内部,则是由mod这个概念管理的,所谓mod可以理解为namespace。我们可以使用use语句把其他模块中的内容引入到当前模块中来。
6).unsafe
可以通过unsafe关键字来切换到不安全Rust,接着可以开启一个新的存放不安全代码的块。这里有5类可以在不安全Rust中进行而不能用于安全Rust的操作,它们称之为“不安全的超能力”:
解引用裸指针;调用不安全的函数或方法;访问或修改可变静态变量;实现不安全trait;访问 union 的字段。
unsafe并不会关闭借用检查器或禁用任何其它Rust安全检查:如果在不安全代码中使用引用,它仍会被检查。unsafe关键字只是提供了那5个不会被编译器检查内存安全的功能。但仍然能在不安全块中获得某种程序的安全。unsafe不意味着块中的代码就一定是危险的或者必然导致内存安全问题:其意图
在于作为程序员你将会确保unsafe块中的代码以有效的方式访问内存。
标记一个函数为不安全:
unsafe fn danger_will_robinson() { // scary stuff }
使用不安全块:
unsafe { // scary stuff }
7)闭包
很多语言中都有闭包的概念,闭包就是一个能够捕获周围作用域中变量的函数,它们通常以简洁的形式展现,比如lambda表达式。
Rust中lambda的基本格式:
|参数列表| -> 返回值 {
语句1;
语句2;
语句3;
...
表达式 // 最后一个表达式作为返回值
}
在lambda只有一句时,括号可以省略
返回值可以省略,由编译器自动推测
参数类型可以省略,由编译器自动推测
在Rust中,所有变量被所有权体系管控,即使被闭包捕获,它们也必须遵从这套体系,所以,根据捕获变量方式的不同衍生出了三种闭包。
不可变借用(Fn)
let x = String::from("a variable");
let print = || println!("{}", x);
上面的闭包print中引用了周围的变量x,由于它并没有修改x,所以,实际是x的不可变引用被借用给了闭包。这种闭包在Rust中的类型为Fn。你不能在捕获了变量x的Fn类型闭包的最后一次使用之前创建变量x的可变引用或修改x的值,这是所有权系统的限制
可变借用(FnMut)
let mut x = String::from("a variable");
let mut push = || {
x.push_str("123456");
};
上面的闭包push中修改了x,所以,x的可变引用被借用给了闭包,同时,由于闭包每次调用的内部状态也发生了改变,你必须把push也声明成mut。这种闭包的在Rust中的类型为FnMut。你不能在捕获了变量x的Fn类型闭包的最后一次使用之前创建变量x的任何引用或访问x的值,这是所有权系统的限制
获取所有权(FnOnce)
let movable = Box::new(3);
let consume = || {
println!("movable : {:?}", movable);
mem::drop(movable);
};
上面的闭包consume中,由于mem::drop函数需要获取参数的所有权,所以,movable被移动到闭包中,它的所有权也归闭包所有,闭包再把它移动给mem::drop。
所以,在调用consume之后,无法再次调用,因为movable的所有权已经不在了。这种闭包在Rust中的类型为FnOnce
编写类型说明
简单来说,闭包的类型描述了它捕获外部变量能力的上界,如类型为FnOnce的闭包,其内部可以通过&T(不可变借用)、&mut T(可变借用)和T(移动所有权)的方式来捕获外部变量,而FnMut则不能通过T(移动所有权)的方式捕获外部变量。
5.Rust数据类型
5.1 内置
定义已存在数据类型的别名,语法: type Name = ExistingType;
.
type Meters = u32;
5.2 slice
5.3 集合
集合(Collection)是数据结构中最普遍的数据存放形式,Rust 标准库中提供了丰富的集合类型帮助开发者处理数据结构的操作。
向量(Vector)是一个存放多值的单数据结构,该结构将相同类型的值线性的存放在内存中。
向量是线性表,在 Rust 中的表示是 Vec<T>。
向量的使用方式类似于列表(List),我们可以通过这种方式创建指定类型的向量:
let vector: Vec<i32> = Vec::new(); // 创建类型为 i32 的空向量
let vector = vec![1, 2, 4, 8]; // 通过数组创建向量
我们使用线性表常常会用到追加的操作,但是追加和栈的 push 操作本质是一样的,所以向量只有 push 方法来追加单个元素:
1 fn main() { 2 let mut vector = vec![1, 2, 4, 8]; 3 vector.push(16); 4 vector.push(32); 5 vector.push(64); 6 println!("{:?}", vector); 7 }
输出:[1, 2, 4, 8, 16, 32, 64]
除了”{:?}"”,也可以使用Debug trait #[derive(Debug)] 或者用dbg!()打印整个结构体
append 方法用于将一个向量拼接到另一个向量的尾部:
1 fn main() { 2 let mut v1: Vec<i32> = vec![1, 2, 4, 8]; 3 let mut v2: Vec<i32> = vec![16, 32, 64]; 4 v1.append(&mut v2); 5 println!("{:?}", v1); 6 }
输出:[1, 2, 4, 8, 16, 32, 64]
get 方法用于取出向量中的值:
1 fn main() { 2 let mut v = vec![1, 2, 4, 8]; 3 println!("{}", match v.get(0) { 4 Some(value) => value.to_string(), 5 None => "None".to_string() 6 }); 7 }
输出:1
如果你能够保证取值的下标不会超出向量下标取值范围,你也可以使用数组取值语法:v[1]
映射表(Map)在其他语言中广泛存在。其中应用最普遍的就是键值散列映射表(Hash Map)。
1 use std::collections::HashMap; 2 3 fn main() { 4 let mut map = HashMap::new(); 5 6 map.insert("color", "red"); 7 map.insert("size", "10 m^2"); 8 9 println!("{}", map.get("color").unwrap()); 10 }
注意:这里没有声明散列表的泛型,是因为 Rust 的自动判断类型机制。
insert 方法和 get 方法是映射表最常用的两个方法。
Rust 的映射表是十分方便的数据结构,当使用 insert 方法添加新的键值对的时候,如果已经存在相同的键,会直接覆盖对应的值。如果你想"安全地插入",就是在确认当前不存在某个键时才执行的插入动作,可以这样:
map.entry("color").or_insert("red");
在已经确定有某个键的情况下如果想直接修改对应的值,有更快的办法:
1 use std::collections::HashMap; 2 3 fn main() { 4 let mut map = HashMap::new(); 5 map.insert(1, "a"); 6 7 if let Some(x) = map.get_mut(&1) { 8 *x = "b"; 9 } 10 }
字符串
这里不再赘述用法,重点讲解String与str区别
Let s=”hello”;
其中,"hello" 的数据类型是 str,变量 s 的数据类型是 &str。
在 rust 中,s 是切片。切片是一个结构体,包含两个字段,一个是指向数据的指针,另一个是数据的长度。
String
切片 &str 虽然可以安全使用,但是,我们很难动态修改其内容 —— 其地址、长度都是固定的。于是 rust 提供了数据类型 String
String 包含了数据指针、数组容量、数据长度等三个字段。如果新修改的数据长度在其容量范围内,数据可以原地修改。如果新修改的数据长度超出了容量范围,它可以重新申请更大的内存。
于是我们看到,String 和 &str 是两个完全不一样的结构体。为什么字符串要保留这两种形式?原因就是效率。rust 希望在数组容量不会变化的时候,用 &str。在数组长度可能发生变化的情况下,使用 String。
&str 转 String
可以用 &str 的 to_string() 方法,或者用 String::from() 方法。例如:
String 转 &str
很有意思,在 rust 中,凡是需要用 &str 的地方,都可以直接用 &String 类型的数据。
事实上,上述转换是借助于 deref coercing 这个特性实现的。如果我们自定义的数据类型也想实现类似的自动转换,实现这个特性即可。
规则很简单,一般情况下,&str 用于只读数据,String 用于可修改的数据。
6.Rust函数
7.Rust条件语句
8.Rust循环
while 循环
1 fn main() { 2 let mut number = 1; 3 while number != 4 { 4 println!("{}", number); 5 number += 1; 6 } 7 println!("EXIT"); 8 }
Rust 语言到此教程编撰之日还没有 do-while 的用法,但是 do 被规定为保留字,也许以后的版本中会用到。
for 循环常用来遍历一个线性数据结构(比如数组)
for 循环遍历数组:
1 fn main() { 2 let a = [10, 20, 30, 40, 50]; 3 for i in a.iter() { 4 println!("值为 : {}", i); 5 } 6 }
当然,for 循环其实是可以通过下标来访问数组的:
1 fn main() { 2 let a = [10, 20, 30, 40, 50]; 3 for i in 0..5 { 4 println!("a[{}] = {}", i, a[i]); 5 } 6 }
备注:
1)…定义一个Range;
2)…放在一个对象前面,将对象的成员展开(只能写在大括号中的最右侧)
Rust 语言有原生的无限循环结构 —— loop:
1 fn main() { 2 let s = ['R', 'U', 'N', 'O', 'O', 'B']; 3 let mut i = 0; 4 loop { 5 let ch = s[i]; 6 if ch == 'O' { 7 break; 8 } 9 println!("\'{}\'", ch); 10 i += 1; 11 } 12 }
9.Rust所有权
10.Rust结构体
结构体是由用户定义的一种复合类型,我们知道不同的语言使用不同的机制在计算机内存中布局数据,这样 Rust 编译器可能会执行某些优化而导致类型布局有所不同,无法和其他语言编写的程序正确交互。
类型布局(Type layout),是指类型在内存中的排列方式,是其数据在内存中的大小,对齐方式以及其字段的相对偏移量。当数据自然对齐时,CPU 可以最有效地执行内存读写。
repr属性
为了解决上述问题,Rust 引入了repr属性来指定类型的内存布局,该属性支持的值有:
#[repr(Rust)],默认布局或不指定repr属性。
#[repr(C)],C 布局,这告诉编译器"像C那样对类型布局",可使用在结构体,枚举和联合类型。
#[repr(transparent)],此布局仅可应用于结构体为:
包含单个非零大小的字段( newtype-like ),以及
任意数量的大小为 0 且对齐方式为 1 的字段(例如PhantomData<T>)
#[repr(u*)],#[repr(i*)],原始整型的表示形式,如:u8,i32,isize等,仅可应用于枚举。
结构体的成员总是按照指定的顺序存放在内存中,由于各种类型的对齐要求,通常需要填充以确保成员以适当对齐的字节开始。对于 1 和 2 ,可以分别使用对齐修饰符align和packed来提高或降低其对齐方式。使用repr属性,只可以更改其字段之间的填充,但不能更改字段本身的内存布局。repr(packed)可能导致未定义的行为,不要轻易使用。
总结
在 Rust 中调用 C 库,进行 Rust FFI 绑定:
传递结构体类型的参数时,可以使用repr属性#[repr(C)]确保有一致的内存布局。
对于 C 库中的 Opaque 结构体类型的参数,在 Rust 中可以使用一个拥有私有字段的结构体来表示。
11.Rust枚举类
12. Rust 工程管理
对于一个工程来讲,组织代码是十分重要的。
Rust 中有三个重要的组织概念:箱、包、模块。
箱(Crate)
"箱"是二进制程序文件或者库文件,存在于"包"中。
"箱"是树状结构的,它的树根是编译器开始运行时编译的源文件所编译的程序。
注意:"二进制程序文件"不一定是"二进制可执行文件",只能确定是是包含目标机器语言的文件,
文件格式随编译环境的不同而不同。
包(Package)
当我们使用 Cargo 执行 new 命令创建 Rust 工程时,工程目录下会建立一个 Cargo.toml
文件。
工程的实质就是一个包,包必须由一个 Cargo.toml 文件来管理,该文件描述了包的基本信息以
及依赖项。
一个包最多包含一个库"箱",可以包含任意数量的二进制"箱",但是至少包含一个"箱"(不管是库
还是二进制"箱")。
当使用 cargo new 命令创建完包之后,src 目录下会生成一个 main.rs 源文件,Cargo 默认
这个文件为二进制箱的根,编译之后的二进制箱将与包名相同。
模块(Module)
对于一个软件工程来说,我们往往按照所使用的编程语言的组织规范来进行组织,组织模块的主要结构
往往是树。Java 组织功能模块的主要单位是类,而 JavaScript 组织模块的主要方式是 function。
这些先进的语言的组织单位可以层层包含,就像文件系统的目录结构一样。Rust 中的组织单位是模块(Module)。
访问权限
Rust 中有两种简单的访问权:公共(public)和私有(private)。
默认情况下,如果不加修饰符,模块中的成员访问权将是私有的。
如果想使用公共权限,需要使用 pub 关键字。
对于私有的模块,只有在与其平级的位置或下级的位置才能访问,不能从其外部访问。
1 mod nation { 2 pub mod government { 3 pub fn govern() {} 4 } 5 6 mod congress { 7 pub fn legislate() {} 8 } 9 10 mod court { 11 fn judicial() { 12 super::congress::legislate(); 13 } 14 } 15 } 16 17 fn main() { 18 nation::government::govern(); 19 }
这段程序是能通过编译的。请注意观察 court 模块中 super 的访问方法。
如果模块中定义了结构体,结构体除了其本身是私有的以外,其字段也默认是私有的。所以如果想使用模块中的结构体以及其字段,需要 pub 声明:
枚举类枚举项可以内含字段,但不具备类似的访问权限性质:
1 mod SomeModule { 2 pub enum Person { 3 King { 4 name: String 5 }, 6 Quene 7 } 8 } 9 10 fn main() { 11 let person = SomeModule::Person::King{ 12 name: String::from("Blue") 13 }; 14 match person { 15 SomeModule::Person::King {name} => { 16 println!("{}", name); 17 } 18 _ => {} 19 } 20 }
每一个 Rust 文件的内容都是一个"难以发现"的模块。
main.rs 文件
1 // main.rs 2 mod second_module; 3 4 fn main() { 5 println!("This is the main module."); 6 println!("{}", second_module::message()); 7 } 8 9 10 11 second_module.rs 文件 12 // second_module.rs 13 pub fn message() -> String { 14 String::from("This is the 2nd module.") 15 }
use 关键字
use 关键字能够将模块标识符引入当前作用域:
use 关键字把标识符导入到了当前的模块下,可以直接使用,能够解决了局部模块路径过长的问题
1 mod nation { 2 pub mod government { 3 pub fn govern() {} 4 } 5 pub fn govern() {} 6 } 7 8 use crate::nation::government::govern; 9 use crate::nation::govern as nation_govern; 10 11 fn main() { 12 nation_govern(); 13 govern(); 14 }
有些情况下存在两个相同的名称,且同样需要导入,我们可以使用 as 关键字为标识符添加别名:
引用标准库
use std::f64::consts::PI;
13. Rust错误处理
对于不可恢复错误使用 panic! 宏来处理。
1 fn main() { 2 panic!("error occured"); 3 println!("Hello, Rust"); 4 }
在 panic! 宏被调用时停止了运行,所以程序并不能如约运行到 println!("Hello, Rust") 。
对于可恢复错误用 Result<T, E> 类来处理
1 enum Result<T, E> { 2 Ok(T), 3 Err(E), 4 }
Rust的异常处理是通过 Result 的 Ok 和 Err 成员来传递和包裹错误信息.
然而错误信息的处理一般都是要通过match来对类型进行比较, 所以很多时候代码比较冗余, 通过?符号来简化Ok和Err的判断。
- 使用?的函数的返回值必须是Result的结构。
The ? Operator Can Be Used in Functions That Return Result
- 有一些库是有自己返回值到Result的转换的,例如
nom
如果想使一个可恢复错误按不可恢复错误处理,Result 类提供了两个办法:unwrap() 和 expect(message: &str) :
1 use std::fs::File; 2 fn main() { 3 let f1 = File::open("rust.txt").unwrap(); 4 let f2 = File::open("rust.txt").expect("Failed to open."); 5 }
这段程序相当于在 Result 为 Err 时调用 panic! 宏。两者的区别在于 expect 能够向 panic! 宏发送一段指定的错误信息。
可恢复的错误的传递
编写一个函数在遇到错误时想传递出去,Rust 中可以在 Result 对象后添加 ? 操作符将同类的 Err 直接传递出去:
1 fn f(i: i32) -> Result<i32, bool> { 2 if i >= 0 { Ok(i) } 3 else { Err(false) } 4 } 5 fn g(i: i32) -> Result<i32, bool> { 6 let t = f(i)?; 7 Ok(t) // 因为确定 t 不是 Err, t 在这里已经是 i32 类型 8 } 9 fn main() { 10 let r = g(10000); 11 if let Ok(v) = r { 12 println!("Ok: g(10000) = {}", v); 13 } else { 14 println!("Err"); 15 } 16 }
? 符的实际作用是将 Result 类非异常的值直接取出,如果有异常就将异常 Result 返回出去。所以,? 符仅用于返回值类型为 Result<T, E> 的函数,其中 E 类型必须和 ? 所处理的 Result 的 E 类型一致。
kind 方法
到此为止,Rust 似乎没有像 try 块一样可以令任何位置发生的同类异常都直接得到相同的解决的语法,但这样并不意味着 Rust 实现不了:我们完全可以把 try 块在独立的函数中实现,将所有的异常都传递出去解决。实际上这才是一个分化良好的程序应当遵循的编程方法:应该注重独立功能的完整性。
但是这样需要判断 Result 的 Err 类型,获取 Err 类型的函数是 kind()。
1 use std::io; 2 use std::io::Read; 3 use std::fs::File; 4 5 fn read_text_from_file(path: &str) -> Result<String, io::Error> { 6 let mut f = File::open(path)?; 7 let mut s = String::new(); 8 f.read_to_string(&mut s)?; 9 Ok(s) 10 } 11 12 fn main() { 13 let str_file = read_text_from_file("hello.txt"); 14 match str_file { 15 Ok(s) => println!("{}", s), 16 Err(e) => { 17 match e.kind() { 18 io::ErrorKind::NotFound => { 19 println!("No such file"); 20 }, 21 _ => { 22 println!("Cannot read the file"); 23 } 24 } 25 } 26 } 27 }
14. Rust泛型
Rust 泛型
泛型是一个编程语言不可或缺的机制。泛型机制是编程语言用于表达类型抽象的机制,一般用于功能确定、数据类型待定的类,如链表、映射表等。rust库中Option 和 Result 枚举类就是泛型的。Rust 中的结构体,方法和枚举类都可以实现泛型机制。
1 struct Point<T> { 2 x: T, 3 y: T, 4 } 5 6 impl<T> Point<T> { 7 fn x(&self) -> &T { 8 &self.x 9 } 10 } 11 12 fn main() { 13 let p = Point { x: 1, y: 2 }; 14 println!("p.x = {}", p.x()); 15 }
注意,impl 关键字的后方必须有 <T>,因为它后面的 T 是以之为榜样的。但我们也可以为其中的一种泛型添加方法:
1 impl Point<f64> { 2 fn x(&self) -> f64 { 3 self.x 4 }}
impl 块本身的泛型并没有阻碍其内部方法具有泛型的能力:
1 impl<T, U> Point<T, U> { 2 fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> { 3 Point { 4 x: self.x, 5 y: other.y, 6 } 7 }}
方法 mixup 将一个 Point<T, U> 点的 x 与 Point<V, W> 点的 y 融合成一个类型为 Point<T, W> 的新点。
特性
特性(trait)概念接近于 Java 中的接口(Interface),但两者不完全相同。特性与接口相同的地方在于它们都是一种行为规范,可以用于标识哪些类有哪些方法。
特性在 Rust 中用 trait 表示:
格式是:impl <特性名> for <所实现的类型名>
Rust 同一个类可以实现多个特性,每个 impl 块只能实现一个。
1 trait Descriptive { 2 fn describe(&self) -> String;}
Descriptive 规定了实现者必需有 describe(&self) -> String 方法。
我们用它实现一个结构体:
1 struct Person { 2 name: String, 3 age: u8 4 } 5 6 impl Descriptive for Person { 7 fn describe(&self) -> String { 8 format!("{} {}", self.name, self.age) 9 } 10 }
默认特性
这是特性与接口的不同点:接口只能规范方法而不能定义方法,但特性可以定义方法作为默认方法,因为是"默认",所以对象既可以重新定义方法,也可以不重新定义方法使用默认的方法:
1 trait Descriptive { 2 fn describe(&self) -> String { 3 String::from("[Object]") 4 } 5 } 6 7 struct Person { 8 name: String, 9 age: u8 10 } 11 12 impl Descriptive for Person { 13 fn describe(&self) -> String { 14 format!("{} {}", self.name, self.age) 15 } 16 } 17 18 fn main() { 19 let cali = Person { 20 name: String::from("Cali"), 21 age: 24 22 }; 23 println!("{}", cali.describe()); 24 }
特性做参数
很多情况下我们需要传递一个函数做参数,例如回调函数、设置按钮事件等。在 Java 中函数必须以接口实现的类实例来传递,在 Rust 中可以通过传递特性参数来实现:
1 fn output(object: impl Descriptive) { 2 println!("{}", object.describe()); 3 }
任何实现了 Descriptive 特性的对象都可以作为这个函数的参数,这个函数没必要了解传入对象有没有其他属性或方法,只需要了解它一定有 Descriptive 特性规范的方法就可以了。当然,此函数内也无法使用其他的属性与方法。
特性参数还可以用这种等效语法实现:
1 fn output<T: Descriptive>(object: T) { 2 println!("{}", object.describe());}
这是一种风格类似泛型的语法糖,这种语法糖在有多个参数类型均是特性的情况下十分实用:
1 fn output_two<T: Descriptive>(arg1: T, arg2: T) { 2 println!("{}", arg1.describe()); 3 println!("{}", arg2.describe());}
特性作类型表示时如果涉及多个特性,可以用 + 符号表示,例如:
1 fn notify(item: impl Summary + Display) 2 fn notify<T: Summary + Display>(item: T)
注意:仅用于表示类型的时候,并不意味着可以在 impl 块中使用。
复杂的实现关系可以使用 where 关键字简化,例如:
fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U)
可以简化成:
1 fn some_function<T, U>(t: T, u: U) -> i32 2 where T: Display + Clone, 3 U: Clone + Debug
实现取最大值:
1 trait Comparable { 2 fn compare(&self, object: &Self) -> i8; 3 } 4 5 fn max<T: Comparable>(array: &[T]) -> &T { 6 let mut max_index = 0; 7 let mut i = 1; 8 while i < array.len() { 9 if array[i].compare(&array[max_index]) > 0 { 10 max_index = i; 11 } 12 i += 1; 13 } 14 &array[max_index] 15 } 16 17 impl Comparable for f64 { 18 fn compare(&self, object: &f64) -> i8 { 19 if &self > &object { 1 } 20 else if &self == &object { 0 } 21 else { -1 } 22 } 23 } 24 25 fn main() { 26 let arr = [1.0, 3.0, 5.0, 4.0, 2.0]; 27 println!("maximum of arr is {}", max(&arr)); 28 }
Tip: 由于需要声明 compare 函数的第二参数必须与实现该特性的类型相同,所以 Self (注意大小写)关键字就代表了当前类型(不是实例)本身。
特性做返回值
1 fn person() -> impl Descriptive { 2 Person { 3 name: String::from("Cali"), 4 age: 24 5 } 6 }
特性做返回值只接受实现了该特性的对象做返回值且在同一个函数中所有可能的返回值类型必须完全一样。
15. Rust生命周期
16. Rust文件及IO
17.Rust集合与字符串
18.Rust面向对象
19.Rust并发编程
20.Rust网络编程
21. Rust GUI
22. Rust web