学了常量,变量,数据类型,控制流,所有权
char
的宽度是4字节,一个 unicode 的宽度- 控制流条件都不要括号
- rust 中的元组使用和 c++ 中的非常相似
// clang++ test.cpp -std=c++11 && ./a.out #include <iostream> #include <string> #include <tuple> int main() { std::tuple<int, std::string> t = std::make_tuple(12, "zxh"); std::cout << std::get<0>(t) << " " << std::get<1>(t) << std::endl; std::string s; int i; std::tie(i, s) = std::make_tuple(1, "test-tie"); std::cout << i << " " << s << std::endl; } // 输出 12 zxh 1 test-tie
- 所有权
- 定义非常清晰严格,在作用域结束时生命周期结束,赋值操作会发生所有权的转移,旧值变得不可用(内建类型除外)
- 同一作用域内,不允许出现两个可变引用,可以减少产生竞态条件的情况
- 不能在拥有不可变引用的同时拥有可变引用
- 切片 slice,在使用和内部实现上和 cpp 的 string_view 或者 golang 的切片都有点类似,对于函数的入参而言使用切片和原始数据类型无差异
- 结构体 struct 用法和 cpp 中类似
- 方法定义等同于 cpp
- 关联函数的使用类似于 cpp 中的 static 函数使用
#[derive(Debug)]
打印整个结构和值
习题
华氏摄氏度温度互相转换
fn c_to_f(c: f32) -> f32 {
return 1.8 * c + 32.0;
}
fn f_to_c(f: f32) -> f32 {
return (f - 32.0) / 1.8;
}
打印斐波那契n项值
fn fibonacci(x: u32) -> u32 {
if x == 0 {
return 0;
} else if x == 1 || x == 2 {
return 1;
} else {
return fibonacci(x - 1) + fibonacci(x - 2);
}
}
TODO
类型系统的转换看起来还有点不相同,像其它的语言的类型转换,在 rust 中会报错
day2 (2021/05/28)
枚举
在 cpp 中的定义
enum Color { Read, Green };
在 rust 中这样也可以工作的很好,里面的所有元素都是相同类型的,在 c 中元素类型默认为 int, cpp 中赋予了更多的类型定义。
更进一步,rust 中允许每个元素 关联不同的数据类型,如官方教程中的
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
但是既然定义为 enum
,那表现的语义就应该是类型相同的,即使内部数据类型有差异,就可以这样使用了
#[derive(Debug)]
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn foo(m: Message) {
println!("{:?}", m)
}
fn main() {
foo(Message::Quit);
foo(Message::Move { x: 1, y: 2 });
foo(Message::Write(String::from("test-enum")));
foo(Message::ChangeColor(1, 2, 3))
}
// output:
// Quit
// Move { x: 1, y: 2 }
// Write("test-enum")
// ChangeColor(1, 2, 3)
将不同的数据类型聚集在一起来表示同一类型的语义,理论上来说是提高了抽象表达能力的;但看起来又和结构体又十分相似尤其在 cpp 中,一个父类派生出多个子类,然后可以用父类指针来表达到不同的子类上,但在 rust 这种实现用于了枚举。
Option
实现和 cpp 中的 type_traits
非常类似,在 cpp 中在编译时通过类型来匹配对应的函数。
enum Option<T> {
None,
Some(T),
}
对于一个函数的返回而言,如果需要返回空则直接返回 None
;若是需要返回一个 i32
,则返回 Some(i32)。进而函数调用后对结果进行 is_none()
的判断,如果非空,则可以使用 x.unwrap() 取出值。
再次表达和 cpp 中的 SFINAE
极其相似,看起来还有一些运行时消耗。
match
match 是一个表达式,每个分支相关联的代码是一个表达式,而表达式的结果值将作为整个 match 表达式的返回值。
=>
后每个分支的返回值类型必须相同,对于枚举的类型系统而言完成了一个闭环。
前面看到枚举可以由不同类型甚至携带不同的参数组成,开始我对枚举进行判断相同时的构想是这个样子的:
fn foo(m: Message) {
if m == Message::Quit {}
if m == Message::Write(String::from("compare")) {}
}
当然,上面的代码是行不通的,但是从这样子来看的话,类型判断还算准确,带参数的要参数才能匹配,
match 出现后,在上面相同的语义上继续放大了威力。enum 带了参数是吧,我match可以对你的参数进行解析,然后中间的过程你自己决定,对于参数而言,不一定要相等,我约等于也可以。
结合 match
的 enum
才算是真正发挥了设计的效果。
一个语法糖 if let
可以省略 match 表达式,match 中的分支跟在 if let
之后。
day3 (2021/05/30)
rust 包管
rust 的包管理系统都是围绕着 cargo
来进行的。有几个相关的概念.
- 包(Packages): 源文件和描述包的 Cargo.toml 清单文件的集合。
- Crates :一个模块的树形结构,是一个库或一个可执行程序,分别称为lib crate 或 binary crate。
- 模块(Modules)和 use: 允许你控制作用域和路径的私有性。
- 路径(path):一个命名例如结构体、函数或模块等项的方式
上面的话是官方的解释(The Book 和 The Cargo Book).
在 cpp 中,没有啥包的概念,只有一个命名空间的概念,粗略一看和 rust 中的方式差的远,golang 和 rust 同属现代语言,这里用 golang 做比较会好理解一些。
- 模块路径都好理解,理解为文件系统的路径就行。一般而言模块的名称就是文件的名称,golang 中模块名为目录名。
- ccrate 和 package 的关系为,package 可以包含至多一个 lib crate 和多个 binary crate。rust 的 crate 功能就和 golang 中的
package main
相似,可以有多个。
root 的概念
- crate root, src/main|lib.rs 就是一个与包同名的 crate 根。通过将文件放在 src/bin 目录下,一个包可以拥有多个二进制 crate.
- package root, 包的 Cargo.toml 文件所在目录
- workspace root, 工作区的 Cargo.toml 文件坐在目录
集合
vector
提供了宏的操作 vec!
,作用类似 cpp 中的初始化列表在 vector 中的使用。
由于语法限制了不能多个可变引用,所以就没有办法变出这种脑淤血的代码,这种代码在 cpp 中叫做迭代器失效,rust 直接禁止这样的写法
let mut y = vec!["z", "x", "h"];
for v in &mut y {
println!("{}", v);
y.push("sb");
}
内部实现和各种 vector 实现无异,都是 data,len,capacity
的实现,增长因子为 2.
与 cpp 比起来,多了一个 pop
操作。
字符串
rust 语言内置字符串类型:str
,字符串 slice,它通常以被借用的形式出现,&str
. 引用存储在其它别处的UTF-8的数据,目前这些数据为字面量字符串和String中的数据。
和 cpp 中的 const * char *
有点类似,但是提供了很多只读的操作,使用起来比较自然,并且是 unicode 而无需为操蛋的 char wchar 浪费精力。
fn read_str(s: &str) {}
read_str("string-literal"); // OK
let s = "&str"; read_str(s); // OK
let ss = String::from("String"); read_str(&ss); // OK, type coerced
对于需要使用到可变字符串的操作而言,需要使用 String
, 同样是 unicode 编码,底层实现为 Vec<u8>
,不过使用向量不过是方便对数据的存储,省了一遍造轮子的代码。
虽然是向量作为储存,和 cpp 那种 std::string
表面为字符串,实则为字符数组的的实现不同,String
不支持随机读取,
let name = String::from("123举个🌰");
println!("len: {}", name.len()); // len: 13
println!("{:?}", name.as_bytes());
// [49, 50, 51, 228, 184, 190, 228, 184, 170, 240, 159, 140, 176]
// 1 --> 49
// 2 --> 50
// 3 --> 51
// 举 --> 228, 184, 190
// 个 --> 190, 228, 170
// 🌰 --> 240, 159, 140, 176
String 不支持随机存取的操作,但可以使用 slice
来绕过编译器的检查,但是一个unicode码可能是多个字节组成的,当索引卡在一个字符中间时,直接 panic。
和 str
作为一个区分,String 有所有权,str 看起来并没有所有权的一些规则限制,如同i32一般(不过本就是内置类型)
HashMap
map 用着并不自然,存取操作不能使用常规的 map[key] = value
. 对于数据的获取可以说非常反人类了,map[&7]
这种东西都出来了,设计可能合理但是使用不自然。
习题
给定一系列数字,使用 vector 并返回这个列表的平均数(mean, average)、中位数(排列数组后位于中间的值)和众数(mode,出现次数最多的值)。
// 平均数
fn mean(nums: &Vec<i32>) -> Option<i32> {
if nums.len() == 0 {
return None;
}
let mut m: i32 = 0;
for n in nums.iter() {
m += n;
}
return Some(m / nums.len() as i32);
}
// 中位数
fn middle(nums: &Vec<i32>) -> Option<i32> {
if nums.len() == 0 {
return None;
}
let mut tmp = nums.clone();
tmp.sort();
return Some(tmp[tmp.len() / 2]);
}
// 众数
fn mode(nums: &Vec<i32>) -> Option<i32> {
if nums.len() == 0 {
return None;
}
let mut freq = HashMap::new();
for n in nums.iter() {
let count = freq.entry(n).or_insert(0);
*count += 1;
}
let mut key = nums[0];
let mut max: i32 = 0;
for (n, count) in freq.iter() {
if max < *count {
max = *count;
key = **n;
}
}
return Some(key);
}
day4 (2021/05/31)
panic!
中断程序的运行,使用环境变量 RUST_BACKTRACE=1
可以打印退出程序时的堆栈调用情况。
错误错误的核心结构为 enum Result.
enum Result<T, E> {
Ok(T),
Err(E),
}
对于一个函数而言,无错误发生则返回 Ok(T)
, 发生错误则返回 Err(E)
.
传播错误的简写:?
运算符,工作方式如同以下 match 的逻辑.
let _ = match foo {
Ok(file) => file,
Err(e) => return Err(e),
};
foo 为一个 Result,其值为错误时,直接作为整个函数的返回值返回。当前来看 ?
运算符只能作用于返回值类型为 Result<T, E>
的函数。
day5 (2021/06/02)
学习泛型
一个普通的 c++ 的泛型相加函数实现如下
template <typename T> T add(T lhs, T rhs) { return lhs + rhs; }
// 特化版本·
template <> int add(int lhs, int rhs) { return lhs * 2 + rhs * 2; }
在 rust 中的实现非常类似,但是这是编译不过的版本
fn add<T>(x: T, y: T) -> T { x + y }
x + y
的过程是不确认的,如果是两个自定义类型,可能是不支持相加的操作的,rust 同c++一样编译不过去。
和 c++ 不太一样的是,c++ 是实例化的时候编译错误的,rust 还没有实例化的时候就提示不对,因为需要提前标注是否支持相加的操作.
形式如下,rust 中需要提前表明这个类型需要支持相加的操作然后在内部才能够相加
fn add<T: Add<Output = T>>(x: T, y: T) -> T { x + y }
有一点类似 c++ 中的萃取技术,提前预判类型
template <typename T>
typename std::enable_if<std::is_integral<T>::value, T>::type add2(T lhs, T rhs) {
return lhs + rhs;
}
在使用上又有点类似于 interface,举个 golang 的例子,不过 golang 是运行时,如果未实现了 add 方法会 panic(简单的可以编译报错)
type Adder interface {
add(lhs, rhs int) int
}
type FuckWork struct{}
func (f *FuckWork) add(lhs, rhs int) int { return lhs*2 + rhs*2 }
func fuckAdder(adder interface{}) int {
ar := adder.(FuckWork)
return ar.add(1, 2)
}
func main() {
fw := FuckWork{}
fmt.Println(fuckAdder(fw)) // 6
}
rust 的 trait 同时吸收了 c++ 的 type_traits 机制和一般语言的 interface 设计。
泛型的设计应该满足于 trait 才能够进行,上面的 add 就需要优先表示这个类型是支持 +
的操作。
trait 的定义是一种将方法签名组合起来的方法,目的是定义一个实现某些目的所必需的行为的集合。这是官方的定义。
个人理解是为了泛型而存在的一种语言特性,在泛型中将相同的操作方法剥离出来,分别对应到不同类型实现上,是一种自底向上的实现;在使用上和 interface 极其类似,唯一的区别就是编译时。
https://www.jianshu.com/p/14607ab7af42
https://itbbs.pconline.com.cn/soft/54637926.html
https://www.wenjuan.com/s/2uMzQrk/
https://www.meipian.cn/3n7qdvv4?share_depth=1
https://blog.csdn.net/yunkeyi/article/details/117746809
https://zhuanlan.zhihu.com/p/379239733
https://www.im286.net/thread-24262930-1.html
https://xueqiu.com/4824316523/182279283
https://weibo.com/ttarticle/p/show?id=2309404646219480695313
https://tieba.baidu.com/p/7395962785
http://www.360doc.com/content/21/0609/16/74567251_981295824.shtml
以下定义了一个评估的 trait,包含了两个方法,其中一个方法默认实现。
pub trait Evaluate {
fn height(&self) -> usize;
fn weight(&self) -> usize { return 0; }
}
实现的方式和写诗一般 imple xx for XX,为 Persion 这个结构实现了 height 的 trait
struct Person {
name: String,
}
impl Evaluate for Person {
fn height(&self) -> usize {
self.name.len()
}
}
一个默认和特化实现的 trait 调用方式如下
let p = Person {name:String::from("panda")};
println!("persion {} weight {} and height {}", p.name, p.weight(), p.height());
// output:
// persion panda weight 0 and height 5
接着上面的 rust 泛型 相加的操作,实现了一个自定义的结构的泛型操作
use std::ops::Add;
#[derive(Debug)]
struct FuckWork<T> {
work_name: T,
work_type: T,
}
// Notice that the implementation uses the associated type `Output`.
impl<T: Add<Output = T>> Add for FuckWork<T> {
type Output = Self;
fn add(self, other: Self) -> Self::Output {
Self {
work_name: self.work_name + other.work_name,
work_type: self.work_type + other.work_type,
}
}
}
fn main() {
let x = FuckWork { work_name: 1, work_type: 2 };
let y = FuckWork { work_name: 2, work_type: 4 };
let xf = FuckWork { work_name: 1.0, work_type: 2.0 };
let yf = FuckWork { work_name: 2.0, work_type: 4.0 };
println!("{:?}", x + y); // FuckWork { work_name: 3, work_type: 6 }
println!("{:?}", xf + yf); // FuckWork { work_name: 3.0, work_type: 6.0 }
}
但是以上部分并不是对所有的 T 的add 操作都支持的,比如 String
,但由于并不允许进行修改,所以在添加一个的 fucker 来说明如何支持更多的相加类型操作。